##// END OF EJS Templates
docs: updated copyrights to 2019
marcink -
r620:b2eaa2a6 default
parent child Browse files
Show More
@@ -1,136 +1,136 b''
1 1 # -*- coding: utf-8 -*-
2 2 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # Copyright (C) 2014-2017 RodeCode GmbH
3 # Copyright (C) 2014-2019 RodeCode GmbH
4 4 #
5 5 # This program is free software; you can redistribute it and/or modify
6 6 # it under the terms of the GNU General Public License as published by
7 7 # the Free Software Foundation; either version 3 of the License, or
8 8 # (at your option) any later version.
9 9 #
10 10 # This program is distributed in the hope that it will be useful,
11 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 13 # GNU General Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License
16 16 # along with this program; if not, write to the Free Software Foundation,
17 17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 18
19 19 # Import early to make sure things are patched up properly
20 20 from setuptools import setup, find_packages
21 21
22 22 import os
23 23 import sys
24 24 import pkgutil
25 25 import platform
26 26 import codecs
27 27
28 28 try: # for pip >= 10
29 29 from pip._internal.req import parse_requirements
30 30 except ImportError: # for pip <= 9.0.3
31 31 from pip.req import parse_requirements
32 32
33 33 try: # for pip >= 10
34 34 from pip._internal.download import PipSession
35 35 except ImportError: # for pip <= 9.0.3
36 36 from pip.download import PipSession
37 37
38 38
39 39
40 40 if sys.version_info < (2, 7):
41 41 raise Exception('VCSServer requires Python 2.7 or later')
42 42
43 43 here = os.path.abspath(os.path.dirname(__file__))
44 44
45 45 # defines current platform
46 46 __platform__ = platform.system()
47 47 __license__ = 'GPL V3'
48 48 __author__ = 'RhodeCode GmbH'
49 49 __url__ = 'https://code.rhodecode.com'
50 50 is_windows = __platform__ in ('Windows',)
51 51
52 52
53 53 def _get_requirements(req_filename, exclude=None, extras=None):
54 54 extras = extras or []
55 55 exclude = exclude or []
56 56
57 57 try:
58 58 parsed = parse_requirements(
59 59 os.path.join(here, req_filename), session=PipSession())
60 60 except TypeError:
61 61 # try pip < 6.0.0, that doesn't support session
62 62 parsed = parse_requirements(os.path.join(here, req_filename))
63 63
64 64 requirements = []
65 65 for ir in parsed:
66 66 if ir.req and ir.name not in exclude:
67 67 requirements.append(str(ir.req))
68 68 return requirements + extras
69 69
70 70
71 71 # requirements extract
72 72 setup_requirements = ['pytest-runner']
73 73 install_requirements = _get_requirements(
74 74 'requirements.txt', exclude=['setuptools'])
75 75 test_requirements = _get_requirements(
76 76 'requirements_test.txt', extras=['configobj'])
77 77
78 78
79 79 def get_version():
80 80 version = pkgutil.get_data('vcsserver', 'VERSION')
81 81 return version.strip()
82 82
83 83
84 84 # additional files that goes into package itself
85 85 package_data = {
86 86 '': ['*.txt', '*.rst'],
87 87 'configs': ['*.ini'],
88 88 'vcsserver': ['VERSION'],
89 89 }
90 90
91 91 description = 'Version Control System Server'
92 92 keywords = ' '.join([
93 93 'CLI', 'RhodeCode', 'RhodeCode Enterprise', 'RhodeCode Tools'])
94 94
95 95 # README/DESCRIPTION generation
96 96 readme_file = 'README.rst'
97 97 changelog_file = 'CHANGES.rst'
98 98 try:
99 99 long_description = codecs.open(readme_file).read() + '\n\n' + \
100 100 codecs.open(changelog_file).read()
101 101 except IOError as err:
102 102 sys.stderr.write(
103 103 "[WARNING] Cannot find file specified as long_description (%s)\n "
104 104 "or changelog (%s) skipping that file" % (readme_file, changelog_file))
105 105 long_description = description
106 106
107 107
108 108 setup(
109 109 name='rhodecode-vcsserver',
110 110 version=get_version(),
111 111 description=description,
112 112 long_description=long_description,
113 113 keywords=keywords,
114 114 license=__license__,
115 115 author=__author__,
116 116 author_email='admin@rhodecode.com',
117 117 url=__url__,
118 118 setup_requires=setup_requirements,
119 119 install_requires=install_requirements,
120 120 tests_require=test_requirements,
121 121 zip_safe=False,
122 122 packages=find_packages(exclude=["docs", "tests*"]),
123 123 package_data=package_data,
124 124 include_package_data=True,
125 125 classifiers=[
126 126 'Development Status :: 6 - Mature',
127 127 'Intended Audience :: Developers',
128 128 'Operating System :: OS Independent',
129 129 'Topic :: Software Development :: Version Control',
130 130 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
131 131 'Programming Language :: Python :: 2.7',
132 132 ],
133 133 entry_points={
134 134 'paste.app_factory': ['main=vcsserver.http_main:main']
135 135 },
136 136 )
@@ -1,28 +1,28 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import pkgutil
19 19
20 20
21 21 __version__ = pkgutil.get_data('vcsserver', 'VERSION').strip()
22 22
23 23 # link to config for pyramid
24 24 CONFIG = {}
25 25
26 26 # Populated with the settings dictionary from application init in
27 27 #
28 28 PYRAMID_SETTINGS = {}
@@ -1,91 +1,91 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import sys
19 19 import traceback
20 20 import logging
21 21 import urlparse
22 22
23 23 from vcsserver.lib.rc_cache import region_meta
24 24 log = logging.getLogger(__name__)
25 25
26 26
27 27 class RepoFactory(object):
28 28 """
29 29 Utility to create instances of repository
30 30
31 31 It provides internal caching of the `repo` object based on
32 32 the :term:`call context`.
33 33 """
34 34 repo_type = None
35 35
36 36 def __init__(self):
37 37 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
38 38
39 39 def _create_config(self, path, config):
40 40 config = {}
41 41 return config
42 42
43 43 def _create_repo(self, wire, create):
44 44 raise NotImplementedError()
45 45
46 46 def repo(self, wire, create=False):
47 47 """
48 48 Get a repository instance for the given path.
49 49
50 50 Uses internally the low level beaker API since the decorators introduce
51 51 significant overhead.
52 52 """
53 53 region = self._cache_region
54 54 context = wire.get('context', None)
55 55 repo_path = wire.get('path', '')
56 56 context_uid = '{}'.format(context)
57 57 cache = wire.get('cache', True)
58 58 cache_on = context and cache
59 59
60 60 @region.conditional_cache_on_arguments(condition=cache_on)
61 61 def create_new_repo(_repo_type, _repo_path, _context_uid):
62 62 return self._create_repo(wire, create)
63 63
64 64 repo = create_new_repo(self.repo_type, repo_path, context_uid)
65 65 return repo
66 66
67 67
68 68 def obfuscate_qs(query_string):
69 69 if query_string is None:
70 70 return None
71 71
72 72 parsed = []
73 73 for k, v in urlparse.parse_qsl(query_string, keep_blank_values=True):
74 74 if k in ['auth_token', 'api_key']:
75 75 v = "*****"
76 76 parsed.append((k, v))
77 77
78 78 return '&'.join('{}{}'.format(
79 79 k, '={}'.format(v) if v else '') for k, v in parsed)
80 80
81 81
82 82 def raise_from_original(new_type):
83 83 """
84 84 Raise a new exception type with original args and traceback.
85 85 """
86 86 exc_type, exc_value, exc_traceback = sys.exc_info()
87 87
88 88 try:
89 89 raise new_type(*exc_value.args), None, exc_traceback
90 90 finally:
91 91 del exc_traceback
@@ -1,116 +1,116 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 """
19 19 Special exception handling over the wire.
20 20
21 21 Since we cannot assume that our client is able to import our exception classes,
22 22 this module provides a "wrapping" mechanism to raise plain exceptions
23 23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
24 24 different error conditions.
25 25 """
26 26
27 27 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
28 28
29 29
30 30 def _make_exception(kind, org_exc, *args):
31 31 """
32 32 Prepares a base `Exception` instance to be sent over the wire.
33 33
34 34 To give our caller a hint what this is about, it will attach an attribute
35 35 `_vcs_kind` to the exception.
36 36 """
37 37 exc = Exception(*args)
38 38 exc._vcs_kind = kind
39 39 exc._org_exc = org_exc
40 40 return exc
41 41
42 42
43 43 def AbortException(org_exc=None):
44 44 def _make_exception_wrapper(*args):
45 45 return _make_exception('abort', org_exc, *args)
46 46 return _make_exception_wrapper
47 47
48 48
49 49 def ArchiveException(org_exc=None):
50 50 def _make_exception_wrapper(*args):
51 51 return _make_exception('archive', org_exc, *args)
52 52 return _make_exception_wrapper
53 53
54 54
55 55 def LookupException(org_exc=None):
56 56 def _make_exception_wrapper(*args):
57 57 return _make_exception('lookup', org_exc, *args)
58 58 return _make_exception_wrapper
59 59
60 60
61 61 def VcsException(org_exc=None):
62 62 def _make_exception_wrapper(*args):
63 63 return _make_exception('error', org_exc, *args)
64 64 return _make_exception_wrapper
65 65
66 66
67 67 def RepositoryLockedException(org_exc=None):
68 68 def _make_exception_wrapper(*args):
69 69 return _make_exception('repo_locked', org_exc, *args)
70 70 return _make_exception_wrapper
71 71
72 72
73 73 def RepositoryBranchProtectedException(org_exc=None):
74 74 def _make_exception_wrapper(*args):
75 75 return _make_exception('repo_branch_protected', org_exc, *args)
76 76 return _make_exception_wrapper
77 77
78 78
79 79 def RequirementException(org_exc=None):
80 80 def _make_exception_wrapper(*args):
81 81 return _make_exception('requirement', org_exc, *args)
82 82 return _make_exception_wrapper
83 83
84 84
85 85 def UnhandledException(org_exc=None):
86 86 def _make_exception_wrapper(*args):
87 87 return _make_exception('unhandled', org_exc, *args)
88 88 return _make_exception_wrapper
89 89
90 90
91 91 def URLError(org_exc=None):
92 92 def _make_exception_wrapper(*args):
93 93 return _make_exception('url_error', org_exc, *args)
94 94 return _make_exception_wrapper
95 95
96 96
97 97 def SubrepoMergeException(org_exc=None):
98 98 def _make_exception_wrapper(*args):
99 99 return _make_exception('subrepo_merge_error', org_exc, *args)
100 100 return _make_exception_wrapper
101 101
102 102
103 103 class HTTPRepoLocked(HTTPLocked):
104 104 """
105 105 Subclass of HTTPLocked response that allows to set the title and status
106 106 code via constructor arguments.
107 107 """
108 108 def __init__(self, title, status_code=None, **kwargs):
109 109 self.code = status_code or HTTPLocked.code
110 110 self.title = title
111 111 super(HTTPRepoLocked, self).__init__(**kwargs)
112 112
113 113
114 114 class HTTPRepoBranchProtected(HTTPForbidden):
115 115 def __init__(self, *args, **kwargs):
116 116 super(HTTPForbidden, self).__init__(*args, **kwargs)
@@ -1,728 +1,728 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17 import collections
18 18 import logging
19 19 import os
20 20 import posixpath as vcspath
21 21 import re
22 22 import stat
23 23 import traceback
24 24 import urllib
25 25 import urllib2
26 26 from functools import wraps
27 27
28 28 from dulwich import index, objects
29 29 from dulwich.client import HttpGitClient, LocalGitClient
30 30 from dulwich.errors import (
31 31 NotGitRepository, ChecksumMismatch, WrongObjectException,
32 32 MissingCommitError, ObjectMissing, HangupException,
33 33 UnexpectedCommandError)
34 34 from dulwich.repo import Repo as DulwichRepo, Tag
35 35 from dulwich.server import update_server_info
36 36
37 37 from vcsserver import exceptions, settings, subprocessio
38 38 from vcsserver.utils import safe_str
39 39 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
40 40 from vcsserver.hgcompat import (
41 41 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
42 42 from vcsserver.git_lfs.lib import LFSOidStore
43 43
44 44 DIR_STAT = stat.S_IFDIR
45 45 FILE_MODE = stat.S_IFMT
46 46 GIT_LINK = objects.S_IFGITLINK
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 def reraise_safe_exceptions(func):
52 52 """Converts Dulwich exceptions to something neutral."""
53 53 @wraps(func)
54 54 def wrapper(*args, **kwargs):
55 55 try:
56 56 return func(*args, **kwargs)
57 57 except (ChecksumMismatch, WrongObjectException, MissingCommitError,
58 58 ObjectMissing) as e:
59 59 exc = exceptions.LookupException(e)
60 60 raise exc(e)
61 61 except (HangupException, UnexpectedCommandError) as e:
62 62 exc = exceptions.VcsException(e)
63 63 raise exc(e)
64 64 except Exception as e:
65 65 # NOTE(marcink): becuase of how dulwich handles some exceptions
66 66 # (KeyError on empty repos), we cannot track this and catch all
67 67 # exceptions, it's an exceptions from other handlers
68 68 #if not hasattr(e, '_vcs_kind'):
69 69 #log.exception("Unhandled exception in git remote call")
70 70 #raise_from_original(exceptions.UnhandledException)
71 71 raise
72 72 return wrapper
73 73
74 74
75 75 class Repo(DulwichRepo):
76 76 """
77 77 A wrapper for dulwich Repo class.
78 78
79 79 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
80 80 "Too many open files" error. We need to close all opened file descriptors
81 81 once the repo object is destroyed.
82 82
83 83 TODO: mikhail: please check if we need this wrapper after updating dulwich
84 84 to 0.12.0 +
85 85 """
86 86 def __del__(self):
87 87 if hasattr(self, 'object_store'):
88 88 self.close()
89 89
90 90
91 91 class GitFactory(RepoFactory):
92 92 repo_type = 'git'
93 93
94 94 def _create_repo(self, wire, create):
95 95 repo_path = str_to_dulwich(wire['path'])
96 96 return Repo(repo_path)
97 97
98 98
99 99 class GitRemote(object):
100 100
101 101 def __init__(self, factory):
102 102 self._factory = factory
103 103 self.peeled_ref_marker = '^{}'
104 104 self._bulk_methods = {
105 105 "author": self.commit_attribute,
106 106 "date": self.get_object_attrs,
107 107 "message": self.commit_attribute,
108 108 "parents": self.commit_attribute,
109 109 "_commit": self.revision,
110 110 }
111 111
112 112 def _wire_to_config(self, wire):
113 113 if 'config' in wire:
114 114 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
115 115 return {}
116 116
117 117 def _assign_ref(self, wire, ref, commit_id):
118 118 repo = self._factory.repo(wire)
119 119 repo[ref] = commit_id
120 120
121 121 def _remote_conf(self, config):
122 122 params = [
123 123 '-c', 'core.askpass=""',
124 124 ]
125 125 ssl_cert_dir = config.get('vcs_ssl_dir')
126 126 if ssl_cert_dir:
127 127 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
128 128 return params
129 129
130 130 @reraise_safe_exceptions
131 131 def add_object(self, wire, content):
132 132 repo = self._factory.repo(wire)
133 133 blob = objects.Blob()
134 134 blob.set_raw_string(content)
135 135 repo.object_store.add_object(blob)
136 136 return blob.id
137 137
138 138 @reraise_safe_exceptions
139 139 def assert_correct_path(self, wire):
140 140 path = wire.get('path')
141 141 try:
142 142 self._factory.repo(wire)
143 143 except NotGitRepository as e:
144 144 tb = traceback.format_exc()
145 145 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
146 146 return False
147 147
148 148 return True
149 149
150 150 @reraise_safe_exceptions
151 151 def bare(self, wire):
152 152 repo = self._factory.repo(wire)
153 153 return repo.bare
154 154
155 155 @reraise_safe_exceptions
156 156 def blob_as_pretty_string(self, wire, sha):
157 157 repo = self._factory.repo(wire)
158 158 return repo[sha].as_pretty_string()
159 159
160 160 @reraise_safe_exceptions
161 161 def blob_raw_length(self, wire, sha):
162 162 repo = self._factory.repo(wire)
163 163 blob = repo[sha]
164 164 return blob.raw_length()
165 165
166 166 def _parse_lfs_pointer(self, raw_content):
167 167
168 168 spec_string = 'version https://git-lfs.github.com/spec'
169 169 if raw_content and raw_content.startswith(spec_string):
170 170 pattern = re.compile(r"""
171 171 (?:\n)?
172 172 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
173 173 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
174 174 ^size[ ](?P<oid_size>[0-9]+)\n
175 175 (?:\n)?
176 176 """, re.VERBOSE | re.MULTILINE)
177 177 match = pattern.match(raw_content)
178 178 if match:
179 179 return match.groupdict()
180 180
181 181 return {}
182 182
183 183 @reraise_safe_exceptions
184 184 def is_large_file(self, wire, sha):
185 185 repo = self._factory.repo(wire)
186 186 blob = repo[sha]
187 187 return self._parse_lfs_pointer(blob.as_raw_string())
188 188
189 189 @reraise_safe_exceptions
190 190 def in_largefiles_store(self, wire, oid):
191 191 repo = self._factory.repo(wire)
192 192 conf = self._wire_to_config(wire)
193 193
194 194 store_location = conf.get('vcs_git_lfs_store_location')
195 195 if store_location:
196 196 repo_name = repo.path
197 197 store = LFSOidStore(
198 198 oid=oid, repo=repo_name, store_location=store_location)
199 199 return store.has_oid()
200 200
201 201 return False
202 202
203 203 @reraise_safe_exceptions
204 204 def store_path(self, wire, oid):
205 205 repo = self._factory.repo(wire)
206 206 conf = self._wire_to_config(wire)
207 207
208 208 store_location = conf.get('vcs_git_lfs_store_location')
209 209 if store_location:
210 210 repo_name = repo.path
211 211 store = LFSOidStore(
212 212 oid=oid, repo=repo_name, store_location=store_location)
213 213 return store.oid_path
214 214 raise ValueError('Unable to fetch oid with path {}'.format(oid))
215 215
216 216 @reraise_safe_exceptions
217 217 def bulk_request(self, wire, rev, pre_load):
218 218 result = {}
219 219 for attr in pre_load:
220 220 try:
221 221 method = self._bulk_methods[attr]
222 222 args = [wire, rev]
223 223 if attr == "date":
224 224 args.extend(["commit_time", "commit_timezone"])
225 225 elif attr in ["author", "message", "parents"]:
226 226 args.append(attr)
227 227 result[attr] = method(*args)
228 228 except KeyError as e:
229 229 raise exceptions.VcsException(e)(
230 230 "Unknown bulk attribute: %s" % attr)
231 231 return result
232 232
233 233 def _build_opener(self, url):
234 234 handlers = []
235 235 url_obj = url_parser(url)
236 236 _, authinfo = url_obj.authinfo()
237 237
238 238 if authinfo:
239 239 # create a password manager
240 240 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
241 241 passmgr.add_password(*authinfo)
242 242
243 243 handlers.extend((httpbasicauthhandler(passmgr),
244 244 httpdigestauthhandler(passmgr)))
245 245
246 246 return urllib2.build_opener(*handlers)
247 247
248 248 @reraise_safe_exceptions
249 249 def check_url(self, url, config):
250 250 url_obj = url_parser(url)
251 251 test_uri, _ = url_obj.authinfo()
252 252 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
253 253 url_obj.query = obfuscate_qs(url_obj.query)
254 254 cleaned_uri = str(url_obj)
255 255 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
256 256
257 257 if not test_uri.endswith('info/refs'):
258 258 test_uri = test_uri.rstrip('/') + '/info/refs'
259 259
260 260 o = self._build_opener(url)
261 261 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
262 262
263 263 q = {"service": 'git-upload-pack'}
264 264 qs = '?%s' % urllib.urlencode(q)
265 265 cu = "%s%s" % (test_uri, qs)
266 266 req = urllib2.Request(cu, None, {})
267 267
268 268 try:
269 269 log.debug("Trying to open URL %s", cleaned_uri)
270 270 resp = o.open(req)
271 271 if resp.code != 200:
272 272 raise exceptions.URLError()('Return Code is not 200')
273 273 except Exception as e:
274 274 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
275 275 # means it cannot be cloned
276 276 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
277 277
278 278 # now detect if it's proper git repo
279 279 gitdata = resp.read()
280 280 if 'service=git-upload-pack' in gitdata:
281 281 pass
282 282 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
283 283 # old style git can return some other format !
284 284 pass
285 285 else:
286 286 raise exceptions.URLError()(
287 287 "url [%s] does not look like an git" % (cleaned_uri,))
288 288
289 289 return True
290 290
291 291 @reraise_safe_exceptions
292 292 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
293 293 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
294 294 remote_refs = self.pull(wire, url, apply_refs=False)
295 295 repo = self._factory.repo(wire)
296 296 if isinstance(valid_refs, list):
297 297 valid_refs = tuple(valid_refs)
298 298
299 299 for k in remote_refs:
300 300 # only parse heads/tags and skip so called deferred tags
301 301 if k.startswith(valid_refs) and not k.endswith(deferred):
302 302 repo[k] = remote_refs[k]
303 303
304 304 if update_after_clone:
305 305 # we want to checkout HEAD
306 306 repo["HEAD"] = remote_refs["HEAD"]
307 307 index.build_index_from_tree(repo.path, repo.index_path(),
308 308 repo.object_store, repo["HEAD"].tree)
309 309
310 310 # TODO: this is quite complex, check if that can be simplified
311 311 @reraise_safe_exceptions
312 312 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
313 313 repo = self._factory.repo(wire)
314 314 object_store = repo.object_store
315 315
316 316 # Create tree and populates it with blobs
317 317 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
318 318
319 319 for node in updated:
320 320 # Compute subdirs if needed
321 321 dirpath, nodename = vcspath.split(node['path'])
322 322 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
323 323 parent = commit_tree
324 324 ancestors = [('', parent)]
325 325
326 326 # Tries to dig for the deepest existing tree
327 327 while dirnames:
328 328 curdir = dirnames.pop(0)
329 329 try:
330 330 dir_id = parent[curdir][1]
331 331 except KeyError:
332 332 # put curdir back into dirnames and stops
333 333 dirnames.insert(0, curdir)
334 334 break
335 335 else:
336 336 # If found, updates parent
337 337 parent = repo[dir_id]
338 338 ancestors.append((curdir, parent))
339 339 # Now parent is deepest existing tree and we need to create
340 340 # subtrees for dirnames (in reverse order)
341 341 # [this only applies for nodes from added]
342 342 new_trees = []
343 343
344 344 blob = objects.Blob.from_string(node['content'])
345 345
346 346 if dirnames:
347 347 # If there are trees which should be created we need to build
348 348 # them now (in reverse order)
349 349 reversed_dirnames = list(reversed(dirnames))
350 350 curtree = objects.Tree()
351 351 curtree[node['node_path']] = node['mode'], blob.id
352 352 new_trees.append(curtree)
353 353 for dirname in reversed_dirnames[:-1]:
354 354 newtree = objects.Tree()
355 355 newtree[dirname] = (DIR_STAT, curtree.id)
356 356 new_trees.append(newtree)
357 357 curtree = newtree
358 358 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
359 359 else:
360 360 parent.add(
361 361 name=node['node_path'], mode=node['mode'], hexsha=blob.id)
362 362
363 363 new_trees.append(parent)
364 364 # Update ancestors
365 365 reversed_ancestors = reversed(
366 366 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
367 367 for parent, tree, path in reversed_ancestors:
368 368 parent[path] = (DIR_STAT, tree.id)
369 369 object_store.add_object(tree)
370 370
371 371 object_store.add_object(blob)
372 372 for tree in new_trees:
373 373 object_store.add_object(tree)
374 374
375 375 for node_path in removed:
376 376 paths = node_path.split('/')
377 377 tree = commit_tree
378 378 trees = [tree]
379 379 # Traverse deep into the forest...
380 380 for path in paths:
381 381 try:
382 382 obj = repo[tree[path][1]]
383 383 if isinstance(obj, objects.Tree):
384 384 trees.append(obj)
385 385 tree = obj
386 386 except KeyError:
387 387 break
388 388 # Cut down the blob and all rotten trees on the way back...
389 389 for path, tree in reversed(zip(paths, trees)):
390 390 del tree[path]
391 391 if tree:
392 392 # This tree still has elements - don't remove it or any
393 393 # of it's parents
394 394 break
395 395
396 396 object_store.add_object(commit_tree)
397 397
398 398 # Create commit
399 399 commit = objects.Commit()
400 400 commit.tree = commit_tree.id
401 401 for k, v in commit_data.iteritems():
402 402 setattr(commit, k, v)
403 403 object_store.add_object(commit)
404 404
405 405 ref = 'refs/heads/%s' % branch
406 406 repo.refs[ref] = commit.id
407 407
408 408 return commit.id
409 409
410 410 @reraise_safe_exceptions
411 411 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
412 412 if url != 'default' and '://' not in url:
413 413 client = LocalGitClient(url)
414 414 else:
415 415 url_obj = url_parser(url)
416 416 o = self._build_opener(url)
417 417 url, _ = url_obj.authinfo()
418 418 client = HttpGitClient(base_url=url, opener=o)
419 419 repo = self._factory.repo(wire)
420 420
421 421 determine_wants = repo.object_store.determine_wants_all
422 422 if refs:
423 423 def determine_wants_requested(references):
424 424 return [references[r] for r in references if r in refs]
425 425 determine_wants = determine_wants_requested
426 426
427 427 try:
428 428 remote_refs = client.fetch(
429 429 path=url, target=repo, determine_wants=determine_wants)
430 430 except NotGitRepository as e:
431 431 log.warning(
432 432 'Trying to fetch from "%s" failed, not a Git repository.', url)
433 433 # Exception can contain unicode which we convert
434 434 raise exceptions.AbortException(e)(repr(e))
435 435
436 436 # mikhail: client.fetch() returns all the remote refs, but fetches only
437 437 # refs filtered by `determine_wants` function. We need to filter result
438 438 # as well
439 439 if refs:
440 440 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
441 441
442 442 if apply_refs:
443 443 # TODO: johbo: Needs proper test coverage with a git repository
444 444 # that contains a tag object, so that we would end up with
445 445 # a peeled ref at this point.
446 446 for k in remote_refs:
447 447 if k.endswith(self.peeled_ref_marker):
448 448 log.debug("Skipping peeled reference %s", k)
449 449 continue
450 450 repo[k] = remote_refs[k]
451 451
452 452 if refs and not update_after:
453 453 # mikhail: explicitly set the head to the last ref.
454 454 repo['HEAD'] = remote_refs[refs[-1]]
455 455
456 456 if update_after:
457 457 # we want to checkout HEAD
458 458 repo["HEAD"] = remote_refs["HEAD"]
459 459 index.build_index_from_tree(repo.path, repo.index_path(),
460 460 repo.object_store, repo["HEAD"].tree)
461 461 return remote_refs
462 462
463 463 @reraise_safe_exceptions
464 464 def sync_fetch(self, wire, url, refs=None):
465 465 repo = self._factory.repo(wire)
466 466 if refs and not isinstance(refs, (list, tuple)):
467 467 refs = [refs]
468 468 config = self._wire_to_config(wire)
469 469 # get all remote refs we'll use to fetch later
470 470 output, __ = self.run_git_command(
471 471 wire, ['ls-remote', url], fail_on_stderr=False,
472 472 _copts=self._remote_conf(config),
473 473 extra_env={'GIT_TERMINAL_PROMPT': '0'})
474 474
475 475 remote_refs = collections.OrderedDict()
476 476 fetch_refs = []
477 477
478 478 for ref_line in output.splitlines():
479 479 sha, ref = ref_line.split('\t')
480 480 sha = sha.strip()
481 481 if ref in remote_refs:
482 482 # duplicate, skip
483 483 continue
484 484 if ref.endswith(self.peeled_ref_marker):
485 485 log.debug("Skipping peeled reference %s", ref)
486 486 continue
487 487 # don't sync HEAD
488 488 if ref in ['HEAD']:
489 489 continue
490 490
491 491 remote_refs[ref] = sha
492 492
493 493 if refs and sha in refs:
494 494 # we filter fetch using our specified refs
495 495 fetch_refs.append('{}:{}'.format(ref, ref))
496 496 elif not refs:
497 497 fetch_refs.append('{}:{}'.format(ref, ref))
498 498
499 499 if fetch_refs:
500 500 _out, _err = self.run_git_command(
501 501 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs,
502 502 fail_on_stderr=False,
503 503 _copts=self._remote_conf(config),
504 504 extra_env={'GIT_TERMINAL_PROMPT': '0'})
505 505
506 506 return remote_refs
507 507
508 508 @reraise_safe_exceptions
509 509 def sync_push(self, wire, url, refs=None):
510 510 if not self.check_url(url, wire):
511 511 return
512 512 config = self._wire_to_config(wire)
513 513 repo = self._factory.repo(wire)
514 514 self.run_git_command(
515 515 wire, ['push', url, '--mirror'], fail_on_stderr=False,
516 516 _copts=self._remote_conf(config),
517 517 extra_env={'GIT_TERMINAL_PROMPT': '0'})
518 518
519 519 @reraise_safe_exceptions
520 520 def get_remote_refs(self, wire, url):
521 521 repo = Repo(url)
522 522 return repo.get_refs()
523 523
524 524 @reraise_safe_exceptions
525 525 def get_description(self, wire):
526 526 repo = self._factory.repo(wire)
527 527 return repo.get_description()
528 528
529 529 @reraise_safe_exceptions
530 530 def get_missing_revs(self, wire, rev1, rev2, path2):
531 531 repo = self._factory.repo(wire)
532 532 LocalGitClient(thin_packs=False).fetch(path2, repo)
533 533
534 534 wire_remote = wire.copy()
535 535 wire_remote['path'] = path2
536 536 repo_remote = self._factory.repo(wire_remote)
537 537 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
538 538
539 539 revs = [
540 540 x.commit.id
541 541 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
542 542 return revs
543 543
544 544 @reraise_safe_exceptions
545 545 def get_object(self, wire, sha):
546 546 repo = self._factory.repo(wire)
547 547 obj = repo.get_object(sha)
548 548 commit_id = obj.id
549 549
550 550 if isinstance(obj, Tag):
551 551 commit_id = obj.object[1]
552 552
553 553 return {
554 554 'id': obj.id,
555 555 'type': obj.type_name,
556 556 'commit_id': commit_id
557 557 }
558 558
559 559 @reraise_safe_exceptions
560 560 def get_object_attrs(self, wire, sha, *attrs):
561 561 repo = self._factory.repo(wire)
562 562 obj = repo.get_object(sha)
563 563 return list(getattr(obj, a) for a in attrs)
564 564
565 565 @reraise_safe_exceptions
566 566 def get_refs(self, wire):
567 567 repo = self._factory.repo(wire)
568 568 result = {}
569 569 for ref, sha in repo.refs.as_dict().items():
570 570 peeled_sha = repo.get_peeled(ref)
571 571 result[ref] = peeled_sha
572 572 return result
573 573
574 574 @reraise_safe_exceptions
575 575 def get_refs_path(self, wire):
576 576 repo = self._factory.repo(wire)
577 577 return repo.refs.path
578 578
579 579 @reraise_safe_exceptions
580 580 def head(self, wire, show_exc=True):
581 581 repo = self._factory.repo(wire)
582 582 try:
583 583 return repo.head()
584 584 except Exception:
585 585 if show_exc:
586 586 raise
587 587
588 588 @reraise_safe_exceptions
589 589 def init(self, wire):
590 590 repo_path = str_to_dulwich(wire['path'])
591 591 self.repo = Repo.init(repo_path)
592 592
593 593 @reraise_safe_exceptions
594 594 def init_bare(self, wire):
595 595 repo_path = str_to_dulwich(wire['path'])
596 596 self.repo = Repo.init_bare(repo_path)
597 597
598 598 @reraise_safe_exceptions
599 599 def revision(self, wire, rev):
600 600 repo = self._factory.repo(wire)
601 601 obj = repo[rev]
602 602 obj_data = {
603 603 'id': obj.id,
604 604 }
605 605 try:
606 606 obj_data['tree'] = obj.tree
607 607 except AttributeError:
608 608 pass
609 609 return obj_data
610 610
611 611 @reraise_safe_exceptions
612 612 def commit_attribute(self, wire, rev, attr):
613 613 repo = self._factory.repo(wire)
614 614 obj = repo[rev]
615 615 return getattr(obj, attr)
616 616
617 617 @reraise_safe_exceptions
618 618 def set_refs(self, wire, key, value):
619 619 repo = self._factory.repo(wire)
620 620 repo.refs[key] = value
621 621
622 622 @reraise_safe_exceptions
623 623 def remove_ref(self, wire, key):
624 624 repo = self._factory.repo(wire)
625 625 del repo.refs[key]
626 626
627 627 @reraise_safe_exceptions
628 628 def tree_changes(self, wire, source_id, target_id):
629 629 repo = self._factory.repo(wire)
630 630 source = repo[source_id].tree if source_id else None
631 631 target = repo[target_id].tree
632 632 result = repo.object_store.tree_changes(source, target)
633 633 return list(result)
634 634
635 635 @reraise_safe_exceptions
636 636 def tree_items(self, wire, tree_id):
637 637 repo = self._factory.repo(wire)
638 638 tree = repo[tree_id]
639 639
640 640 result = []
641 641 for item in tree.iteritems():
642 642 item_sha = item.sha
643 643 item_mode = item.mode
644 644
645 645 if FILE_MODE(item_mode) == GIT_LINK:
646 646 item_type = "link"
647 647 else:
648 648 item_type = repo[item_sha].type_name
649 649
650 650 result.append((item.path, item_mode, item_sha, item_type))
651 651 return result
652 652
653 653 @reraise_safe_exceptions
654 654 def update_server_info(self, wire):
655 655 repo = self._factory.repo(wire)
656 656 update_server_info(repo)
657 657
658 658 @reraise_safe_exceptions
659 659 def discover_git_version(self):
660 660 stdout, _ = self.run_git_command(
661 661 {}, ['--version'], _bare=True, _safe=True)
662 662 prefix = 'git version'
663 663 if stdout.startswith(prefix):
664 664 stdout = stdout[len(prefix):]
665 665 return stdout.strip()
666 666
667 667 @reraise_safe_exceptions
668 668 def run_git_command(self, wire, cmd, **opts):
669 669 path = wire.get('path', None)
670 670
671 671 if path and os.path.isdir(path):
672 672 opts['cwd'] = path
673 673
674 674 if '_bare' in opts:
675 675 _copts = []
676 676 del opts['_bare']
677 677 else:
678 678 _copts = ['-c', 'core.quotepath=false', ]
679 679 safe_call = False
680 680 if '_safe' in opts:
681 681 # no exc on failure
682 682 del opts['_safe']
683 683 safe_call = True
684 684
685 685 if '_copts' in opts:
686 686 _copts.extend(opts['_copts'] or [])
687 687 del opts['_copts']
688 688
689 689 gitenv = os.environ.copy()
690 690 gitenv.update(opts.pop('extra_env', {}))
691 691 # need to clean fix GIT_DIR !
692 692 if 'GIT_DIR' in gitenv:
693 693 del gitenv['GIT_DIR']
694 694 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
695 695 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
696 696
697 697 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
698 698 _opts = {'env': gitenv, 'shell': False}
699 699
700 700 try:
701 701 _opts.update(opts)
702 702 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
703 703
704 704 return ''.join(p), ''.join(p.error)
705 705 except (EnvironmentError, OSError) as err:
706 706 cmd = ' '.join(cmd) # human friendly CMD
707 707 tb_err = ("Couldn't run git command (%s).\n"
708 708 "Original error was:%s\n"
709 709 "Call options:%s\n"
710 710 % (cmd, err, _opts))
711 711 log.exception(tb_err)
712 712 if safe_call:
713 713 return '', err
714 714 else:
715 715 raise exceptions.VcsException()(tb_err)
716 716
717 717 @reraise_safe_exceptions
718 718 def install_hooks(self, wire, force=False):
719 719 from vcsserver.hook_utils import install_git_hooks
720 720 repo = self._factory.repo(wire)
721 721 return install_git_hooks(repo.path, repo.bare, force_create=force)
722 722
723 723
724 724 def str_to_dulwich(value):
725 725 """
726 726 Dulwich 0.10.1a requires `unicode` objects to be passed in.
727 727 """
728 728 return value.decode(settings.WIRE_ENCODING)
@@ -1,19 +1,19 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18
19 19 from app import create_app
@@ -1,287 +1,287 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import re
19 19 import logging
20 20 from wsgiref.util import FileWrapper
21 21
22 22 import simplejson as json
23 23 from pyramid.config import Configurator
24 24 from pyramid.response import Response, FileIter
25 25 from pyramid.httpexceptions import (
26 26 HTTPBadRequest, HTTPNotImplemented, HTTPNotFound, HTTPForbidden,
27 27 HTTPUnprocessableEntity)
28 28
29 29 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
30 30 from vcsserver.git_lfs.utils import safe_result, get_cython_compat_decorator
31 31 from vcsserver.utils import safe_int
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 GIT_LFS_CONTENT_TYPE = 'application/vnd.git-lfs' #+json ?
37 37 GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
38 38
39 39
40 40 def write_response_error(http_exception, text=None):
41 41 content_type = GIT_LFS_CONTENT_TYPE + '+json'
42 42 _exception = http_exception(content_type=content_type)
43 43 _exception.content_type = content_type
44 44 if text:
45 45 _exception.body = json.dumps({'message': text})
46 46 log.debug('LFS: writing response of type %s to client with text:%s',
47 47 http_exception, text)
48 48 return _exception
49 49
50 50
51 51 class AuthHeaderRequired(object):
52 52 """
53 53 Decorator to check if request has proper auth-header
54 54 """
55 55
56 56 def __call__(self, func):
57 57 return get_cython_compat_decorator(self.__wrapper, func)
58 58
59 59 def __wrapper(self, func, *fargs, **fkwargs):
60 60 request = fargs[1]
61 61 auth = request.authorization
62 62 if not auth:
63 63 return write_response_error(HTTPForbidden)
64 64 return func(*fargs[1:], **fkwargs)
65 65
66 66
67 67 # views
68 68
69 69 def lfs_objects(request):
70 70 # indicate not supported, V1 API
71 71 log.warning('LFS: v1 api not supported, reporting it back to client')
72 72 return write_response_error(HTTPNotImplemented, 'LFS: v1 api not supported')
73 73
74 74
75 75 @AuthHeaderRequired()
76 76 def lfs_objects_batch(request):
77 77 """
78 78 The client sends the following information to the Batch endpoint to transfer some objects:
79 79
80 80 operation - Should be download or upload.
81 81 transfers - An optional Array of String identifiers for transfer
82 82 adapters that the client has configured. If omitted, the basic
83 83 transfer adapter MUST be assumed by the server.
84 84 objects - An Array of objects to download.
85 85 oid - String OID of the LFS object.
86 86 size - Integer byte size of the LFS object. Must be at least zero.
87 87 """
88 88 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
89 89 auth = request.authorization
90 90 repo = request.matchdict.get('repo')
91 91 data = request.json
92 92 operation = data.get('operation')
93 93 if operation not in ('download', 'upload'):
94 94 log.debug('LFS: unsupported operation:%s', operation)
95 95 return write_response_error(
96 96 HTTPBadRequest, 'unsupported operation mode: `%s`' % operation)
97 97
98 98 if 'objects' not in data:
99 99 log.debug('LFS: missing objects data')
100 100 return write_response_error(
101 101 HTTPBadRequest, 'missing objects data')
102 102
103 103 log.debug('LFS: handling operation of type: %s', operation)
104 104
105 105 objects = []
106 106 for o in data['objects']:
107 107 try:
108 108 oid = o['oid']
109 109 obj_size = o['size']
110 110 except KeyError:
111 111 log.exception('LFS, failed to extract data')
112 112 return write_response_error(
113 113 HTTPBadRequest, 'unsupported data in objects')
114 114
115 115 obj_data = {'oid': oid}
116 116
117 117 obj_href = request.route_url('lfs_objects_oid', repo=repo, oid=oid)
118 118 obj_verify_href = request.route_url('lfs_objects_verify', repo=repo)
119 119 store = LFSOidStore(
120 120 oid, repo, store_location=request.registry.git_lfs_store_path)
121 121 handler = OidHandler(
122 122 store, repo, auth, oid, obj_size, obj_data,
123 123 obj_href, obj_verify_href)
124 124
125 125 # this verifies also OIDs
126 126 actions, errors = handler.exec_operation(operation)
127 127 if errors:
128 128 log.warning('LFS: got following errors: %s', errors)
129 129 obj_data['errors'] = errors
130 130
131 131 if actions:
132 132 obj_data['actions'] = actions
133 133
134 134 obj_data['size'] = obj_size
135 135 obj_data['authenticated'] = True
136 136 objects.append(obj_data)
137 137
138 138 result = {'objects': objects, 'transfer': 'basic'}
139 139 log.debug('LFS Response %s', safe_result(result))
140 140
141 141 return result
142 142
143 143
144 144 def lfs_objects_oid_upload(request):
145 145 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
146 146 repo = request.matchdict.get('repo')
147 147 oid = request.matchdict.get('oid')
148 148 store = LFSOidStore(
149 149 oid, repo, store_location=request.registry.git_lfs_store_path)
150 150 engine = store.get_engine(mode='wb')
151 151 log.debug('LFS: starting chunked write of LFS oid: %s to storage', oid)
152 152
153 153 body = request.environ['wsgi.input']
154 154
155 155 with engine as f:
156 156 blksize = 64 * 1024 # 64kb
157 157 while True:
158 158 # read in chunks as stream comes in from Gunicorn
159 159 # this is a specific Gunicorn support function.
160 160 # might work differently on waitress
161 161 chunk = body.read(blksize)
162 162 if not chunk:
163 163 break
164 164 f.write(chunk)
165 165
166 166 return {'upload': 'ok'}
167 167
168 168
169 169 def lfs_objects_oid_download(request):
170 170 repo = request.matchdict.get('repo')
171 171 oid = request.matchdict.get('oid')
172 172
173 173 store = LFSOidStore(
174 174 oid, repo, store_location=request.registry.git_lfs_store_path)
175 175 if not store.has_oid():
176 176 log.debug('LFS: oid %s does not exists in store', oid)
177 177 return write_response_error(
178 178 HTTPNotFound, 'requested file with oid `%s` not found in store' % oid)
179 179
180 180 # TODO(marcink): support range header ?
181 181 # Range: bytes=0-, `bytes=(\d+)\-.*`
182 182
183 183 f = open(store.oid_path, 'rb')
184 184 response = Response(
185 185 content_type='application/octet-stream', app_iter=FileIter(f))
186 186 response.headers.add('X-RC-LFS-Response-Oid', str(oid))
187 187 return response
188 188
189 189
190 190 def lfs_objects_verify(request):
191 191 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
192 192 repo = request.matchdict.get('repo')
193 193
194 194 data = request.json
195 195 oid = data.get('oid')
196 196 size = safe_int(data.get('size'))
197 197
198 198 if not (oid and size):
199 199 return write_response_error(
200 200 HTTPBadRequest, 'missing oid and size in request data')
201 201
202 202 store = LFSOidStore(
203 203 oid, repo, store_location=request.registry.git_lfs_store_path)
204 204 if not store.has_oid():
205 205 log.debug('LFS: oid %s does not exists in store', oid)
206 206 return write_response_error(
207 207 HTTPNotFound, 'oid `%s` does not exists in store' % oid)
208 208
209 209 store_size = store.size_oid()
210 210 if store_size != size:
211 211 msg = 'requested file size mismatch store size:%s requested:%s' % (
212 212 store_size, size)
213 213 return write_response_error(
214 214 HTTPUnprocessableEntity, msg)
215 215
216 216 return {'message': {'size': 'ok', 'in_store': 'ok'}}
217 217
218 218
219 219 def lfs_objects_lock(request):
220 220 return write_response_error(
221 221 HTTPNotImplemented, 'GIT LFS locking api not supported')
222 222
223 223
224 224 def not_found(request):
225 225 return write_response_error(
226 226 HTTPNotFound, 'request path not found')
227 227
228 228
229 229 def lfs_disabled(request):
230 230 return write_response_error(
231 231 HTTPNotImplemented, 'GIT LFS disabled for this repo')
232 232
233 233
234 234 def git_lfs_app(config):
235 235
236 236 # v1 API deprecation endpoint
237 237 config.add_route('lfs_objects',
238 238 '/{repo:.*?[^/]}/info/lfs/objects')
239 239 config.add_view(lfs_objects, route_name='lfs_objects',
240 240 request_method='POST', renderer='json')
241 241
242 242 # locking API
243 243 config.add_route('lfs_objects_lock',
244 244 '/{repo:.*?[^/]}/info/lfs/locks')
245 245 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock',
246 246 request_method=('POST', 'GET'), renderer='json')
247 247
248 248 config.add_route('lfs_objects_lock_verify',
249 249 '/{repo:.*?[^/]}/info/lfs/locks/verify')
250 250 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock_verify',
251 251 request_method=('POST', 'GET'), renderer='json')
252 252
253 253 # batch API
254 254 config.add_route('lfs_objects_batch',
255 255 '/{repo:.*?[^/]}/info/lfs/objects/batch')
256 256 config.add_view(lfs_objects_batch, route_name='lfs_objects_batch',
257 257 request_method='POST', renderer='json')
258 258
259 259 # oid upload/download API
260 260 config.add_route('lfs_objects_oid',
261 261 '/{repo:.*?[^/]}/info/lfs/objects/{oid}')
262 262 config.add_view(lfs_objects_oid_upload, route_name='lfs_objects_oid',
263 263 request_method='PUT', renderer='json')
264 264 config.add_view(lfs_objects_oid_download, route_name='lfs_objects_oid',
265 265 request_method='GET', renderer='json')
266 266
267 267 # verification API
268 268 config.add_route('lfs_objects_verify',
269 269 '/{repo:.*?[^/]}/info/lfs/verify')
270 270 config.add_view(lfs_objects_verify, route_name='lfs_objects_verify',
271 271 request_method='POST', renderer='json')
272 272
273 273 # not found handler for API
274 274 config.add_notfound_view(not_found, renderer='json')
275 275
276 276
277 277 def create_app(git_lfs_enabled, git_lfs_store_path):
278 278 config = Configurator()
279 279 if git_lfs_enabled:
280 280 config.include(git_lfs_app)
281 281 config.registry.git_lfs_store_path = git_lfs_store_path
282 282 else:
283 283 # not found handler for API, reporting disabled LFS support
284 284 config.add_notfound_view(lfs_disabled, renderer='json')
285 285
286 286 app = config.make_wsgi_app()
287 287 return app
@@ -1,175 +1,175 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import shutil
20 20 import logging
21 21 from collections import OrderedDict
22 22
23 23 log = logging.getLogger(__name__)
24 24
25 25
26 26 class OidHandler(object):
27 27
28 28 def __init__(self, store, repo_name, auth, oid, obj_size, obj_data, obj_href,
29 29 obj_verify_href=None):
30 30 self.current_store = store
31 31 self.repo_name = repo_name
32 32 self.auth = auth
33 33 self.oid = oid
34 34 self.obj_size = obj_size
35 35 self.obj_data = obj_data
36 36 self.obj_href = obj_href
37 37 self.obj_verify_href = obj_verify_href
38 38
39 39 def get_store(self, mode=None):
40 40 return self.current_store
41 41
42 42 def get_auth(self):
43 43 """returns auth header for re-use in upload/download"""
44 44 return " ".join(self.auth)
45 45
46 46 def download(self):
47 47
48 48 store = self.get_store()
49 49 response = None
50 50 has_errors = None
51 51
52 52 if not store.has_oid():
53 53 # error reply back to client that something is wrong with dl
54 54 err_msg = 'object: {} does not exist in store'.format(store.oid)
55 55 has_errors = OrderedDict(
56 56 error=OrderedDict(
57 57 code=404,
58 58 message=err_msg
59 59 )
60 60 )
61 61
62 62 download_action = OrderedDict(
63 63 href=self.obj_href,
64 64 header=OrderedDict([("Authorization", self.get_auth())])
65 65 )
66 66 if not has_errors:
67 67 response = OrderedDict(download=download_action)
68 68 return response, has_errors
69 69
70 70 def upload(self, skip_existing=True):
71 71 """
72 72 Write upload action for git-lfs server
73 73 """
74 74
75 75 store = self.get_store()
76 76 response = None
77 77 has_errors = None
78 78
79 79 # verify if we have the OID before, if we do, reply with empty
80 80 if store.has_oid():
81 81 log.debug('LFS: store already has oid %s', store.oid)
82 82
83 83 # validate size
84 84 store_size = store.size_oid()
85 85 size_match = store_size == self.obj_size
86 86 if not size_match:
87 87 log.warning(
88 88 'LFS: size mismatch for oid:%s, in store:%s expected: %s',
89 89 self.oid, store_size, self.obj_size)
90 90 elif skip_existing:
91 91 log.debug('LFS: skipping further action as oid is existing')
92 92 return response, has_errors
93 93
94 94 chunked = ("Transfer-Encoding", "chunked")
95 95 upload_action = OrderedDict(
96 96 href=self.obj_href,
97 97 header=OrderedDict([("Authorization", self.get_auth()), chunked])
98 98 )
99 99 if not has_errors:
100 100 response = OrderedDict(upload=upload_action)
101 101 # if specified in handler, return the verification endpoint
102 102 if self.obj_verify_href:
103 103 verify_action = OrderedDict(
104 104 href=self.obj_verify_href,
105 105 header=OrderedDict([("Authorization", self.get_auth())])
106 106 )
107 107 response['verify'] = verify_action
108 108 return response, has_errors
109 109
110 110 def exec_operation(self, operation, *args, **kwargs):
111 111 handler = getattr(self, operation)
112 112 log.debug('LFS: handling request using %s handler', handler)
113 113 return handler(*args, **kwargs)
114 114
115 115
116 116 class LFSOidStore(object):
117 117
118 118 def __init__(self, oid, repo, store_location=None):
119 119 self.oid = oid
120 120 self.repo = repo
121 121 self.store_path = store_location or self.get_default_store()
122 122 self.tmp_oid_path = os.path.join(self.store_path, oid + '.tmp')
123 123 self.oid_path = os.path.join(self.store_path, oid)
124 124 self.fd = None
125 125
126 126 def get_engine(self, mode):
127 127 """
128 128 engine = .get_engine(mode='wb')
129 129 with engine as f:
130 130 f.write('...')
131 131 """
132 132
133 133 class StoreEngine(object):
134 134 def __init__(self, mode, store_path, oid_path, tmp_oid_path):
135 135 self.mode = mode
136 136 self.store_path = store_path
137 137 self.oid_path = oid_path
138 138 self.tmp_oid_path = tmp_oid_path
139 139
140 140 def __enter__(self):
141 141 if not os.path.isdir(self.store_path):
142 142 os.makedirs(self.store_path)
143 143
144 144 # TODO(marcink): maybe write metadata here with size/oid ?
145 145 fd = open(self.tmp_oid_path, self.mode)
146 146 self.fd = fd
147 147 return fd
148 148
149 149 def __exit__(self, exc_type, exc_value, traceback):
150 150 # close tmp file, and rename to final destination
151 151 self.fd.close()
152 152 shutil.move(self.tmp_oid_path, self.oid_path)
153 153
154 154 return StoreEngine(
155 155 mode, self.store_path, self.oid_path, self.tmp_oid_path)
156 156
157 157 def get_default_store(self):
158 158 """
159 159 Default store, consistent with defaults of Mercurial large files store
160 160 which is /home/username/.cache/largefiles
161 161 """
162 162 user_home = os.path.expanduser("~")
163 163 return os.path.join(user_home, '.cache', 'lfs-store')
164 164
165 165 def has_oid(self):
166 166 return os.path.exists(os.path.join(self.store_path, self.oid))
167 167
168 168 def size_oid(self):
169 169 size = -1
170 170
171 171 if self.has_oid():
172 172 oid = os.path.join(self.store_path, self.oid)
173 173 size = os.stat(oid).st_size
174 174
175 175 return size
@@ -1,16 +1,16 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
@@ -1,239 +1,239 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import pytest
20 20 from webtest.app import TestApp as WebObTestApp
21 21 import simplejson as json
22 22
23 23 from vcsserver.git_lfs.app import create_app
24 24
25 25
26 26 @pytest.fixture(scope='function')
27 27 def git_lfs_app(tmpdir):
28 28 custom_app = WebObTestApp(create_app(
29 29 git_lfs_enabled=True, git_lfs_store_path=str(tmpdir)))
30 30 custom_app._store = str(tmpdir)
31 31 return custom_app
32 32
33 33
34 34 @pytest.fixture()
35 35 def http_auth():
36 36 return {'HTTP_AUTHORIZATION': "Basic XXXXX"}
37 37
38 38
39 39 class TestLFSApplication(object):
40 40
41 41 def test_app_wrong_path(self, git_lfs_app):
42 42 git_lfs_app.get('/repo/info/lfs/xxx', status=404)
43 43
44 44 def test_app_deprecated_endpoint(self, git_lfs_app):
45 45 response = git_lfs_app.post('/repo/info/lfs/objects', status=501)
46 46 assert response.status_code == 501
47 47 assert json.loads(response.text) == {u'message': u'LFS: v1 api not supported'}
48 48
49 49 def test_app_lock_verify_api_not_available(self, git_lfs_app):
50 50 response = git_lfs_app.post('/repo/info/lfs/locks/verify', status=501)
51 51 assert response.status_code == 501
52 52 assert json.loads(response.text) == {
53 53 u'message': u'GIT LFS locking api not supported'}
54 54
55 55 def test_app_lock_api_not_available(self, git_lfs_app):
56 56 response = git_lfs_app.post('/repo/info/lfs/locks', status=501)
57 57 assert response.status_code == 501
58 58 assert json.loads(response.text) == {
59 59 u'message': u'GIT LFS locking api not supported'}
60 60
61 61 def test_app_batch_api_missing_auth(self, git_lfs_app,):
62 62 git_lfs_app.post_json(
63 63 '/repo/info/lfs/objects/batch', params={}, status=403)
64 64
65 65 def test_app_batch_api_unsupported_operation(self, git_lfs_app, http_auth):
66 66 response = git_lfs_app.post_json(
67 67 '/repo/info/lfs/objects/batch', params={}, status=400,
68 68 extra_environ=http_auth)
69 69 assert json.loads(response.text) == {
70 70 u'message': u'unsupported operation mode: `None`'}
71 71
72 72 def test_app_batch_api_missing_objects(self, git_lfs_app, http_auth):
73 73 response = git_lfs_app.post_json(
74 74 '/repo/info/lfs/objects/batch', params={'operation': 'download'},
75 75 status=400, extra_environ=http_auth)
76 76 assert json.loads(response.text) == {
77 77 u'message': u'missing objects data'}
78 78
79 79 def test_app_batch_api_unsupported_data_in_objects(
80 80 self, git_lfs_app, http_auth):
81 81 params = {'operation': 'download',
82 82 'objects': [{}]}
83 83 response = git_lfs_app.post_json(
84 84 '/repo/info/lfs/objects/batch', params=params, status=400,
85 85 extra_environ=http_auth)
86 86 assert json.loads(response.text) == {
87 87 u'message': u'unsupported data in objects'}
88 88
89 89 def test_app_batch_api_download_missing_object(
90 90 self, git_lfs_app, http_auth):
91 91 params = {'operation': 'download',
92 92 'objects': [{'oid': '123', 'size': '1024'}]}
93 93 response = git_lfs_app.post_json(
94 94 '/repo/info/lfs/objects/batch', params=params,
95 95 extra_environ=http_auth)
96 96
97 97 expected_objects = [
98 98 {u'authenticated': True,
99 99 u'errors': {u'error': {
100 100 u'code': 404,
101 101 u'message': u'object: 123 does not exist in store'}},
102 102 u'oid': u'123',
103 103 u'size': u'1024'}
104 104 ]
105 105 assert json.loads(response.text) == {
106 106 'objects': expected_objects, 'transfer': 'basic'}
107 107
108 108 def test_app_batch_api_download(self, git_lfs_app, http_auth):
109 109 oid = '456'
110 110 oid_path = os.path.join(git_lfs_app._store, oid)
111 111 if not os.path.isdir(os.path.dirname(oid_path)):
112 112 os.makedirs(os.path.dirname(oid_path))
113 113 with open(oid_path, 'wb') as f:
114 114 f.write('OID_CONTENT')
115 115
116 116 params = {'operation': 'download',
117 117 'objects': [{'oid': oid, 'size': '1024'}]}
118 118 response = git_lfs_app.post_json(
119 119 '/repo/info/lfs/objects/batch', params=params,
120 120 extra_environ=http_auth)
121 121
122 122 expected_objects = [
123 123 {u'authenticated': True,
124 124 u'actions': {
125 125 u'download': {
126 126 u'header': {u'Authorization': u'Basic XXXXX'},
127 127 u'href': u'http://localhost/repo/info/lfs/objects/456'},
128 128 },
129 129 u'oid': u'456',
130 130 u'size': u'1024'}
131 131 ]
132 132 assert json.loads(response.text) == {
133 133 'objects': expected_objects, 'transfer': 'basic'}
134 134
135 135 def test_app_batch_api_upload(self, git_lfs_app, http_auth):
136 136 params = {'operation': 'upload',
137 137 'objects': [{'oid': '123', 'size': '1024'}]}
138 138 response = git_lfs_app.post_json(
139 139 '/repo/info/lfs/objects/batch', params=params,
140 140 extra_environ=http_auth)
141 141 expected_objects = [
142 142 {u'authenticated': True,
143 143 u'actions': {
144 144 u'upload': {
145 145 u'header': {u'Authorization': u'Basic XXXXX',
146 146 u'Transfer-Encoding': u'chunked'},
147 147 u'href': u'http://localhost/repo/info/lfs/objects/123'},
148 148 u'verify': {
149 149 u'header': {u'Authorization': u'Basic XXXXX'},
150 150 u'href': u'http://localhost/repo/info/lfs/verify'}
151 151 },
152 152 u'oid': u'123',
153 153 u'size': u'1024'}
154 154 ]
155 155 assert json.loads(response.text) == {
156 156 'objects': expected_objects, 'transfer': 'basic'}
157 157
158 158 def test_app_verify_api_missing_data(self, git_lfs_app):
159 159 params = {'oid': 'missing',}
160 160 response = git_lfs_app.post_json(
161 161 '/repo/info/lfs/verify', params=params,
162 162 status=400)
163 163
164 164 assert json.loads(response.text) == {
165 165 u'message': u'missing oid and size in request data'}
166 166
167 167 def test_app_verify_api_missing_obj(self, git_lfs_app):
168 168 params = {'oid': 'missing', 'size': '1024'}
169 169 response = git_lfs_app.post_json(
170 170 '/repo/info/lfs/verify', params=params,
171 171 status=404)
172 172
173 173 assert json.loads(response.text) == {
174 174 u'message': u'oid `missing` does not exists in store'}
175 175
176 176 def test_app_verify_api_size_mismatch(self, git_lfs_app):
177 177 oid = 'existing'
178 178 oid_path = os.path.join(git_lfs_app._store, oid)
179 179 if not os.path.isdir(os.path.dirname(oid_path)):
180 180 os.makedirs(os.path.dirname(oid_path))
181 181 with open(oid_path, 'wb') as f:
182 182 f.write('OID_CONTENT')
183 183
184 184 params = {'oid': oid, 'size': '1024'}
185 185 response = git_lfs_app.post_json(
186 186 '/repo/info/lfs/verify', params=params, status=422)
187 187
188 188 assert json.loads(response.text) == {
189 189 u'message': u'requested file size mismatch '
190 190 u'store size:11 requested:1024'}
191 191
192 192 def test_app_verify_api(self, git_lfs_app):
193 193 oid = 'existing'
194 194 oid_path = os.path.join(git_lfs_app._store, oid)
195 195 if not os.path.isdir(os.path.dirname(oid_path)):
196 196 os.makedirs(os.path.dirname(oid_path))
197 197 with open(oid_path, 'wb') as f:
198 198 f.write('OID_CONTENT')
199 199
200 200 params = {'oid': oid, 'size': 11}
201 201 response = git_lfs_app.post_json(
202 202 '/repo/info/lfs/verify', params=params)
203 203
204 204 assert json.loads(response.text) == {
205 205 u'message': {u'size': u'ok', u'in_store': u'ok'}}
206 206
207 207 def test_app_download_api_oid_not_existing(self, git_lfs_app):
208 208 oid = 'missing'
209 209
210 210 response = git_lfs_app.get(
211 211 '/repo/info/lfs/objects/{oid}'.format(oid=oid), status=404)
212 212
213 213 assert json.loads(response.text) == {
214 214 u'message': u'requested file with oid `missing` not found in store'}
215 215
216 216 def test_app_download_api(self, git_lfs_app):
217 217 oid = 'existing'
218 218 oid_path = os.path.join(git_lfs_app._store, oid)
219 219 if not os.path.isdir(os.path.dirname(oid_path)):
220 220 os.makedirs(os.path.dirname(oid_path))
221 221 with open(oid_path, 'wb') as f:
222 222 f.write('OID_CONTENT')
223 223
224 224 response = git_lfs_app.get(
225 225 '/repo/info/lfs/objects/{oid}'.format(oid=oid))
226 226 assert response
227 227
228 228 def test_app_upload(self, git_lfs_app):
229 229 oid = 'uploaded'
230 230
231 231 response = git_lfs_app.put(
232 232 '/repo/info/lfs/objects/{oid}'.format(oid=oid), params='CONTENT')
233 233
234 234 assert json.loads(response.text) == {u'upload': u'ok'}
235 235
236 236 # verify that we actually wrote that OID
237 237 oid_path = os.path.join(git_lfs_app._store, oid)
238 238 assert os.path.isfile(oid_path)
239 239 assert 'CONTENT' == open(oid_path).read()
@@ -1,141 +1,141 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import pytest
20 20 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
21 21
22 22
23 23 @pytest.fixture()
24 24 def lfs_store(tmpdir):
25 25 repo = 'test'
26 26 oid = '123456789'
27 27 store = LFSOidStore(oid=oid, repo=repo, store_location=str(tmpdir))
28 28 return store
29 29
30 30
31 31 @pytest.fixture()
32 32 def oid_handler(lfs_store):
33 33 store = lfs_store
34 34 repo = store.repo
35 35 oid = store.oid
36 36
37 37 oid_handler = OidHandler(
38 38 store=store, repo_name=repo, auth=('basic', 'xxxx'),
39 39 oid=oid,
40 40 obj_size='1024', obj_data={}, obj_href='http://localhost/handle_oid',
41 41 obj_verify_href='http://localhost/verify')
42 42 return oid_handler
43 43
44 44
45 45 class TestOidHandler(object):
46 46
47 47 @pytest.mark.parametrize('exec_action', [
48 48 'download',
49 49 'upload',
50 50 ])
51 51 def test_exec_action(self, exec_action, oid_handler):
52 52 handler = oid_handler.exec_operation(exec_action)
53 53 assert handler
54 54
55 55 def test_exec_action_undefined(self, oid_handler):
56 56 with pytest.raises(AttributeError):
57 57 oid_handler.exec_operation('wrong')
58 58
59 59 def test_download_oid_not_existing(self, oid_handler):
60 60 response, has_errors = oid_handler.exec_operation('download')
61 61
62 62 assert response is None
63 63 assert has_errors['error'] == {
64 64 'code': 404,
65 65 'message': 'object: 123456789 does not exist in store'}
66 66
67 67 def test_download_oid(self, oid_handler):
68 68 store = oid_handler.get_store()
69 69 if not os.path.isdir(os.path.dirname(store.oid_path)):
70 70 os.makedirs(os.path.dirname(store.oid_path))
71 71
72 72 with open(store.oid_path, 'wb') as f:
73 73 f.write('CONTENT')
74 74
75 75 response, has_errors = oid_handler.exec_operation('download')
76 76
77 77 assert has_errors is None
78 78 assert response['download'] == {
79 79 'header': {'Authorization': 'basic xxxx'},
80 80 'href': 'http://localhost/handle_oid'
81 81 }
82 82
83 83 def test_upload_oid_that_exists(self, oid_handler):
84 84 store = oid_handler.get_store()
85 85 if not os.path.isdir(os.path.dirname(store.oid_path)):
86 86 os.makedirs(os.path.dirname(store.oid_path))
87 87
88 88 with open(store.oid_path, 'wb') as f:
89 89 f.write('CONTENT')
90 90 oid_handler.obj_size = 7
91 91 response, has_errors = oid_handler.exec_operation('upload')
92 92 assert has_errors is None
93 93 assert response is None
94 94
95 95 def test_upload_oid_that_exists_but_has_wrong_size(self, oid_handler):
96 96 store = oid_handler.get_store()
97 97 if not os.path.isdir(os.path.dirname(store.oid_path)):
98 98 os.makedirs(os.path.dirname(store.oid_path))
99 99
100 100 with open(store.oid_path, 'wb') as f:
101 101 f.write('CONTENT')
102 102
103 103 oid_handler.obj_size = 10240
104 104 response, has_errors = oid_handler.exec_operation('upload')
105 105 assert has_errors is None
106 106 assert response['upload'] == {
107 107 'header': {'Authorization': 'basic xxxx',
108 108 'Transfer-Encoding': 'chunked'},
109 109 'href': 'http://localhost/handle_oid',
110 110 }
111 111
112 112 def test_upload_oid(self, oid_handler):
113 113 response, has_errors = oid_handler.exec_operation('upload')
114 114 assert has_errors is None
115 115 assert response['upload'] == {
116 116 'header': {'Authorization': 'basic xxxx',
117 117 'Transfer-Encoding': 'chunked'},
118 118 'href': 'http://localhost/handle_oid'
119 119 }
120 120
121 121
122 122 class TestLFSStore(object):
123 123 def test_write_oid(self, lfs_store):
124 124 oid_location = lfs_store.oid_path
125 125
126 126 assert not os.path.isfile(oid_location)
127 127
128 128 engine = lfs_store.get_engine(mode='wb')
129 129 with engine as f:
130 130 f.write('CONTENT')
131 131
132 132 assert os.path.isfile(oid_location)
133 133
134 134 def test_detect_has_oid(self, lfs_store):
135 135
136 136 assert lfs_store.has_oid() is False
137 137 engine = lfs_store.get_engine(mode='wb')
138 138 with engine as f:
139 139 f.write('CONTENT')
140 140
141 141 assert lfs_store.has_oid() is True No newline at end of file
@@ -1,50 +1,50 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17 import copy
18 18 from functools import wraps
19 19
20 20
21 21 def get_cython_compat_decorator(wrapper, func):
22 22 """
23 23 Creates a cython compatible decorator. The previously used
24 24 decorator.decorator() function seems to be incompatible with cython.
25 25
26 26 :param wrapper: __wrapper method of the decorator class
27 27 :param func: decorated function
28 28 """
29 29 @wraps(func)
30 30 def local_wrapper(*args, **kwds):
31 31 return wrapper(func, *args, **kwds)
32 32 local_wrapper.__wrapped__ = func
33 33 return local_wrapper
34 34
35 35
36 36 def safe_result(result):
37 37 """clean result for better representation in logs"""
38 38 clean_copy = copy.deepcopy(result)
39 39
40 40 try:
41 41 if 'objects' in clean_copy:
42 42 for oid_data in clean_copy['objects']:
43 43 if 'actions' in oid_data:
44 44 for action_name, data in oid_data['actions'].items():
45 45 if 'header' in data:
46 46 data['header'] = {'Authorization': '*****'}
47 47 except Exception:
48 48 return result
49 49
50 50 return clean_copy
@@ -1,795 +1,795 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import io
19 19 import logging
20 20 import stat
21 21 import urllib
22 22 import urllib2
23 23
24 24 from hgext import largefiles, rebase
25 25 from hgext.strip import strip as hgext_strip
26 26 from mercurial import commands
27 27 from mercurial import unionrepo
28 28 from mercurial import verify
29 29
30 30 from vcsserver import exceptions
31 31 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
32 32 from vcsserver.hgcompat import (
33 33 archival, bin, clone, config as hgconfig, diffopts, hex,
34 34 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
35 35 makepeer, localrepository, match, memctx, exchange, memfilectx, nullrev,
36 36 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
37 37 RepoLookupError, InterventionRequired, RequirementError)
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 def make_ui_from_config(repo_config):
43 43 baseui = ui.ui()
44 44
45 45 # clean the baseui object
46 46 baseui._ocfg = hgconfig.config()
47 47 baseui._ucfg = hgconfig.config()
48 48 baseui._tcfg = hgconfig.config()
49 49
50 50 for section, option, value in repo_config:
51 51 baseui.setconfig(section, option, value)
52 52
53 53 # make our hgweb quiet so it doesn't print output
54 54 baseui.setconfig('ui', 'quiet', 'true')
55 55
56 56 baseui.setconfig('ui', 'paginate', 'never')
57 57 # force mercurial to only use 1 thread, otherwise it may try to set a
58 58 # signal in a non-main thread, thus generating a ValueError.
59 59 baseui.setconfig('worker', 'numcpus', 1)
60 60
61 61 # If there is no config for the largefiles extension, we explicitly disable
62 62 # it here. This overrides settings from repositories hgrc file. Recent
63 63 # mercurial versions enable largefiles in hgrc on clone from largefile
64 64 # repo.
65 65 if not baseui.hasconfig('extensions', 'largefiles'):
66 66 log.debug('Explicitly disable largefiles extension for repo.')
67 67 baseui.setconfig('extensions', 'largefiles', '!')
68 68
69 69 return baseui
70 70
71 71
72 72 def reraise_safe_exceptions(func):
73 73 """Decorator for converting mercurial exceptions to something neutral."""
74 74 def wrapper(*args, **kwargs):
75 75 try:
76 76 return func(*args, **kwargs)
77 77 except (Abort, InterventionRequired) as e:
78 78 raise_from_original(exceptions.AbortException(e))
79 79 except RepoLookupError as e:
80 80 raise_from_original(exceptions.LookupException(e))
81 81 except RequirementError as e:
82 82 raise_from_original(exceptions.RequirementException(e))
83 83 except RepoError as e:
84 84 raise_from_original(exceptions.VcsException(e))
85 85 except LookupError as e:
86 86 raise_from_original(exceptions.LookupException(e))
87 87 except Exception as e:
88 88 if not hasattr(e, '_vcs_kind'):
89 89 log.exception("Unhandled exception in hg remote call")
90 90 raise_from_original(exceptions.UnhandledException(e))
91 91
92 92 raise
93 93 return wrapper
94 94
95 95
96 96 class MercurialFactory(RepoFactory):
97 97 repo_type = 'hg'
98 98
99 99 def _create_config(self, config, hooks=True):
100 100 if not hooks:
101 101 hooks_to_clean = frozenset((
102 102 'changegroup.repo_size', 'preoutgoing.pre_pull',
103 103 'outgoing.pull_logger', 'prechangegroup.pre_push'))
104 104 new_config = []
105 105 for section, option, value in config:
106 106 if section == 'hooks' and option in hooks_to_clean:
107 107 continue
108 108 new_config.append((section, option, value))
109 109 config = new_config
110 110
111 111 baseui = make_ui_from_config(config)
112 112 return baseui
113 113
114 114 def _create_repo(self, wire, create):
115 115 baseui = self._create_config(wire["config"])
116 116 return localrepository(baseui, wire["path"], create)
117 117
118 118
119 119 class HgRemote(object):
120 120
121 121 def __init__(self, factory):
122 122 self._factory = factory
123 123
124 124 self._bulk_methods = {
125 125 "affected_files": self.ctx_files,
126 126 "author": self.ctx_user,
127 127 "branch": self.ctx_branch,
128 128 "children": self.ctx_children,
129 129 "date": self.ctx_date,
130 130 "message": self.ctx_description,
131 131 "parents": self.ctx_parents,
132 132 "status": self.ctx_status,
133 133 "obsolete": self.ctx_obsolete,
134 134 "phase": self.ctx_phase,
135 135 "hidden": self.ctx_hidden,
136 136 "_file_paths": self.ctx_list,
137 137 }
138 138
139 139 @reraise_safe_exceptions
140 140 def discover_hg_version(self):
141 141 from mercurial import util
142 142 return util.version()
143 143
144 144 @reraise_safe_exceptions
145 145 def archive_repo(self, archive_path, mtime, file_info, kind):
146 146 if kind == "tgz":
147 147 archiver = archival.tarit(archive_path, mtime, "gz")
148 148 elif kind == "tbz2":
149 149 archiver = archival.tarit(archive_path, mtime, "bz2")
150 150 elif kind == 'zip':
151 151 archiver = archival.zipit(archive_path, mtime)
152 152 else:
153 153 raise exceptions.ArchiveException()(
154 154 'Remote does not support: "%s".' % kind)
155 155
156 156 for f_path, f_mode, f_is_link, f_content in file_info:
157 157 archiver.addfile(f_path, f_mode, f_is_link, f_content)
158 158 archiver.done()
159 159
160 160 @reraise_safe_exceptions
161 161 def bookmarks(self, wire):
162 162 repo = self._factory.repo(wire)
163 163 return dict(repo._bookmarks)
164 164
165 165 @reraise_safe_exceptions
166 166 def branches(self, wire, normal, closed):
167 167 repo = self._factory.repo(wire)
168 168 iter_branches = repo.branchmap().iterbranches()
169 169 bt = {}
170 170 for branch_name, _heads, tip, is_closed in iter_branches:
171 171 if normal and not is_closed:
172 172 bt[branch_name] = tip
173 173 if closed and is_closed:
174 174 bt[branch_name] = tip
175 175
176 176 return bt
177 177
178 178 @reraise_safe_exceptions
179 179 def bulk_request(self, wire, rev, pre_load):
180 180 result = {}
181 181 for attr in pre_load:
182 182 try:
183 183 method = self._bulk_methods[attr]
184 184 result[attr] = method(wire, rev)
185 185 except KeyError as e:
186 186 raise exceptions.VcsException(e)(
187 187 'Unknown bulk attribute: "%s"' % attr)
188 188 return result
189 189
190 190 @reraise_safe_exceptions
191 191 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
192 192 baseui = self._factory._create_config(wire["config"], hooks=hooks)
193 193 clone(baseui, source, dest, noupdate=not update_after_clone)
194 194
195 195 @reraise_safe_exceptions
196 196 def commitctx(
197 197 self, wire, message, parents, commit_time, commit_timezone,
198 198 user, files, extra, removed, updated):
199 199
200 200 def _filectxfn(_repo, memctx, path):
201 201 """
202 202 Marks given path as added/changed/removed in a given _repo. This is
203 203 for internal mercurial commit function.
204 204 """
205 205
206 206 # check if this path is removed
207 207 if path in removed:
208 208 # returning None is a way to mark node for removal
209 209 return None
210 210
211 211 # check if this path is added
212 212 for node in updated:
213 213 if node['path'] == path:
214 214 return memfilectx(
215 215 _repo,
216 216 changectx=memctx,
217 217 path=node['path'],
218 218 data=node['content'],
219 219 islink=False,
220 220 isexec=bool(node['mode'] & stat.S_IXUSR),
221 221 copied=False)
222 222
223 223 raise exceptions.AbortException()(
224 224 "Given path haven't been marked as added, "
225 225 "changed or removed (%s)" % path)
226 226
227 227 repo = self._factory.repo(wire)
228 228
229 229 commit_ctx = memctx(
230 230 repo=repo,
231 231 parents=parents,
232 232 text=message,
233 233 files=files,
234 234 filectxfn=_filectxfn,
235 235 user=user,
236 236 date=(commit_time, commit_timezone),
237 237 extra=extra)
238 238
239 239 n = repo.commitctx(commit_ctx)
240 240 new_id = hex(n)
241 241
242 242 return new_id
243 243
244 244 @reraise_safe_exceptions
245 245 def ctx_branch(self, wire, revision):
246 246 repo = self._factory.repo(wire)
247 247 ctx = repo[revision]
248 248 return ctx.branch()
249 249
250 250 @reraise_safe_exceptions
251 251 def ctx_children(self, wire, revision):
252 252 repo = self._factory.repo(wire)
253 253 ctx = repo[revision]
254 254 return [child.rev() for child in ctx.children()]
255 255
256 256 @reraise_safe_exceptions
257 257 def ctx_date(self, wire, revision):
258 258 repo = self._factory.repo(wire)
259 259 ctx = repo[revision]
260 260 return ctx.date()
261 261
262 262 @reraise_safe_exceptions
263 263 def ctx_description(self, wire, revision):
264 264 repo = self._factory.repo(wire)
265 265 ctx = repo[revision]
266 266 return ctx.description()
267 267
268 268 @reraise_safe_exceptions
269 269 def ctx_diff(
270 270 self, wire, revision, git=True, ignore_whitespace=True, context=3):
271 271 repo = self._factory.repo(wire)
272 272 ctx = repo[revision]
273 273 result = ctx.diff(
274 274 git=git, ignore_whitespace=ignore_whitespace, context=context)
275 275 return list(result)
276 276
277 277 @reraise_safe_exceptions
278 278 def ctx_files(self, wire, revision):
279 279 repo = self._factory.repo(wire)
280 280 ctx = repo[revision]
281 281 return ctx.files()
282 282
283 283 @reraise_safe_exceptions
284 284 def ctx_list(self, path, revision):
285 285 repo = self._factory.repo(path)
286 286 ctx = repo[revision]
287 287 return list(ctx)
288 288
289 289 @reraise_safe_exceptions
290 290 def ctx_parents(self, wire, revision):
291 291 repo = self._factory.repo(wire)
292 292 ctx = repo[revision]
293 293 return [parent.rev() for parent in ctx.parents()]
294 294
295 295 @reraise_safe_exceptions
296 296 def ctx_phase(self, wire, revision):
297 297 repo = self._factory.repo(wire)
298 298 ctx = repo[revision]
299 299 # public=0, draft=1, secret=3
300 300 return ctx.phase()
301 301
302 302 @reraise_safe_exceptions
303 303 def ctx_obsolete(self, wire, revision):
304 304 repo = self._factory.repo(wire)
305 305 ctx = repo[revision]
306 306 return ctx.obsolete()
307 307
308 308 @reraise_safe_exceptions
309 309 def ctx_hidden(self, wire, revision):
310 310 repo = self._factory.repo(wire)
311 311 ctx = repo[revision]
312 312 return ctx.hidden()
313 313
314 314 @reraise_safe_exceptions
315 315 def ctx_substate(self, wire, revision):
316 316 repo = self._factory.repo(wire)
317 317 ctx = repo[revision]
318 318 return ctx.substate
319 319
320 320 @reraise_safe_exceptions
321 321 def ctx_status(self, wire, revision):
322 322 repo = self._factory.repo(wire)
323 323 ctx = repo[revision]
324 324 status = repo[ctx.p1().node()].status(other=ctx.node())
325 325 # object of status (odd, custom named tuple in mercurial) is not
326 326 # correctly serializable, we make it a list, as the underling
327 327 # API expects this to be a list
328 328 return list(status)
329 329
330 330 @reraise_safe_exceptions
331 331 def ctx_user(self, wire, revision):
332 332 repo = self._factory.repo(wire)
333 333 ctx = repo[revision]
334 334 return ctx.user()
335 335
336 336 @reraise_safe_exceptions
337 337 def check_url(self, url, config):
338 338 _proto = None
339 339 if '+' in url[:url.find('://')]:
340 340 _proto = url[0:url.find('+')]
341 341 url = url[url.find('+') + 1:]
342 342 handlers = []
343 343 url_obj = url_parser(url)
344 344 test_uri, authinfo = url_obj.authinfo()
345 345 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
346 346 url_obj.query = obfuscate_qs(url_obj.query)
347 347
348 348 cleaned_uri = str(url_obj)
349 349 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
350 350
351 351 if authinfo:
352 352 # create a password manager
353 353 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
354 354 passmgr.add_password(*authinfo)
355 355
356 356 handlers.extend((httpbasicauthhandler(passmgr),
357 357 httpdigestauthhandler(passmgr)))
358 358
359 359 o = urllib2.build_opener(*handlers)
360 360 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
361 361 ('Accept', 'application/mercurial-0.1')]
362 362
363 363 q = {"cmd": 'between'}
364 364 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
365 365 qs = '?%s' % urllib.urlencode(q)
366 366 cu = "%s%s" % (test_uri, qs)
367 367 req = urllib2.Request(cu, None, {})
368 368
369 369 try:
370 370 log.debug("Trying to open URL %s", cleaned_uri)
371 371 resp = o.open(req)
372 372 if resp.code != 200:
373 373 raise exceptions.URLError()('Return Code is not 200')
374 374 except Exception as e:
375 375 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
376 376 # means it cannot be cloned
377 377 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
378 378
379 379 # now check if it's a proper hg repo, but don't do it for svn
380 380 try:
381 381 if _proto == 'svn':
382 382 pass
383 383 else:
384 384 # check for pure hg repos
385 385 log.debug(
386 386 "Verifying if URL is a Mercurial repository: %s",
387 387 cleaned_uri)
388 388 ui = make_ui_from_config(config)
389 389 peer_checker = makepeer(ui, url)
390 390 peer_checker.lookup('tip')
391 391 except Exception as e:
392 392 log.warning("URL is not a valid Mercurial repository: %s",
393 393 cleaned_uri)
394 394 raise exceptions.URLError(e)(
395 395 "url [%s] does not look like an hg repo org_exc: %s"
396 396 % (cleaned_uri, e))
397 397
398 398 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
399 399 return True
400 400
401 401 @reraise_safe_exceptions
402 402 def diff(
403 403 self, wire, rev1, rev2, file_filter, opt_git, opt_ignorews,
404 404 context):
405 405 repo = self._factory.repo(wire)
406 406
407 407 if file_filter:
408 408 match_filter = match(file_filter[0], '', [file_filter[1]])
409 409 else:
410 410 match_filter = file_filter
411 411 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context)
412 412
413 413 try:
414 414 return "".join(patch.diff(
415 415 repo, node1=rev1, node2=rev2, match=match_filter, opts=opts))
416 416 except RepoLookupError as e:
417 417 raise exceptions.LookupException(e)()
418 418
419 419 @reraise_safe_exceptions
420 420 def node_history(self, wire, revision, path, limit):
421 421 repo = self._factory.repo(wire)
422 422
423 423 ctx = repo[revision]
424 424 fctx = ctx.filectx(path)
425 425
426 426 def history_iter():
427 427 limit_rev = fctx.rev()
428 428 for obj in reversed(list(fctx.filelog())):
429 429 obj = fctx.filectx(obj)
430 430 if limit_rev >= obj.rev():
431 431 yield obj
432 432
433 433 history = []
434 434 for cnt, obj in enumerate(history_iter()):
435 435 if limit and cnt >= limit:
436 436 break
437 437 history.append(hex(obj.node()))
438 438
439 439 return [x for x in history]
440 440
441 441 @reraise_safe_exceptions
442 442 def node_history_untill(self, wire, revision, path, limit):
443 443 repo = self._factory.repo(wire)
444 444 ctx = repo[revision]
445 445 fctx = ctx.filectx(path)
446 446
447 447 file_log = list(fctx.filelog())
448 448 if limit:
449 449 # Limit to the last n items
450 450 file_log = file_log[-limit:]
451 451
452 452 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
453 453
454 454 @reraise_safe_exceptions
455 455 def fctx_annotate(self, wire, revision, path):
456 456 repo = self._factory.repo(wire)
457 457 ctx = repo[revision]
458 458 fctx = ctx.filectx(path)
459 459
460 460 result = []
461 461 for i, annotate_obj in enumerate(fctx.annotate(), 1):
462 462 ln_no = i
463 463 sha = hex(annotate_obj.fctx.node())
464 464 content = annotate_obj.text
465 465 result.append((ln_no, sha, content))
466 466 return result
467 467
468 468 @reraise_safe_exceptions
469 469 def fctx_data(self, wire, revision, path):
470 470 repo = self._factory.repo(wire)
471 471 ctx = repo[revision]
472 472 fctx = ctx.filectx(path)
473 473 return fctx.data()
474 474
475 475 @reraise_safe_exceptions
476 476 def fctx_flags(self, wire, revision, path):
477 477 repo = self._factory.repo(wire)
478 478 ctx = repo[revision]
479 479 fctx = ctx.filectx(path)
480 480 return fctx.flags()
481 481
482 482 @reraise_safe_exceptions
483 483 def fctx_size(self, wire, revision, path):
484 484 repo = self._factory.repo(wire)
485 485 ctx = repo[revision]
486 486 fctx = ctx.filectx(path)
487 487 return fctx.size()
488 488
489 489 @reraise_safe_exceptions
490 490 def get_all_commit_ids(self, wire, name):
491 491 repo = self._factory.repo(wire)
492 492 revs = repo.filtered(name).changelog.index
493 493 return map(lambda x: hex(x[7]), revs)[:-1]
494 494
495 495 @reraise_safe_exceptions
496 496 def get_config_value(self, wire, section, name, untrusted=False):
497 497 repo = self._factory.repo(wire)
498 498 return repo.ui.config(section, name, untrusted=untrusted)
499 499
500 500 @reraise_safe_exceptions
501 501 def get_config_bool(self, wire, section, name, untrusted=False):
502 502 repo = self._factory.repo(wire)
503 503 return repo.ui.configbool(section, name, untrusted=untrusted)
504 504
505 505 @reraise_safe_exceptions
506 506 def get_config_list(self, wire, section, name, untrusted=False):
507 507 repo = self._factory.repo(wire)
508 508 return repo.ui.configlist(section, name, untrusted=untrusted)
509 509
510 510 @reraise_safe_exceptions
511 511 def is_large_file(self, wire, path):
512 512 return largefiles.lfutil.isstandin(path)
513 513
514 514 @reraise_safe_exceptions
515 515 def in_largefiles_store(self, wire, sha):
516 516 repo = self._factory.repo(wire)
517 517 return largefiles.lfutil.instore(repo, sha)
518 518
519 519 @reraise_safe_exceptions
520 520 def in_user_cache(self, wire, sha):
521 521 repo = self._factory.repo(wire)
522 522 return largefiles.lfutil.inusercache(repo.ui, sha)
523 523
524 524 @reraise_safe_exceptions
525 525 def store_path(self, wire, sha):
526 526 repo = self._factory.repo(wire)
527 527 return largefiles.lfutil.storepath(repo, sha)
528 528
529 529 @reraise_safe_exceptions
530 530 def link(self, wire, sha, path):
531 531 repo = self._factory.repo(wire)
532 532 largefiles.lfutil.link(
533 533 largefiles.lfutil.usercachepath(repo.ui, sha), path)
534 534
535 535 @reraise_safe_exceptions
536 536 def localrepository(self, wire, create=False):
537 537 self._factory.repo(wire, create=create)
538 538
539 539 @reraise_safe_exceptions
540 540 def lookup(self, wire, revision, both):
541 541
542 542 repo = self._factory.repo(wire)
543 543
544 544 if isinstance(revision, int):
545 545 # NOTE(marcink):
546 546 # since Mercurial doesn't support indexes properly
547 547 # we need to shift accordingly by one to get proper index, e.g
548 548 # repo[-1] => repo[-2]
549 549 # repo[0] => repo[-1]
550 550 # repo[1] => repo[2] we also never call repo[0] because
551 551 # it's actually second commit
552 552 if revision <= 0:
553 553 revision = revision + -1
554 554 else:
555 555 revision = revision + 1
556 556
557 557 try:
558 558 ctx = repo[revision]
559 559 except RepoLookupError as e:
560 560 raise exceptions.LookupException(e)(revision)
561 561 except LookupError as e:
562 562 raise exceptions.LookupException(e)(e.name)
563 563
564 564 if not both:
565 565 return ctx.hex()
566 566
567 567 ctx = repo[ctx.hex()]
568 568 return ctx.hex(), ctx.rev()
569 569
570 570 @reraise_safe_exceptions
571 571 def pull(self, wire, url, commit_ids=None):
572 572 repo = self._factory.repo(wire)
573 573 # Disable any prompts for this repo
574 574 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
575 575
576 576 remote = peer(repo, {}, url)
577 577 # Disable any prompts for this remote
578 578 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
579 579
580 580 if commit_ids:
581 581 commit_ids = [bin(commit_id) for commit_id in commit_ids]
582 582
583 583 return exchange.pull(
584 584 repo, remote, heads=commit_ids, force=None).cgresult
585 585
586 586 @reraise_safe_exceptions
587 587 def sync_push(self, wire, url):
588 588 if not self.check_url(url, wire['config']):
589 589 return
590 590
591 591 repo = self._factory.repo(wire)
592 592
593 593 # Disable any prompts for this repo
594 594 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
595 595
596 596 bookmarks = dict(repo._bookmarks).keys()
597 597 remote = peer(repo, {}, url)
598 598 # Disable any prompts for this remote
599 599 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
600 600
601 601 return exchange.push(
602 602 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
603 603
604 604 @reraise_safe_exceptions
605 605 def revision(self, wire, rev):
606 606 repo = self._factory.repo(wire)
607 607 ctx = repo[rev]
608 608 return ctx.rev()
609 609
610 610 @reraise_safe_exceptions
611 611 def rev_range(self, wire, filter):
612 612 repo = self._factory.repo(wire)
613 613 revisions = [rev for rev in revrange(repo, filter)]
614 614 return revisions
615 615
616 616 @reraise_safe_exceptions
617 617 def rev_range_hash(self, wire, node):
618 618 repo = self._factory.repo(wire)
619 619
620 620 def get_revs(repo, rev_opt):
621 621 if rev_opt:
622 622 revs = revrange(repo, rev_opt)
623 623 if len(revs) == 0:
624 624 return (nullrev, nullrev)
625 625 return max(revs), min(revs)
626 626 else:
627 627 return len(repo) - 1, 0
628 628
629 629 stop, start = get_revs(repo, [node + ':'])
630 630 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
631 631 return revs
632 632
633 633 @reraise_safe_exceptions
634 634 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
635 635 other_path = kwargs.pop('other_path', None)
636 636
637 637 # case when we want to compare two independent repositories
638 638 if other_path and other_path != wire["path"]:
639 639 baseui = self._factory._create_config(wire["config"])
640 640 repo = unionrepo.unionrepository(baseui, other_path, wire["path"])
641 641 else:
642 642 repo = self._factory.repo(wire)
643 643 return list(repo.revs(rev_spec, *args))
644 644
645 645 @reraise_safe_exceptions
646 646 def strip(self, wire, revision, update, backup):
647 647 repo = self._factory.repo(wire)
648 648 ctx = repo[revision]
649 649 hgext_strip(
650 650 repo.baseui, repo, ctx.node(), update=update, backup=backup)
651 651
652 652 @reraise_safe_exceptions
653 653 def verify(self, wire,):
654 654 repo = self._factory.repo(wire)
655 655 baseui = self._factory._create_config(wire['config'])
656 656 baseui.setconfig('ui', 'quiet', 'false')
657 657 output = io.BytesIO()
658 658
659 659 def write(data, **unused_kwargs):
660 660 output.write(data)
661 661 baseui.write = write
662 662
663 663 repo.ui = baseui
664 664 verify.verify(repo)
665 665 return output.getvalue()
666 666
667 667 @reraise_safe_exceptions
668 668 def tag(self, wire, name, revision, message, local, user,
669 669 tag_time, tag_timezone):
670 670 repo = self._factory.repo(wire)
671 671 ctx = repo[revision]
672 672 node = ctx.node()
673 673
674 674 date = (tag_time, tag_timezone)
675 675 try:
676 676 hg_tag.tag(repo, name, node, message, local, user, date)
677 677 except Abort as e:
678 678 log.exception("Tag operation aborted")
679 679 # Exception can contain unicode which we convert
680 680 raise exceptions.AbortException(e)(repr(e))
681 681
682 682 @reraise_safe_exceptions
683 683 def tags(self, wire):
684 684 repo = self._factory.repo(wire)
685 685 return repo.tags()
686 686
687 687 @reraise_safe_exceptions
688 688 def update(self, wire, node=None, clean=False):
689 689 repo = self._factory.repo(wire)
690 690 baseui = self._factory._create_config(wire['config'])
691 691 commands.update(baseui, repo, node=node, clean=clean)
692 692
693 693 @reraise_safe_exceptions
694 694 def identify(self, wire):
695 695 repo = self._factory.repo(wire)
696 696 baseui = self._factory._create_config(wire['config'])
697 697 output = io.BytesIO()
698 698 baseui.write = output.write
699 699 # This is required to get a full node id
700 700 baseui.debugflag = True
701 701 commands.identify(baseui, repo, id=True)
702 702
703 703 return output.getvalue()
704 704
705 705 @reraise_safe_exceptions
706 706 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None,
707 707 hooks=True):
708 708 repo = self._factory.repo(wire)
709 709 baseui = self._factory._create_config(wire['config'], hooks=hooks)
710 710
711 711 # Mercurial internally has a lot of logic that checks ONLY if
712 712 # option is defined, we just pass those if they are defined then
713 713 opts = {}
714 714 if bookmark:
715 715 opts['bookmark'] = bookmark
716 716 if branch:
717 717 opts['branch'] = branch
718 718 if revision:
719 719 opts['rev'] = revision
720 720
721 721 commands.pull(baseui, repo, source, **opts)
722 722
723 723 @reraise_safe_exceptions
724 724 def heads(self, wire, branch=None):
725 725 repo = self._factory.repo(wire)
726 726 baseui = self._factory._create_config(wire['config'])
727 727 output = io.BytesIO()
728 728
729 729 def write(data, **unused_kwargs):
730 730 output.write(data)
731 731
732 732 baseui.write = write
733 733 if branch:
734 734 args = [branch]
735 735 else:
736 736 args = []
737 737 commands.heads(baseui, repo, template='{node} ', *args)
738 738
739 739 return output.getvalue()
740 740
741 741 @reraise_safe_exceptions
742 742 def ancestor(self, wire, revision1, revision2):
743 743 repo = self._factory.repo(wire)
744 744 changelog = repo.changelog
745 745 lookup = repo.lookup
746 746 a = changelog.ancestor(lookup(revision1), lookup(revision2))
747 747 return hex(a)
748 748
749 749 @reraise_safe_exceptions
750 750 def push(self, wire, revisions, dest_path, hooks=True,
751 751 push_branches=False):
752 752 repo = self._factory.repo(wire)
753 753 baseui = self._factory._create_config(wire['config'], hooks=hooks)
754 754 commands.push(baseui, repo, dest=dest_path, rev=revisions,
755 755 new_branch=push_branches)
756 756
757 757 @reraise_safe_exceptions
758 758 def merge(self, wire, revision):
759 759 repo = self._factory.repo(wire)
760 760 baseui = self._factory._create_config(wire['config'])
761 761 repo.ui.setconfig('ui', 'merge', 'internal:dump')
762 762
763 763 # In case of sub repositories are used mercurial prompts the user in
764 764 # case of merge conflicts or different sub repository sources. By
765 765 # setting the interactive flag to `False` mercurial doesn't prompt the
766 766 # used but instead uses a default value.
767 767 repo.ui.setconfig('ui', 'interactive', False)
768 768
769 769 commands.merge(baseui, repo, rev=revision)
770 770
771 771 @reraise_safe_exceptions
772 772 def commit(self, wire, message, username, close_branch=False):
773 773 repo = self._factory.repo(wire)
774 774 baseui = self._factory._create_config(wire['config'])
775 775 repo.ui.setconfig('ui', 'username', username)
776 776 commands.commit(baseui, repo, message=message, close_branch=close_branch)
777 777
778 778 @reraise_safe_exceptions
779 779 def rebase(self, wire, source=None, dest=None, abort=False):
780 780 repo = self._factory.repo(wire)
781 781 baseui = self._factory._create_config(wire['config'])
782 782 repo.ui.setconfig('ui', 'merge', 'internal:dump')
783 783 rebase.rebase(
784 784 baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
785 785
786 786 @reraise_safe_exceptions
787 787 def bookmark(self, wire, bookmark, revision=None):
788 788 repo = self._factory.repo(wire)
789 789 baseui = self._factory._create_config(wire['config'])
790 790 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
791 791
792 792 @reraise_safe_exceptions
793 793 def install_hooks(self, wire, force=False):
794 794 # we don't need any special hooks for Mercurial
795 795 pass
@@ -1,63 +1,63 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 """
19 19 Mercurial libs compatibility
20 20 """
21 21
22 22 import mercurial
23 23 from mercurial import demandimport
24 24 # patch demandimport, due to bug in mercurial when it always triggers
25 25 # demandimport.enable()
26 26 demandimport.enable = lambda *args, **kwargs: 1
27 27
28 28 from mercurial import ui
29 29 from mercurial import patch
30 30 from mercurial import config
31 31 from mercurial import extensions
32 32 from mercurial import scmutil
33 33 from mercurial import archival
34 34 from mercurial import discovery
35 35 from mercurial import unionrepo
36 36 from mercurial import localrepo
37 37 from mercurial import merge as hg_merge
38 38 from mercurial import subrepo
39 39 from mercurial import tags as hg_tag
40 40
41 41 from mercurial.commands import clone, nullid, pull
42 42 from mercurial.context import memctx, memfilectx
43 43 from mercurial.error import (
44 44 LookupError, RepoError, RepoLookupError, Abort, InterventionRequired,
45 45 RequirementError)
46 46 from mercurial.hgweb import hgweb_mod
47 47 from mercurial.localrepo import localrepository
48 48 from mercurial.match import match
49 49 from mercurial.mdiff import diffopts
50 50 from mercurial.node import bin, hex
51 51 from mercurial.encoding import tolocal
52 52 from mercurial.discovery import findcommonoutgoing
53 53 from mercurial.hg import peer
54 54 from mercurial.httppeer import makepeer
55 55 from mercurial.util import url as hg_url
56 56 from mercurial.scmutil import revrange
57 57 from mercurial.node import nullrev
58 58 from mercurial import exchange
59 59 from hgext import largefiles
60 60
61 61 # those authnadlers are patched for python 2.6.5 bug an
62 62 # infinit looping when given invalid resources
63 63 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
@@ -1,134 +1,134 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 """
19 19 Adjustments to Mercurial
20 20
21 21 Intentionally kept separate from `hgcompat` and `hg`, so that these patches can
22 22 be applied without having to import the whole Mercurial machinery.
23 23
24 24 Imports are function local, so that just importing this module does not cause
25 25 side-effects other than these functions being defined.
26 26 """
27 27
28 28 import logging
29 29
30 30
31 31 def patch_largefiles_capabilities():
32 32 """
33 33 Patches the capabilities function in the largefiles extension.
34 34 """
35 35 from vcsserver import hgcompat
36 36 lfproto = hgcompat.largefiles.proto
37 37 wrapper = _dynamic_capabilities_wrapper(
38 38 lfproto, hgcompat.extensions.extensions)
39 39 lfproto._capabilities = wrapper
40 40
41 41
42 42 def _dynamic_capabilities_wrapper(lfproto, extensions):
43 43
44 44 wrapped_capabilities = lfproto._capabilities
45 45 logger = logging.getLogger('vcsserver.hg')
46 46
47 47 def _dynamic_capabilities(orig, repo, proto):
48 48 """
49 49 Adds dynamic behavior, so that the capability is only added if the
50 50 extension is enabled in the current ui object.
51 51 """
52 52 if 'largefiles' in dict(extensions(repo.ui)):
53 53 logger.debug('Extension largefiles enabled')
54 54 calc_capabilities = wrapped_capabilities
55 55 return calc_capabilities(orig, repo, proto)
56 56 else:
57 57 logger.debug('Extension largefiles disabled')
58 58 return orig(repo, proto)
59 59
60 60 return _dynamic_capabilities
61 61
62 62
63 63 def patch_subrepo_type_mapping():
64 64 from collections import defaultdict
65 65 from hgcompat import subrepo
66 66 from exceptions import SubrepoMergeException
67 67
68 68 class NoOpSubrepo(subrepo.abstractsubrepo):
69 69
70 70 def __init__(self, ctx, path, *args, **kwargs):
71 71 """Initialize abstractsubrepo part
72 72
73 73 ``ctx`` is the context referring this subrepository in the
74 74 parent repository.
75 75
76 76 ``path`` is the path to this subrepository as seen from
77 77 innermost repository.
78 78 """
79 79 self.ui = ctx.repo().ui
80 80 self._ctx = ctx
81 81 self._path = path
82 82
83 83 def storeclean(self, path):
84 84 """
85 85 returns true if the repository has not changed since it was last
86 86 cloned from or pushed to a given repository.
87 87 """
88 88 return True
89 89
90 90 def dirty(self, ignoreupdate=False, missing=False):
91 91 """returns true if the dirstate of the subrepo is dirty or does not
92 92 match current stored state. If ignoreupdate is true, only check
93 93 whether the subrepo has uncommitted changes in its dirstate.
94 94 """
95 95 return False
96 96
97 97 def basestate(self):
98 98 """current working directory base state, disregarding .hgsubstate
99 99 state and working directory modifications"""
100 100 substate = subrepo.state(self._ctx, self.ui)
101 101 file_system_path, rev, repotype = substate.get(self._path)
102 102 return rev
103 103
104 104 def remove(self):
105 105 """remove the subrepo
106 106
107 107 (should verify the dirstate is not dirty first)
108 108 """
109 109 pass
110 110
111 111 def get(self, state, overwrite=False):
112 112 """run whatever commands are needed to put the subrepo into
113 113 this state
114 114 """
115 115 pass
116 116
117 117 def merge(self, state):
118 118 """merge currently-saved state with the new state."""
119 119 raise SubrepoMergeException()()
120 120
121 121 def push(self, opts):
122 122 """perform whatever action is analogous to 'hg push'
123 123
124 124 This may be a no-op on some systems.
125 125 """
126 126 pass
127 127
128 128 # Patch subrepo type mapping to always return our NoOpSubrepo class
129 129 # whenever a subrepo class is looked up.
130 130 subrepo.types = {
131 131 'hg': NoOpSubrepo,
132 132 'git': NoOpSubrepo,
133 133 'svn': NoOpSubrepo
134 134 }
@@ -1,154 +1,154 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2018 RhodeCode GmbH
4 # Copyright (C) 2014-2019 RhodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 20 import re
21 21 import os
22 22 import sys
23 23 import datetime
24 24 import logging
25 25 import pkg_resources
26 26
27 27 import vcsserver
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
33 33 """
34 34 Creates a RhodeCode hook inside a git repository
35 35
36 36 :param repo_path: path to repository
37 37 :param executable: binary executable to put in the hooks
38 38 :param force_create: Create even if same name hook exists
39 39 """
40 40 executable = executable or sys.executable
41 41 hooks_path = os.path.join(repo_path, 'hooks')
42 42 if not bare:
43 43 hooks_path = os.path.join(repo_path, '.git', 'hooks')
44 44 if not os.path.isdir(hooks_path):
45 45 os.makedirs(hooks_path, mode=0o777)
46 46
47 47 tmpl_post = pkg_resources.resource_string(
48 48 'vcsserver', '/'.join(
49 49 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
50 50 tmpl_pre = pkg_resources.resource_string(
51 51 'vcsserver', '/'.join(
52 52 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
53 53
54 54 path = '' # not used for now
55 55 timestamp = datetime.datetime.utcnow().isoformat()
56 56
57 57 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
58 58 log.debug('Installing git hook in repo %s', repo_path)
59 59 _hook_file = os.path.join(hooks_path, '%s-receive' % h_type)
60 60 _rhodecode_hook = check_rhodecode_hook(_hook_file)
61 61
62 62 if _rhodecode_hook or force_create:
63 63 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
64 64 try:
65 65 with open(_hook_file, 'wb') as f:
66 66 template = template.replace(
67 67 '_TMPL_', vcsserver.__version__)
68 68 template = template.replace('_DATE_', timestamp)
69 69 template = template.replace('_ENV_', executable)
70 70 template = template.replace('_PATH_', path)
71 71 f.write(template)
72 72 os.chmod(_hook_file, 0o755)
73 73 except IOError:
74 74 log.exception('error writing hook file %s', _hook_file)
75 75 else:
76 76 log.debug('skipping writing hook file')
77 77
78 78 return True
79 79
80 80
81 81 def install_svn_hooks(repo_path, executable=None, force_create=False):
82 82 """
83 83 Creates RhodeCode hooks inside a svn repository
84 84
85 85 :param repo_path: path to repository
86 86 :param executable: binary executable to put in the hooks
87 87 :param force_create: Create even if same name hook exists
88 88 """
89 89 executable = executable or sys.executable
90 90 hooks_path = os.path.join(repo_path, 'hooks')
91 91 if not os.path.isdir(hooks_path):
92 92 os.makedirs(hooks_path, mode=0o777)
93 93
94 94 tmpl_post = pkg_resources.resource_string(
95 95 'vcsserver', '/'.join(
96 96 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
97 97 tmpl_pre = pkg_resources.resource_string(
98 98 'vcsserver', '/'.join(
99 99 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
100 100
101 101 path = '' # not used for now
102 102 timestamp = datetime.datetime.utcnow().isoformat()
103 103
104 104 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
105 105 log.debug('Installing svn hook in repo %s', repo_path)
106 106 _hook_file = os.path.join(hooks_path, '%s-commit' % h_type)
107 107 _rhodecode_hook = check_rhodecode_hook(_hook_file)
108 108
109 109 if _rhodecode_hook or force_create:
110 110 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
111 111
112 112 try:
113 113 with open(_hook_file, 'wb') as f:
114 114 template = template.replace(
115 115 '_TMPL_', vcsserver.__version__)
116 116 template = template.replace('_DATE_', timestamp)
117 117 template = template.replace('_ENV_', executable)
118 118 template = template.replace('_PATH_', path)
119 119
120 120 f.write(template)
121 121 os.chmod(_hook_file, 0o755)
122 122 except IOError:
123 123 log.exception('error writing hook file %s', _hook_file)
124 124 else:
125 125 log.debug('skipping writing hook file')
126 126
127 127 return True
128 128
129 129
130 130 def check_rhodecode_hook(hook_path):
131 131 """
132 132 Check if the hook was created by RhodeCode
133 133 """
134 134 if not os.path.exists(hook_path):
135 135 return True
136 136
137 137 log.debug('hook exists, checking if it is from rhodecode')
138 138 hook_content = read_hook_content(hook_path)
139 139 matches = re.search(r'(?:RC_HOOK_VER)\s*=\s*(.*)', hook_content)
140 140 if matches:
141 141 try:
142 142 version = matches.groups()[0]
143 143 log.debug('got version %s from hooks.', version)
144 144 return True
145 145 except Exception:
146 146 log.exception("Exception while reading the hook version.")
147 147
148 148 return False
149 149
150 150
151 151 def read_hook_content(hook_path):
152 152 with open(hook_path, 'rb') as f:
153 153 content = f.read()
154 154 return content
@@ -1,711 +1,711 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2018 RhodeCode GmbH
4 # Copyright (C) 2014-2019 RhodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 20 import io
21 21 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import importlib
26 26 import base64
27 27
28 28 from httplib import HTTPConnection
29 29
30 30
31 31 import mercurial.scmutil
32 32 import mercurial.node
33 33 import simplejson as json
34 34
35 35 from vcsserver import exceptions, subprocessio, settings
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class HooksHttpClient(object):
41 41 connection = None
42 42
43 43 def __init__(self, hooks_uri):
44 44 self.hooks_uri = hooks_uri
45 45
46 46 def __call__(self, method, extras):
47 47 connection = HTTPConnection(self.hooks_uri)
48 48 body = self._serialize(method, extras)
49 49 try:
50 50 connection.request('POST', '/', body)
51 51 except Exception:
52 52 log.error('Connection failed on %s', connection)
53 53 raise
54 54 response = connection.getresponse()
55 55
56 56 response_data = response.read()
57 57
58 58 try:
59 59 return json.loads(response_data)
60 60 except Exception:
61 61 log.exception('Failed to decode hook response json data. '
62 62 'response_code:%s, raw_data:%s',
63 63 response.status, response_data)
64 64 raise
65 65
66 66 def _serialize(self, hook_name, extras):
67 67 data = {
68 68 'method': hook_name,
69 69 'extras': extras
70 70 }
71 71 return json.dumps(data)
72 72
73 73
74 74 class HooksDummyClient(object):
75 75 def __init__(self, hooks_module):
76 76 self._hooks_module = importlib.import_module(hooks_module)
77 77
78 78 def __call__(self, hook_name, extras):
79 79 with self._hooks_module.Hooks() as hooks:
80 80 return getattr(hooks, hook_name)(extras)
81 81
82 82
83 83 class RemoteMessageWriter(object):
84 84 """Writer base class."""
85 85 def write(self, message):
86 86 raise NotImplementedError()
87 87
88 88
89 89 class HgMessageWriter(RemoteMessageWriter):
90 90 """Writer that knows how to send messages to mercurial clients."""
91 91
92 92 def __init__(self, ui):
93 93 self.ui = ui
94 94
95 95 def write(self, message):
96 96 # TODO: Check why the quiet flag is set by default.
97 97 old = self.ui.quiet
98 98 self.ui.quiet = False
99 99 self.ui.status(message.encode('utf-8'))
100 100 self.ui.quiet = old
101 101
102 102
103 103 class GitMessageWriter(RemoteMessageWriter):
104 104 """Writer that knows how to send messages to git clients."""
105 105
106 106 def __init__(self, stdout=None):
107 107 self.stdout = stdout or sys.stdout
108 108
109 109 def write(self, message):
110 110 self.stdout.write(message.encode('utf-8'))
111 111
112 112
113 113 class SvnMessageWriter(RemoteMessageWriter):
114 114 """Writer that knows how to send messages to svn clients."""
115 115
116 116 def __init__(self, stderr=None):
117 117 # SVN needs data sent to stderr for back-to-client messaging
118 118 self.stderr = stderr or sys.stderr
119 119
120 120 def write(self, message):
121 121 self.stderr.write(message.encode('utf-8'))
122 122
123 123
124 124 def _handle_exception(result):
125 125 exception_class = result.get('exception')
126 126 exception_traceback = result.get('exception_traceback')
127 127
128 128 if exception_traceback:
129 129 log.error('Got traceback from remote call:%s', exception_traceback)
130 130
131 131 if exception_class == 'HTTPLockedRC':
132 132 raise exceptions.RepositoryLockedException()(*result['exception_args'])
133 133 elif exception_class == 'HTTPBranchProtected':
134 134 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
135 135 elif exception_class == 'RepositoryError':
136 136 raise exceptions.VcsException()(*result['exception_args'])
137 137 elif exception_class:
138 138 raise Exception('Got remote exception "%s" with args "%s"' %
139 139 (exception_class, result['exception_args']))
140 140
141 141
142 142 def _get_hooks_client(extras):
143 143 if 'hooks_uri' in extras:
144 144 protocol = extras.get('hooks_protocol')
145 145 return HooksHttpClient(extras['hooks_uri'])
146 146 else:
147 147 return HooksDummyClient(extras['hooks_module'])
148 148
149 149
150 150 def _call_hook(hook_name, extras, writer):
151 151 hooks_client = _get_hooks_client(extras)
152 152 log.debug('Hooks, using client:%s', hooks_client)
153 153 result = hooks_client(hook_name, extras)
154 154 log.debug('Hooks got result: %s', result)
155 155
156 156 _handle_exception(result)
157 157 writer.write(result['output'])
158 158
159 159 return result['status']
160 160
161 161
162 162 def _extras_from_ui(ui):
163 163 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
164 164 if not hook_data:
165 165 # maybe it's inside environ ?
166 166 env_hook_data = os.environ.get('RC_SCM_DATA')
167 167 if env_hook_data:
168 168 hook_data = env_hook_data
169 169
170 170 extras = {}
171 171 if hook_data:
172 172 extras = json.loads(hook_data)
173 173 return extras
174 174
175 175
176 176 def _rev_range_hash(repo, node, check_heads=False):
177 177
178 178 commits = []
179 179 revs = []
180 180 start = repo[node].rev()
181 181 end = len(repo)
182 182 for rev in range(start, end):
183 183 revs.append(rev)
184 184 ctx = repo[rev]
185 185 commit_id = mercurial.node.hex(ctx.node())
186 186 branch = ctx.branch()
187 187 commits.append((commit_id, branch))
188 188
189 189 parent_heads = []
190 190 if check_heads:
191 191 parent_heads = _check_heads(repo, start, end, revs)
192 192 return commits, parent_heads
193 193
194 194
195 195 def _check_heads(repo, start, end, commits):
196 196 changelog = repo.changelog
197 197 parents = set()
198 198
199 199 for new_rev in commits:
200 200 for p in changelog.parentrevs(new_rev):
201 201 if p == mercurial.node.nullrev:
202 202 continue
203 203 if p < start:
204 204 parents.add(p)
205 205
206 206 for p in parents:
207 207 branch = repo[p].branch()
208 208 # The heads descending from that parent, on the same branch
209 209 parent_heads = set([p])
210 210 reachable = set([p])
211 211 for x in xrange(p + 1, end):
212 212 if repo[x].branch() != branch:
213 213 continue
214 214 for pp in changelog.parentrevs(x):
215 215 if pp in reachable:
216 216 reachable.add(x)
217 217 parent_heads.discard(pp)
218 218 parent_heads.add(x)
219 219 # More than one head? Suggest merging
220 220 if len(parent_heads) > 1:
221 221 return list(parent_heads)
222 222
223 223 return []
224 224
225 225
226 226 def _get_git_env():
227 227 env = {}
228 228 for k, v in os.environ.items():
229 229 if k.startswith('GIT'):
230 230 env[k] = v
231 231
232 232 # serialized version
233 233 return [(k, v) for k, v in env.items()]
234 234
235 235
236 236 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
237 237 env = {}
238 238 for k, v in os.environ.items():
239 239 if k.startswith('HG'):
240 240 env[k] = v
241 241
242 242 env['HG_NODE'] = old_rev
243 243 env['HG_NODE_LAST'] = new_rev
244 244 env['HG_TXNID'] = txnid
245 245 env['HG_PENDING'] = repo_path
246 246
247 247 return [(k, v) for k, v in env.items()]
248 248
249 249
250 250 def repo_size(ui, repo, **kwargs):
251 251 extras = _extras_from_ui(ui)
252 252 return _call_hook('repo_size', extras, HgMessageWriter(ui))
253 253
254 254
255 255 def pre_pull(ui, repo, **kwargs):
256 256 extras = _extras_from_ui(ui)
257 257 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
258 258
259 259
260 260 def pre_pull_ssh(ui, repo, **kwargs):
261 261 extras = _extras_from_ui(ui)
262 262 if extras and extras.get('SSH'):
263 263 return pre_pull(ui, repo, **kwargs)
264 264 return 0
265 265
266 266
267 267 def post_pull(ui, repo, **kwargs):
268 268 extras = _extras_from_ui(ui)
269 269 return _call_hook('post_pull', extras, HgMessageWriter(ui))
270 270
271 271
272 272 def post_pull_ssh(ui, repo, **kwargs):
273 273 extras = _extras_from_ui(ui)
274 274 if extras and extras.get('SSH'):
275 275 return post_pull(ui, repo, **kwargs)
276 276 return 0
277 277
278 278
279 279 def pre_push(ui, repo, node=None, **kwargs):
280 280 """
281 281 Mercurial pre_push hook
282 282 """
283 283 extras = _extras_from_ui(ui)
284 284 detect_force_push = extras.get('detect_force_push')
285 285
286 286 rev_data = []
287 287 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
288 288 branches = collections.defaultdict(list)
289 289 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
290 290 for commit_id, branch in commits:
291 291 branches[branch].append(commit_id)
292 292
293 293 for branch, commits in branches.items():
294 294 old_rev = kwargs.get('node_last') or commits[0]
295 295 rev_data.append({
296 296 'total_commits': len(commits),
297 297 'old_rev': old_rev,
298 298 'new_rev': commits[-1],
299 299 'ref': '',
300 300 'type': 'branch',
301 301 'name': branch,
302 302 })
303 303
304 304 for push_ref in rev_data:
305 305 push_ref['multiple_heads'] = _heads
306 306
307 307 repo_path = os.path.join(
308 308 extras.get('repo_store', ''), extras.get('repository', ''))
309 309 push_ref['hg_env'] = _get_hg_env(
310 310 old_rev=push_ref['old_rev'],
311 311 new_rev=push_ref['new_rev'], txnid=kwargs.get('txnid'),
312 312 repo_path=repo_path)
313 313
314 314 extras['hook_type'] = kwargs.get('hooktype', 'pre_push')
315 315 extras['commit_ids'] = rev_data
316 316
317 317 return _call_hook('pre_push', extras, HgMessageWriter(ui))
318 318
319 319
320 320 def pre_push_ssh(ui, repo, node=None, **kwargs):
321 321 extras = _extras_from_ui(ui)
322 322 if extras.get('SSH'):
323 323 return pre_push(ui, repo, node, **kwargs)
324 324
325 325 return 0
326 326
327 327
328 328 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
329 329 """
330 330 Mercurial pre_push hook for SSH
331 331 """
332 332 extras = _extras_from_ui(ui)
333 333 if extras.get('SSH'):
334 334 permission = extras['SSH_PERMISSIONS']
335 335
336 336 if 'repository.write' == permission or 'repository.admin' == permission:
337 337 return 0
338 338
339 339 # non-zero ret code
340 340 return 1
341 341
342 342 return 0
343 343
344 344
345 345 def post_push(ui, repo, node, **kwargs):
346 346 """
347 347 Mercurial post_push hook
348 348 """
349 349 extras = _extras_from_ui(ui)
350 350
351 351 commit_ids = []
352 352 branches = []
353 353 bookmarks = []
354 354 tags = []
355 355
356 356 commits, _heads = _rev_range_hash(repo, node)
357 357 for commit_id, branch in commits:
358 358 commit_ids.append(commit_id)
359 359 if branch not in branches:
360 360 branches.append(branch)
361 361
362 362 if hasattr(ui, '_rc_pushkey_branches'):
363 363 bookmarks = ui._rc_pushkey_branches
364 364
365 365 extras['hook_type'] = kwargs.get('hooktype', 'post_push')
366 366 extras['commit_ids'] = commit_ids
367 367 extras['new_refs'] = {
368 368 'branches': branches,
369 369 'bookmarks': bookmarks,
370 370 'tags': tags
371 371 }
372 372
373 373 return _call_hook('post_push', extras, HgMessageWriter(ui))
374 374
375 375
376 376 def post_push_ssh(ui, repo, node, **kwargs):
377 377 """
378 378 Mercurial post_push hook for SSH
379 379 """
380 380 if _extras_from_ui(ui).get('SSH'):
381 381 return post_push(ui, repo, node, **kwargs)
382 382 return 0
383 383
384 384
385 385 def key_push(ui, repo, **kwargs):
386 386 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
387 387 # store new bookmarks in our UI object propagated later to post_push
388 388 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
389 389 return
390 390
391 391
392 392 # backward compat
393 393 log_pull_action = post_pull
394 394
395 395 # backward compat
396 396 log_push_action = post_push
397 397
398 398
399 399 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
400 400 """
401 401 Old hook name: keep here for backward compatibility.
402 402
403 403 This is only required when the installed git hooks are not upgraded.
404 404 """
405 405 pass
406 406
407 407
408 408 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
409 409 """
410 410 Old hook name: keep here for backward compatibility.
411 411
412 412 This is only required when the installed git hooks are not upgraded.
413 413 """
414 414 pass
415 415
416 416
417 417 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
418 418
419 419
420 420 def git_pre_pull(extras):
421 421 """
422 422 Pre pull hook.
423 423
424 424 :param extras: dictionary containing the keys defined in simplevcs
425 425 :type extras: dict
426 426
427 427 :return: status code of the hook. 0 for success.
428 428 :rtype: int
429 429 """
430 430 if 'pull' not in extras['hooks']:
431 431 return HookResponse(0, '')
432 432
433 433 stdout = io.BytesIO()
434 434 try:
435 435 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
436 436 except Exception as error:
437 437 status = 128
438 438 stdout.write('ERROR: %s\n' % str(error))
439 439
440 440 return HookResponse(status, stdout.getvalue())
441 441
442 442
443 443 def git_post_pull(extras):
444 444 """
445 445 Post pull hook.
446 446
447 447 :param extras: dictionary containing the keys defined in simplevcs
448 448 :type extras: dict
449 449
450 450 :return: status code of the hook. 0 for success.
451 451 :rtype: int
452 452 """
453 453 if 'pull' not in extras['hooks']:
454 454 return HookResponse(0, '')
455 455
456 456 stdout = io.BytesIO()
457 457 try:
458 458 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
459 459 except Exception as error:
460 460 status = 128
461 461 stdout.write('ERROR: %s\n' % error)
462 462
463 463 return HookResponse(status, stdout.getvalue())
464 464
465 465
466 466 def _parse_git_ref_lines(revision_lines):
467 467 rev_data = []
468 468 for revision_line in revision_lines or []:
469 469 old_rev, new_rev, ref = revision_line.strip().split(' ')
470 470 ref_data = ref.split('/', 2)
471 471 if ref_data[1] in ('tags', 'heads'):
472 472 rev_data.append({
473 473 # NOTE(marcink):
474 474 # we're unable to tell total_commits for git at this point
475 475 # but we set the variable for consistency with GIT
476 476 'total_commits': -1,
477 477 'old_rev': old_rev,
478 478 'new_rev': new_rev,
479 479 'ref': ref,
480 480 'type': ref_data[1],
481 481 'name': ref_data[2],
482 482 })
483 483 return rev_data
484 484
485 485
486 486 def git_pre_receive(unused_repo_path, revision_lines, env):
487 487 """
488 488 Pre push hook.
489 489
490 490 :param extras: dictionary containing the keys defined in simplevcs
491 491 :type extras: dict
492 492
493 493 :return: status code of the hook. 0 for success.
494 494 :rtype: int
495 495 """
496 496 extras = json.loads(env['RC_SCM_DATA'])
497 497 rev_data = _parse_git_ref_lines(revision_lines)
498 498 if 'push' not in extras['hooks']:
499 499 return 0
500 500 empty_commit_id = '0' * 40
501 501
502 502 detect_force_push = extras.get('detect_force_push')
503 503
504 504 for push_ref in rev_data:
505 505 # store our git-env which holds the temp store
506 506 push_ref['git_env'] = _get_git_env()
507 507 push_ref['pruned_sha'] = ''
508 508 if not detect_force_push:
509 509 # don't check for forced-push when we don't need to
510 510 continue
511 511
512 512 type_ = push_ref['type']
513 513 new_branch = push_ref['old_rev'] == empty_commit_id
514 514 if type_ == 'heads' and not new_branch:
515 515 old_rev = push_ref['old_rev']
516 516 new_rev = push_ref['new_rev']
517 517 cmd = [settings.GIT_EXECUTABLE, 'rev-list',
518 518 old_rev, '^{}'.format(new_rev)]
519 519 stdout, stderr = subprocessio.run_command(
520 520 cmd, env=os.environ.copy())
521 521 # means we're having some non-reachable objects, this forced push
522 522 # was used
523 523 if stdout:
524 524 push_ref['pruned_sha'] = stdout.splitlines()
525 525
526 526 extras['hook_type'] = 'pre_receive'
527 527 extras['commit_ids'] = rev_data
528 528 return _call_hook('pre_push', extras, GitMessageWriter())
529 529
530 530
531 531 def git_post_receive(unused_repo_path, revision_lines, env):
532 532 """
533 533 Post push hook.
534 534
535 535 :param extras: dictionary containing the keys defined in simplevcs
536 536 :type extras: dict
537 537
538 538 :return: status code of the hook. 0 for success.
539 539 :rtype: int
540 540 """
541 541 extras = json.loads(env['RC_SCM_DATA'])
542 542 if 'push' not in extras['hooks']:
543 543 return 0
544 544
545 545 rev_data = _parse_git_ref_lines(revision_lines)
546 546
547 547 git_revs = []
548 548
549 549 # N.B.(skreft): it is ok to just call git, as git before calling a
550 550 # subcommand sets the PATH environment variable so that it point to the
551 551 # correct version of the git executable.
552 552 empty_commit_id = '0' * 40
553 553 branches = []
554 554 tags = []
555 555 for push_ref in rev_data:
556 556 type_ = push_ref['type']
557 557
558 558 if type_ == 'heads':
559 559 if push_ref['old_rev'] == empty_commit_id:
560 560 # starting new branch case
561 561 if push_ref['name'] not in branches:
562 562 branches.append(push_ref['name'])
563 563
564 564 # Fix up head revision if needed
565 565 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
566 566 try:
567 567 subprocessio.run_command(cmd, env=os.environ.copy())
568 568 except Exception:
569 569 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
570 570 'refs/heads/%s' % push_ref['name']]
571 571 print("Setting default branch to %s" % push_ref['name'])
572 572 subprocessio.run_command(cmd, env=os.environ.copy())
573 573
574 574 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
575 575 '--format=%(refname)', 'refs/heads/*']
576 576 stdout, stderr = subprocessio.run_command(
577 577 cmd, env=os.environ.copy())
578 578 heads = stdout
579 579 heads = heads.replace(push_ref['ref'], '')
580 580 heads = ' '.join(head for head
581 581 in heads.splitlines() if head) or '.'
582 582 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
583 583 '--pretty=format:%H', '--', push_ref['new_rev'],
584 584 '--not', heads]
585 585 stdout, stderr = subprocessio.run_command(
586 586 cmd, env=os.environ.copy())
587 587 git_revs.extend(stdout.splitlines())
588 588 elif push_ref['new_rev'] == empty_commit_id:
589 589 # delete branch case
590 590 git_revs.append('delete_branch=>%s' % push_ref['name'])
591 591 else:
592 592 if push_ref['name'] not in branches:
593 593 branches.append(push_ref['name'])
594 594
595 595 cmd = [settings.GIT_EXECUTABLE, 'log',
596 596 '{old_rev}..{new_rev}'.format(**push_ref),
597 597 '--reverse', '--pretty=format:%H']
598 598 stdout, stderr = subprocessio.run_command(
599 599 cmd, env=os.environ.copy())
600 600 git_revs.extend(stdout.splitlines())
601 601 elif type_ == 'tags':
602 602 if push_ref['name'] not in tags:
603 603 tags.append(push_ref['name'])
604 604 git_revs.append('tag=>%s' % push_ref['name'])
605 605
606 606 extras['hook_type'] = 'post_receive'
607 607 extras['commit_ids'] = git_revs
608 608 extras['new_refs'] = {
609 609 'branches': branches,
610 610 'bookmarks': [],
611 611 'tags': tags,
612 612 }
613 613
614 614 if 'repo_size' in extras['hooks']:
615 615 try:
616 616 _call_hook('repo_size', extras, GitMessageWriter())
617 617 except:
618 618 pass
619 619
620 620 return _call_hook('post_push', extras, GitMessageWriter())
621 621
622 622
623 623 def _get_extras_from_txn_id(path, txn_id):
624 624 extras = {}
625 625 try:
626 626 cmd = ['svnlook', 'pget',
627 627 '-t', txn_id,
628 628 '--revprop', path, 'rc-scm-extras']
629 629 stdout, stderr = subprocessio.run_command(
630 630 cmd, env=os.environ.copy())
631 631 extras = json.loads(base64.urlsafe_b64decode(stdout))
632 632 except Exception:
633 633 log.exception('Failed to extract extras info from txn_id')
634 634
635 635 return extras
636 636
637 637
638 638 def _get_extras_from_commit_id(commit_id, path):
639 639 extras = {}
640 640 try:
641 641 cmd = ['svnlook', 'pget',
642 642 '-r', commit_id,
643 643 '--revprop', path, 'rc-scm-extras']
644 644 stdout, stderr = subprocessio.run_command(
645 645 cmd, env=os.environ.copy())
646 646 extras = json.loads(base64.urlsafe_b64decode(stdout))
647 647 except Exception:
648 648 log.exception('Failed to extract extras info from commit_id')
649 649
650 650 return extras
651 651
652 652
653 653 def svn_pre_commit(repo_path, commit_data, env):
654 654 path, txn_id = commit_data
655 655 branches = []
656 656 tags = []
657 657
658 658 if env.get('RC_SCM_DATA'):
659 659 extras = json.loads(env['RC_SCM_DATA'])
660 660 else:
661 661 # fallback method to read from TXN-ID stored data
662 662 extras = _get_extras_from_txn_id(path, txn_id)
663 663 if not extras:
664 664 return 0
665 665
666 666 extras['hook_type'] = 'pre_commit'
667 667 extras['commit_ids'] = []
668 668 extras['txn_id'] = txn_id
669 669 extras['new_refs'] = {
670 670 'total_commits': 1,
671 671 'branches': branches,
672 672 'bookmarks': [],
673 673 'tags': tags,
674 674 }
675 675
676 676 return _call_hook('pre_push', extras, SvnMessageWriter())
677 677
678 678
679 679 def svn_post_commit(repo_path, commit_data, env):
680 680 """
681 681 commit_data is path, rev, txn_id
682 682 """
683 683 path, commit_id, txn_id = commit_data
684 684 branches = []
685 685 tags = []
686 686
687 687 if env.get('RC_SCM_DATA'):
688 688 extras = json.loads(env['RC_SCM_DATA'])
689 689 else:
690 690 # fallback method to read from TXN-ID stored data
691 691 extras = _get_extras_from_commit_id(commit_id, path)
692 692 if not extras:
693 693 return 0
694 694
695 695 extras['hook_type'] = 'post_commit'
696 696 extras['commit_ids'] = [commit_id]
697 697 extras['txn_id'] = txn_id
698 698 extras['new_refs'] = {
699 699 'branches': branches,
700 700 'bookmarks': [],
701 701 'tags': tags,
702 702 'total_commits': 1,
703 703 }
704 704
705 705 if 'repo_size' in extras['hooks']:
706 706 try:
707 707 _call_hook('repo_size', extras, SvnMessageWriter())
708 708 except Exception:
709 709 pass
710 710
711 711 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,607 +1,607 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import sys
20 20 import base64
21 21 import locale
22 22 import logging
23 23 import uuid
24 24 import wsgiref.util
25 25 import traceback
26 26 import tempfile
27 27 from itertools import chain
28 28
29 29 import simplejson as json
30 30 import msgpack
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 34 from pyramid.compat import configparser
35 35
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
40 40 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
41 41
42 42 try:
43 43 locale.setlocale(locale.LC_ALL, '')
44 44 except locale.Error as e:
45 45 log.error(
46 46 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
47 47 os.environ['LC_ALL'] = 'C'
48 48
49 49 import vcsserver
50 50 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
51 51 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
52 52 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
53 53 from vcsserver.echo_stub.echo_app import EchoApp
54 54 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
55 55 from vcsserver.lib.exc_tracking import store_exception
56 56 from vcsserver.server import VcsServer
57 57
58 58 try:
59 59 from vcsserver.git import GitFactory, GitRemote
60 60 except ImportError:
61 61 GitFactory = None
62 62 GitRemote = None
63 63
64 64 try:
65 65 from vcsserver.hg import MercurialFactory, HgRemote
66 66 except ImportError:
67 67 MercurialFactory = None
68 68 HgRemote = None
69 69
70 70 try:
71 71 from vcsserver.svn import SubversionFactory, SvnRemote
72 72 except ImportError:
73 73 SubversionFactory = None
74 74 SvnRemote = None
75 75
76 76
77 77 def _is_request_chunked(environ):
78 78 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
79 79 return stream
80 80
81 81
82 82 def _int_setting(settings, name, default):
83 83 settings[name] = int(settings.get(name, default))
84 84 return settings[name]
85 85
86 86
87 87 def _bool_setting(settings, name, default):
88 88 input_val = settings.get(name, default)
89 89 if isinstance(input_val, unicode):
90 90 input_val = input_val.encode('utf8')
91 91 settings[name] = asbool(input_val)
92 92 return settings[name]
93 93
94 94
95 95 def _list_setting(settings, name, default):
96 96 raw_value = settings.get(name, default)
97 97
98 98 # Otherwise we assume it uses pyramids space/newline separation.
99 99 settings[name] = aslist(raw_value)
100 100 return settings[name]
101 101
102 102
103 103 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
104 104 value = settings.get(name, default)
105 105
106 106 if default_when_empty and not value:
107 107 # use default value when value is empty
108 108 value = default
109 109
110 110 if lower:
111 111 value = value.lower()
112 112 settings[name] = value
113 113 return settings[name]
114 114
115 115
116 116 class VCS(object):
117 117 def __init__(self, locale=None, cache_config=None):
118 118 self.locale = locale
119 119 self.cache_config = cache_config
120 120 self._configure_locale()
121 121
122 122 if GitFactory and GitRemote:
123 123 git_factory = GitFactory()
124 124 self._git_remote = GitRemote(git_factory)
125 125 else:
126 126 log.info("Git client import failed")
127 127
128 128 if MercurialFactory and HgRemote:
129 129 hg_factory = MercurialFactory()
130 130 self._hg_remote = HgRemote(hg_factory)
131 131 else:
132 132 log.info("Mercurial client import failed")
133 133
134 134 if SubversionFactory and SvnRemote:
135 135 svn_factory = SubversionFactory()
136 136
137 137 # hg factory is used for svn url validation
138 138 hg_factory = MercurialFactory()
139 139 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
140 140 else:
141 141 log.info("Subversion client import failed")
142 142
143 143 self._vcsserver = VcsServer()
144 144
145 145 def _configure_locale(self):
146 146 if self.locale:
147 147 log.info('Settings locale: `LC_ALL` to %s', self.locale)
148 148 else:
149 149 log.info(
150 150 'Configuring locale subsystem based on environment variables')
151 151 try:
152 152 # If self.locale is the empty string, then the locale
153 153 # module will use the environment variables. See the
154 154 # documentation of the package `locale`.
155 155 locale.setlocale(locale.LC_ALL, self.locale)
156 156
157 157 language_code, encoding = locale.getlocale()
158 158 log.info(
159 159 'Locale set to language code "%s" with encoding "%s".',
160 160 language_code, encoding)
161 161 except locale.Error:
162 162 log.exception(
163 163 'Cannot set locale, not configuring the locale system')
164 164
165 165
166 166 class WsgiProxy(object):
167 167 def __init__(self, wsgi):
168 168 self.wsgi = wsgi
169 169
170 170 def __call__(self, environ, start_response):
171 171 input_data = environ['wsgi.input'].read()
172 172 input_data = msgpack.unpackb(input_data)
173 173
174 174 error = None
175 175 try:
176 176 data, status, headers = self.wsgi.handle(
177 177 input_data['environment'], input_data['input_data'],
178 178 *input_data['args'], **input_data['kwargs'])
179 179 except Exception as e:
180 180 data, status, headers = [], None, None
181 181 error = {
182 182 'message': str(e),
183 183 '_vcs_kind': getattr(e, '_vcs_kind', None)
184 184 }
185 185
186 186 start_response(200, {})
187 187 return self._iterator(error, status, headers, data)
188 188
189 189 def _iterator(self, error, status, headers, data):
190 190 initial_data = [
191 191 error,
192 192 status,
193 193 headers,
194 194 ]
195 195
196 196 for d in chain(initial_data, data):
197 197 yield msgpack.packb(d)
198 198
199 199
200 200 def not_found(request):
201 201 return {'status': '404 NOT FOUND'}
202 202
203 203
204 204 class VCSViewPredicate(object):
205 205 def __init__(self, val, config):
206 206 self.remotes = val
207 207
208 208 def text(self):
209 209 return 'vcs view method = %s' % (self.remotes.keys(),)
210 210
211 211 phash = text
212 212
213 213 def __call__(self, context, request):
214 214 """
215 215 View predicate that returns true if given backend is supported by
216 216 defined remotes.
217 217 """
218 218 backend = request.matchdict.get('backend')
219 219 return backend in self.remotes
220 220
221 221
222 222 class HTTPApplication(object):
223 223 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
224 224
225 225 remote_wsgi = remote_wsgi
226 226 _use_echo_app = False
227 227
228 228 def __init__(self, settings=None, global_config=None):
229 229 self._sanitize_settings_and_apply_defaults(settings)
230 230
231 231 self.config = Configurator(settings=settings)
232 232 self.global_config = global_config
233 233 self.config.include('vcsserver.lib.rc_cache')
234 234
235 235 locale = settings.get('locale', '') or 'en_US.UTF-8'
236 236 vcs = VCS(locale=locale, cache_config=settings)
237 237 self._remotes = {
238 238 'hg': vcs._hg_remote,
239 239 'git': vcs._git_remote,
240 240 'svn': vcs._svn_remote,
241 241 'server': vcs._vcsserver,
242 242 }
243 243 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
244 244 self._use_echo_app = True
245 245 log.warning("Using EchoApp for VCS operations.")
246 246 self.remote_wsgi = remote_wsgi_stub
247 247
248 248 self._configure_settings(global_config, settings)
249 249 self._configure()
250 250
251 251 def _configure_settings(self, global_config, app_settings):
252 252 """
253 253 Configure the settings module.
254 254 """
255 255 settings_merged = global_config.copy()
256 256 settings_merged.update(app_settings)
257 257
258 258 git_path = app_settings.get('git_path', None)
259 259 if git_path:
260 260 settings.GIT_EXECUTABLE = git_path
261 261 binary_dir = app_settings.get('core.binary_dir', None)
262 262 if binary_dir:
263 263 settings.BINARY_DIR = binary_dir
264 264
265 265 # Store the settings to make them available to other modules.
266 266 vcsserver.PYRAMID_SETTINGS = settings_merged
267 267 vcsserver.CONFIG = settings_merged
268 268
269 269 def _sanitize_settings_and_apply_defaults(self, settings):
270 270 temp_store = tempfile.gettempdir()
271 271 default_cache_dir = os.path.join(temp_store, 'rc_cache')
272 272
273 273 # save default, cache dir, and use it for all backends later.
274 274 default_cache_dir = _string_setting(
275 275 settings,
276 276 'cache_dir',
277 277 default_cache_dir, lower=False, default_when_empty=True)
278 278
279 279 # ensure we have our dir created
280 280 if not os.path.isdir(default_cache_dir):
281 281 os.makedirs(default_cache_dir, mode=0o755)
282 282
283 283 # exception store cache
284 284 _string_setting(
285 285 settings,
286 286 'exception_tracker.store_path',
287 287 temp_store, lower=False, default_when_empty=True)
288 288
289 289 # repo_object cache
290 290 _string_setting(
291 291 settings,
292 292 'rc_cache.repo_object.backend',
293 293 'dogpile.cache.rc.memory_lru')
294 294 _int_setting(
295 295 settings,
296 296 'rc_cache.repo_object.expiration_time',
297 297 300)
298 298 _int_setting(
299 299 settings,
300 300 'rc_cache.repo_object.max_size',
301 301 1024)
302 302
303 303 def _configure(self):
304 304 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
305 305
306 306 self.config.add_route('service', '/_service')
307 307 self.config.add_route('status', '/status')
308 308 self.config.add_route('hg_proxy', '/proxy/hg')
309 309 self.config.add_route('git_proxy', '/proxy/git')
310 310 self.config.add_route('vcs', '/{backend}')
311 311 self.config.add_route('stream_git', '/stream/git/*repo_name')
312 312 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
313 313
314 314 self.config.add_view(self.status_view, route_name='status', renderer='json')
315 315 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
316 316
317 317 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
318 318 self.config.add_view(self.git_proxy(), route_name='git_proxy')
319 319 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
320 320 vcs_view=self._remotes)
321 321
322 322 self.config.add_view(self.hg_stream(), route_name='stream_hg')
323 323 self.config.add_view(self.git_stream(), route_name='stream_git')
324 324
325 325 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
326 326
327 327 self.config.add_notfound_view(not_found, renderer='json')
328 328
329 329 self.config.add_view(self.handle_vcs_exception, context=Exception)
330 330
331 331 self.config.add_tween(
332 332 'vcsserver.tweens.RequestWrapperTween',
333 333 )
334 334
335 335 def wsgi_app(self):
336 336 return self.config.make_wsgi_app()
337 337
338 338 def vcs_view(self, request):
339 339 remote = self._remotes[request.matchdict['backend']]
340 340 payload = msgpack.unpackb(request.body, use_list=True)
341 341 method = payload.get('method')
342 342 params = payload.get('params')
343 343 wire = params.get('wire')
344 344 args = params.get('args')
345 345 kwargs = params.get('kwargs')
346 346 context_uid = None
347 347
348 348 if wire:
349 349 try:
350 350 wire['context'] = context_uid = uuid.UUID(wire['context'])
351 351 except KeyError:
352 352 pass
353 353 args.insert(0, wire)
354 354
355 355 log.debug('method called:%s with kwargs:%s context_uid: %s',
356 356 method, kwargs, context_uid)
357 357 try:
358 358 resp = getattr(remote, method)(*args, **kwargs)
359 359 except Exception as e:
360 360 exc_info = list(sys.exc_info())
361 361 exc_type, exc_value, exc_traceback = exc_info
362 362
363 363 org_exc = getattr(e, '_org_exc', None)
364 364 org_exc_name = None
365 365 if org_exc:
366 366 org_exc_name = org_exc.__class__.__name__
367 367 # replace our "faked" exception with our org
368 368 exc_info[0] = org_exc.__class__
369 369 exc_info[1] = org_exc
370 370
371 371 store_exception(id(exc_info), exc_info)
372 372
373 373 tb_info = ''.join(
374 374 traceback.format_exception(exc_type, exc_value, exc_traceback))
375 375
376 376 type_ = e.__class__.__name__
377 377 if type_ not in self.ALLOWED_EXCEPTIONS:
378 378 type_ = None
379 379
380 380 resp = {
381 381 'id': payload.get('id'),
382 382 'error': {
383 383 'message': e.message,
384 384 'traceback': tb_info,
385 385 'org_exc': org_exc_name,
386 386 'type': type_
387 387 }
388 388 }
389 389 try:
390 390 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
391 391 except AttributeError:
392 392 pass
393 393 else:
394 394 resp = {
395 395 'id': payload.get('id'),
396 396 'result': resp
397 397 }
398 398
399 399 return resp
400 400
401 401 def status_view(self, request):
402 402 import vcsserver
403 403 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
404 404 'pid': os.getpid()}
405 405
406 406 def service_view(self, request):
407 407 import vcsserver
408 408
409 409 payload = msgpack.unpackb(request.body, use_list=True)
410 410
411 411 try:
412 412 path = self.global_config['__file__']
413 413 config = configparser.ConfigParser()
414 414 config.read(path)
415 415 parsed_ini = config
416 416 if parsed_ini.has_section('server:main'):
417 417 parsed_ini = dict(parsed_ini.items('server:main'))
418 418 except Exception:
419 419 log.exception('Failed to read .ini file for display')
420 420 parsed_ini = {}
421 421
422 422 resp = {
423 423 'id': payload.get('id'),
424 424 'result': dict(
425 425 version=vcsserver.__version__,
426 426 config=parsed_ini,
427 427 payload=payload,
428 428 )
429 429 }
430 430 return resp
431 431
432 432 def _msgpack_renderer_factory(self, info):
433 433 def _render(value, system):
434 434 value = msgpack.packb(value)
435 435 request = system.get('request')
436 436 if request is not None:
437 437 response = request.response
438 438 ct = response.content_type
439 439 if ct == response.default_content_type:
440 440 response.content_type = 'application/x-msgpack'
441 441 return value
442 442 return _render
443 443
444 444 def set_env_from_config(self, environ, config):
445 445 dict_conf = {}
446 446 try:
447 447 for elem in config:
448 448 if elem[0] == 'rhodecode':
449 449 dict_conf = json.loads(elem[2])
450 450 break
451 451 except Exception:
452 452 log.exception('Failed to fetch SCM CONFIG')
453 453 return
454 454
455 455 username = dict_conf.get('username')
456 456 if username:
457 457 environ['REMOTE_USER'] = username
458 458 # mercurial specific, some extension api rely on this
459 459 environ['HGUSER'] = username
460 460
461 461 ip = dict_conf.get('ip')
462 462 if ip:
463 463 environ['REMOTE_HOST'] = ip
464 464
465 465 if _is_request_chunked(environ):
466 466 # set the compatibility flag for webob
467 467 environ['wsgi.input_terminated'] = True
468 468
469 469 def hg_proxy(self):
470 470 @wsgiapp
471 471 def _hg_proxy(environ, start_response):
472 472 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
473 473 return app(environ, start_response)
474 474 return _hg_proxy
475 475
476 476 def git_proxy(self):
477 477 @wsgiapp
478 478 def _git_proxy(environ, start_response):
479 479 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
480 480 return app(environ, start_response)
481 481 return _git_proxy
482 482
483 483 def hg_stream(self):
484 484 if self._use_echo_app:
485 485 @wsgiapp
486 486 def _hg_stream(environ, start_response):
487 487 app = EchoApp('fake_path', 'fake_name', None)
488 488 return app(environ, start_response)
489 489 return _hg_stream
490 490 else:
491 491 @wsgiapp
492 492 def _hg_stream(environ, start_response):
493 493 log.debug('http-app: handling hg stream')
494 494 repo_path = environ['HTTP_X_RC_REPO_PATH']
495 495 repo_name = environ['HTTP_X_RC_REPO_NAME']
496 496 packed_config = base64.b64decode(
497 497 environ['HTTP_X_RC_REPO_CONFIG'])
498 498 config = msgpack.unpackb(packed_config)
499 499 app = scm_app.create_hg_wsgi_app(
500 500 repo_path, repo_name, config)
501 501
502 502 # Consistent path information for hgweb
503 503 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
504 504 environ['REPO_NAME'] = repo_name
505 505 self.set_env_from_config(environ, config)
506 506
507 507 log.debug('http-app: starting app handler '
508 508 'with %s and process request', app)
509 509 return app(environ, ResponseFilter(start_response))
510 510 return _hg_stream
511 511
512 512 def git_stream(self):
513 513 if self._use_echo_app:
514 514 @wsgiapp
515 515 def _git_stream(environ, start_response):
516 516 app = EchoApp('fake_path', 'fake_name', None)
517 517 return app(environ, start_response)
518 518 return _git_stream
519 519 else:
520 520 @wsgiapp
521 521 def _git_stream(environ, start_response):
522 522 log.debug('http-app: handling git stream')
523 523 repo_path = environ['HTTP_X_RC_REPO_PATH']
524 524 repo_name = environ['HTTP_X_RC_REPO_NAME']
525 525 packed_config = base64.b64decode(
526 526 environ['HTTP_X_RC_REPO_CONFIG'])
527 527 config = msgpack.unpackb(packed_config)
528 528
529 529 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
530 530 self.set_env_from_config(environ, config)
531 531
532 532 content_type = environ.get('CONTENT_TYPE', '')
533 533
534 534 path = environ['PATH_INFO']
535 535 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
536 536 log.debug(
537 537 'LFS: Detecting if request `%s` is LFS server path based '
538 538 'on content type:`%s`, is_lfs:%s',
539 539 path, content_type, is_lfs_request)
540 540
541 541 if not is_lfs_request:
542 542 # fallback detection by path
543 543 if GIT_LFS_PROTO_PAT.match(path):
544 544 is_lfs_request = True
545 545 log.debug(
546 546 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
547 547 path, is_lfs_request)
548 548
549 549 if is_lfs_request:
550 550 app = scm_app.create_git_lfs_wsgi_app(
551 551 repo_path, repo_name, config)
552 552 else:
553 553 app = scm_app.create_git_wsgi_app(
554 554 repo_path, repo_name, config)
555 555
556 556 log.debug('http-app: starting app handler '
557 557 'with %s and process request', app)
558 558
559 559 return app(environ, start_response)
560 560
561 561 return _git_stream
562 562
563 563 def handle_vcs_exception(self, exception, request):
564 564 _vcs_kind = getattr(exception, '_vcs_kind', '')
565 565 if _vcs_kind == 'repo_locked':
566 566 # Get custom repo-locked status code if present.
567 567 status_code = request.headers.get('X-RC-Locked-Status-Code')
568 568 return HTTPRepoLocked(
569 569 title=exception.message, status_code=status_code)
570 570
571 571 elif _vcs_kind == 'repo_branch_protected':
572 572 # Get custom repo-branch-protected status code if present.
573 573 return HTTPRepoBranchProtected(title=exception.message)
574 574
575 575 exc_info = request.exc_info
576 576 store_exception(id(exc_info), exc_info)
577 577
578 578 traceback_info = 'unavailable'
579 579 if request.exc_info:
580 580 exc_type, exc_value, exc_tb = request.exc_info
581 581 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
582 582
583 583 log.error(
584 584 'error occurred handling this request for path: %s, \n tb: %s',
585 585 request.path, traceback_info)
586 586 raise exception
587 587
588 588
589 589 class ResponseFilter(object):
590 590
591 591 def __init__(self, start_response):
592 592 self._start_response = start_response
593 593
594 594 def __call__(self, status, response_headers, exc_info=None):
595 595 headers = tuple(
596 596 (h, v) for h, v in response_headers
597 597 if not wsgiref.util.is_hop_by_hop(h))
598 598 return self._start_response(status, headers, exc_info)
599 599
600 600
601 601 def main(global_config, **settings):
602 602 if MercurialFactory:
603 603 hgpatches.patch_largefiles_capabilities()
604 604 hgpatches.patch_subrepo_type_mapping()
605 605
606 606 app = HTTPApplication(settings=settings, global_config=global_config)
607 607 return app.wsgi_app()
@@ -1,16 +1,16 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
@@ -1,151 +1,151 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2018 RhodeCode GmbH
4 # Copyright (C) 2014-2019 RhodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 20
21 21 import os
22 22 import time
23 23 import datetime
24 24 import msgpack
25 25 import logging
26 26 import traceback
27 27 import tempfile
28 28
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32 # NOTE: Any changes should be synced with exc_tracking at rhodecode.lib.exc_tracking
33 33 global_prefix = 'vcsserver'
34 34 exc_store_dir_name = 'rc_exception_store_v1'
35 35
36 36
37 37 def exc_serialize(exc_id, tb, exc_type):
38 38
39 39 data = {
40 40 'version': 'v1',
41 41 'exc_id': exc_id,
42 42 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
43 43 'exc_timestamp': repr(time.time()),
44 44 'exc_message': tb,
45 45 'exc_type': exc_type,
46 46 }
47 47 return msgpack.packb(data), data
48 48
49 49
50 50 def exc_unserialize(tb):
51 51 return msgpack.unpackb(tb)
52 52
53 53
54 54 def get_exc_store():
55 55 """
56 56 Get and create exception store if it's not existing
57 57 """
58 58 import vcsserver as app
59 59
60 60 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
61 61 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
62 62
63 63 _exc_store_path = os.path.abspath(_exc_store_path)
64 64 if not os.path.isdir(_exc_store_path):
65 65 os.makedirs(_exc_store_path)
66 66 log.debug('Initializing exceptions store at %s', _exc_store_path)
67 67 return _exc_store_path
68 68
69 69
70 70 def _store_exception(exc_id, exc_info, prefix):
71 71 exc_type, exc_value, exc_traceback = exc_info
72 72 tb = ''.join(traceback.format_exception(
73 73 exc_type, exc_value, exc_traceback, None))
74 74
75 75 exc_type_name = exc_type.__name__
76 76 exc_store_path = get_exc_store()
77 77 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
78 78 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
79 79 if not os.path.isdir(exc_store_path):
80 80 os.makedirs(exc_store_path)
81 81 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
82 82 with open(stored_exc_path, 'wb') as f:
83 83 f.write(exc_data)
84 84 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
85 85
86 86
87 87 def store_exception(exc_id, exc_info, prefix=global_prefix):
88 88 """
89 89 Example usage::
90 90
91 91 exc_info = sys.exc_info()
92 92 store_exception(id(exc_info), exc_info)
93 93 """
94 94
95 95 try:
96 96 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
97 97 except Exception:
98 98 log.exception('Failed to store exception `%s` information', exc_id)
99 99 # there's no way this can fail, it will crash server badly if it does.
100 100 pass
101 101
102 102
103 103 def _find_exc_file(exc_id, prefix=global_prefix):
104 104 exc_store_path = get_exc_store()
105 105 if prefix:
106 106 exc_id = '{}_{}'.format(exc_id, prefix)
107 107 else:
108 108 # search without a prefix
109 109 exc_id = '{}'.format(exc_id)
110 110
111 111 # we need to search the store for such start pattern as above
112 112 for fname in os.listdir(exc_store_path):
113 113 if fname.startswith(exc_id):
114 114 exc_id = os.path.join(exc_store_path, fname)
115 115 break
116 116 continue
117 117 else:
118 118 exc_id = None
119 119
120 120 return exc_id
121 121
122 122
123 123 def _read_exception(exc_id, prefix):
124 124 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
125 125 if exc_id_file_path:
126 126 with open(exc_id_file_path, 'rb') as f:
127 127 return exc_unserialize(f.read())
128 128 else:
129 129 log.debug('Exception File `%s` not found', exc_id_file_path)
130 130 return None
131 131
132 132
133 133 def read_exception(exc_id, prefix=global_prefix):
134 134 try:
135 135 return _read_exception(exc_id=exc_id, prefix=prefix)
136 136 except Exception:
137 137 log.exception('Failed to read exception `%s` information', exc_id)
138 138 # there's no way this can fail, it will crash server badly if it does.
139 139 return None
140 140
141 141
142 142 def delete_exception(exc_id, prefix=global_prefix):
143 143 try:
144 144 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
145 145 if exc_id_file_path:
146 146 os.remove(exc_id_file_path)
147 147
148 148 except Exception:
149 149 log.exception('Failed to remove exception `%s` information', exc_id)
150 150 # there's no way this can fail, it will crash server badly if it does.
151 151 pass
@@ -1,65 +1,65 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2018 RhodeCode GmbH
4 # Copyright (C) 2014-2019 RhodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 20
21 21 import logging
22 22
23 23 from repoze.lru import LRUCache
24 24
25 25 from vcsserver.utils import safe_str
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29
30 30 class LRUDict(LRUCache):
31 31 """
32 32 Wrapper to provide partial dict access
33 33 """
34 34
35 35 def __setitem__(self, key, value):
36 36 return self.put(key, value)
37 37
38 38 def __getitem__(self, key):
39 39 return self.get(key)
40 40
41 41 def __contains__(self, key):
42 42 return bool(self.get(key))
43 43
44 44 def __delitem__(self, key):
45 45 del self.data[key]
46 46
47 47 def keys(self):
48 48 return self.data.keys()
49 49
50 50
51 51 class LRUDictDebug(LRUDict):
52 52 """
53 53 Wrapper to provide some debug options
54 54 """
55 55 def _report_keys(self):
56 56 elems_cnt = '%s/%s' % (len(self.keys()), self.size)
57 57 # trick for pformat print it more nicely
58 58 fmt = '\n'
59 59 for cnt, elem in enumerate(self.keys()):
60 60 fmt += '%s - %s\n' % (cnt+1, safe_str(elem))
61 61 log.debug('current LRU keys (%s):%s', elems_cnt, fmt)
62 62
63 63 def __getitem__(self, key):
64 64 self._report_keys()
65 65 return self.get(key)
@@ -1,60 +1,60 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import logging
19 19 from dogpile.cache import register_backend
20 20
21 21 register_backend(
22 22 "dogpile.cache.rc.memory_lru", "vcsserver.lib.rc_cache.backends",
23 23 "LRUMemoryBackend")
24 24
25 25 log = logging.getLogger(__name__)
26 26
27 27 from . import region_meta
28 28 from .util import key_generator, get_default_cache_settings, make_region
29 29
30 30
31 31 def configure_dogpile_cache(settings):
32 32 cache_dir = settings.get('cache_dir')
33 33 if cache_dir:
34 34 region_meta.dogpile_config_defaults['cache_dir'] = cache_dir
35 35
36 36 rc_cache_data = get_default_cache_settings(settings, prefixes=['rc_cache.'])
37 37
38 38 # inspect available namespaces
39 39 avail_regions = set()
40 40 for key in rc_cache_data.keys():
41 41 namespace_name = key.split('.', 1)[0]
42 42 avail_regions.add(namespace_name)
43 43 log.debug('dogpile: found following cache regions: %s', avail_regions)
44 44
45 45 # register them into namespace
46 46 for region_name in avail_regions:
47 47 new_region = make_region(
48 48 name=region_name,
49 49 function_key_generator=key_generator
50 50 )
51 51
52 52 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(region_name))
53 53
54 54 log.debug('dogpile: registering a new region %s[%s]',
55 55 region_name, new_region.__dict__)
56 56 region_meta.dogpile_cache_regions[region_name] = new_region
57 57
58 58
59 59 def includeme(config):
60 60 configure_dogpile_cache(config.registry.settings)
@@ -1,51 +1,51 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import logging
19 19
20 20 from dogpile.cache.backends import memory as memory_backend
21 21 from vcsserver.lib.memory_lru_dict import LRUDict, LRUDictDebug
22 22
23 23
24 24 _default_max_size = 1024
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 29 class LRUMemoryBackend(memory_backend.MemoryBackend):
30 30 pickle_values = False
31 31
32 32 def __init__(self, arguments):
33 33 max_size = arguments.pop('max_size', _default_max_size)
34 34
35 35 LRUDictClass = LRUDict
36 36 if arguments.pop('log_key_count', None):
37 37 LRUDictClass = LRUDictDebug
38 38
39 39 arguments['cache_dict'] = LRUDictClass(max_size)
40 40 super(LRUMemoryBackend, self).__init__(arguments)
41 41
42 42 def delete(self, key):
43 43 try:
44 44 del self._cache[key]
45 45 except KeyError:
46 46 # we don't care if key isn't there at deletion
47 47 pass
48 48
49 49 def delete_multi(self, keys):
50 50 for key in keys:
51 51 self.delete(key)
@@ -1,26 +1,26 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import tempfile
20 20
21 21 dogpile_config_defaults = {
22 22 'cache_dir': os.path.join(tempfile.gettempdir(), 'rc_cache')
23 23 }
24 24
25 25 # GLOBAL TO STORE ALL REGISTERED REGIONS
26 26 dogpile_cache_regions = {}
@@ -1,136 +1,136 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import logging
20 20 import functools
21 21
22 22 from vcsserver.utils import safe_str, sha1
23 23 from dogpile.cache import CacheRegion
24 24 from dogpile.cache.util import compat
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 29 class RhodeCodeCacheRegion(CacheRegion):
30 30
31 31 def conditional_cache_on_arguments(
32 32 self, namespace=None,
33 33 expiration_time=None,
34 34 should_cache_fn=None,
35 35 to_str=compat.string_type,
36 36 function_key_generator=None,
37 37 condition=True):
38 38 """
39 39 Custom conditional decorator, that will not touch any dogpile internals if
40 40 condition isn't meet. This works a bit different than should_cache_fn
41 41 And it's faster in cases we don't ever want to compute cached values
42 42 """
43 43 expiration_time_is_callable = compat.callable(expiration_time)
44 44
45 45 if function_key_generator is None:
46 46 function_key_generator = self.function_key_generator
47 47
48 48 def decorator(fn):
49 49 if to_str is compat.string_type:
50 50 # backwards compatible
51 51 key_generator = function_key_generator(namespace, fn)
52 52 else:
53 53 key_generator = function_key_generator(namespace, fn, to_str=to_str)
54 54
55 55 @functools.wraps(fn)
56 56 def decorate(*arg, **kw):
57 57 key = key_generator(*arg, **kw)
58 58
59 59 @functools.wraps(fn)
60 60 def creator():
61 61 return fn(*arg, **kw)
62 62
63 63 if not condition:
64 64 return creator()
65 65
66 66 timeout = expiration_time() if expiration_time_is_callable \
67 67 else expiration_time
68 68
69 69 return self.get_or_create(key, creator, timeout, should_cache_fn)
70 70
71 71 def invalidate(*arg, **kw):
72 72 key = key_generator(*arg, **kw)
73 73 self.delete(key)
74 74
75 75 def set_(value, *arg, **kw):
76 76 key = key_generator(*arg, **kw)
77 77 self.set(key, value)
78 78
79 79 def get(*arg, **kw):
80 80 key = key_generator(*arg, **kw)
81 81 return self.get(key)
82 82
83 83 def refresh(*arg, **kw):
84 84 key = key_generator(*arg, **kw)
85 85 value = fn(*arg, **kw)
86 86 self.set(key, value)
87 87 return value
88 88
89 89 decorate.set = set_
90 90 decorate.invalidate = invalidate
91 91 decorate.refresh = refresh
92 92 decorate.get = get
93 93 decorate.original = fn
94 94 decorate.key_generator = key_generator
95 95
96 96 return decorate
97 97
98 98 return decorator
99 99
100 100
101 101 def make_region(*arg, **kw):
102 102 return RhodeCodeCacheRegion(*arg, **kw)
103 103
104 104
105 105 def get_default_cache_settings(settings, prefixes=None):
106 106 prefixes = prefixes or []
107 107 cache_settings = {}
108 108 for key in settings.keys():
109 109 for prefix in prefixes:
110 110 if key.startswith(prefix):
111 111 name = key.split(prefix)[1].strip()
112 112 val = settings[key]
113 113 if isinstance(val, basestring):
114 114 val = val.strip()
115 115 cache_settings[name] = val
116 116 return cache_settings
117 117
118 118
119 119 def compute_key_from_params(*args):
120 120 """
121 121 Helper to compute key from given params to be used in cache manager
122 122 """
123 123 return sha1("_".join(map(safe_str, args)))
124 124
125 125
126 126 def key_generator(namespace, fn):
127 127 fname = fn.__name__
128 128
129 129 def generate_key(*args):
130 130 namespace_pref = namespace or 'default'
131 131 arg_key = compute_key_from_params(*args)
132 132 final_key = "{}:{}_{}".format(namespace_pref, fname, arg_key)
133 133
134 134 return final_key
135 135
136 136 return generate_key
@@ -1,386 +1,386 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 """Handles the Git smart protocol."""
19 19
20 20 import os
21 21 import socket
22 22 import logging
23 23
24 24 import simplejson as json
25 25 import dulwich.protocol
26 26 from webob import Request, Response, exc
27 27
28 28 from vcsserver import hooks, subprocessio
29 29
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class FileWrapper(object):
35 35 """File wrapper that ensures how much data is read from it."""
36 36
37 37 def __init__(self, fd, content_length):
38 38 self.fd = fd
39 39 self.content_length = content_length
40 40 self.remain = content_length
41 41
42 42 def read(self, size):
43 43 if size <= self.remain:
44 44 try:
45 45 data = self.fd.read(size)
46 46 except socket.error:
47 47 raise IOError(self)
48 48 self.remain -= size
49 49 elif self.remain:
50 50 data = self.fd.read(self.remain)
51 51 self.remain = 0
52 52 else:
53 53 data = None
54 54 return data
55 55
56 56 def __repr__(self):
57 57 return '<FileWrapper %s len: %s, read: %s>' % (
58 58 self.fd, self.content_length, self.content_length - self.remain
59 59 )
60 60
61 61
62 62 class GitRepository(object):
63 63 """WSGI app for handling Git smart protocol endpoints."""
64 64
65 65 git_folder_signature = frozenset(
66 66 ('config', 'head', 'info', 'objects', 'refs'))
67 67 commands = frozenset(('git-upload-pack', 'git-receive-pack'))
68 68 valid_accepts = frozenset(('application/x-%s-result' %
69 69 c for c in commands))
70 70
71 71 # The last bytes are the SHA1 of the first 12 bytes.
72 72 EMPTY_PACK = (
73 73 'PACK\x00\x00\x00\x02\x00\x00\x00\x00' +
74 74 '\x02\x9d\x08\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e'
75 75 )
76 76 SIDE_BAND_CAPS = frozenset(('side-band', 'side-band-64k'))
77 77
78 78 def __init__(self, repo_name, content_path, git_path, update_server_info,
79 79 extras):
80 80 files = frozenset(f.lower() for f in os.listdir(content_path))
81 81 valid_dir_signature = self.git_folder_signature.issubset(files)
82 82
83 83 if not valid_dir_signature:
84 84 raise OSError('%s missing git signature' % content_path)
85 85
86 86 self.content_path = content_path
87 87 self.repo_name = repo_name
88 88 self.extras = extras
89 89 self.git_path = git_path
90 90 self.update_server_info = update_server_info
91 91
92 92 def _get_fixedpath(self, path):
93 93 """
94 94 Small fix for repo_path
95 95
96 96 :param path:
97 97 """
98 98 path = path.split(self.repo_name, 1)[-1]
99 99 if path.startswith('.git'):
100 100 # for bare repos we still get the .git prefix inside, we skip it
101 101 # here, and remove from the service command
102 102 path = path[4:]
103 103
104 104 return path.strip('/')
105 105
106 106 def inforefs(self, request, unused_environ):
107 107 """
108 108 WSGI Response producer for HTTP GET Git Smart
109 109 HTTP /info/refs request.
110 110 """
111 111
112 112 git_command = request.GET.get('service')
113 113 if git_command not in self.commands:
114 114 log.debug('command %s not allowed', git_command)
115 115 return exc.HTTPForbidden()
116 116
117 117 # please, resist the urge to add '\n' to git capture and increment
118 118 # line count by 1.
119 119 # by git docs: Documentation/technical/http-protocol.txt#L214 \n is
120 120 # a part of protocol.
121 121 # The code in Git client not only does NOT need '\n', but actually
122 122 # blows up if you sprinkle "flush" (0000) as "0001\n".
123 123 # It reads binary, per number of bytes specified.
124 124 # if you do add '\n' as part of data, count it.
125 125 server_advert = '# service=%s\n' % git_command
126 126 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
127 127 try:
128 128 gitenv = dict(os.environ)
129 129 # forget all configs
130 130 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
131 131 command = [self.git_path, git_command[4:], '--stateless-rpc',
132 132 '--advertise-refs', self.content_path]
133 133 out = subprocessio.SubprocessIOChunker(
134 134 command,
135 135 env=gitenv,
136 136 starting_values=[packet_len + server_advert + '0000'],
137 137 shell=False
138 138 )
139 139 except EnvironmentError:
140 140 log.exception('Error processing command')
141 141 raise exc.HTTPExpectationFailed()
142 142
143 143 resp = Response()
144 144 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
145 145 resp.charset = None
146 146 resp.app_iter = out
147 147
148 148 return resp
149 149
150 150 def _get_want_capabilities(self, request):
151 151 """Read the capabilities found in the first want line of the request."""
152 152 pos = request.body_file_seekable.tell()
153 153 first_line = request.body_file_seekable.readline()
154 154 request.body_file_seekable.seek(pos)
155 155
156 156 return frozenset(
157 157 dulwich.protocol.extract_want_line_capabilities(first_line)[1])
158 158
159 159 def _build_failed_pre_pull_response(self, capabilities, pre_pull_messages):
160 160 """
161 161 Construct a response with an empty PACK file.
162 162
163 163 We use an empty PACK file, as that would trigger the failure of the pull
164 164 or clone command.
165 165
166 166 We also print in the error output a message explaining why the command
167 167 was aborted.
168 168
169 169 If aditionally, the user is accepting messages we send them the output
170 170 of the pre-pull hook.
171 171
172 172 Note that for clients not supporting side-band we just send them the
173 173 emtpy PACK file.
174 174 """
175 175 if self.SIDE_BAND_CAPS.intersection(capabilities):
176 176 response = []
177 177 proto = dulwich.protocol.Protocol(None, response.append)
178 178 proto.write_pkt_line('NAK\n')
179 179 self._write_sideband_to_proto(pre_pull_messages, proto,
180 180 capabilities)
181 181 # N.B.(skreft): Do not change the sideband channel to 3, as that
182 182 # produces a fatal error in the client:
183 183 # fatal: error in sideband demultiplexer
184 184 proto.write_sideband(2, 'Pre pull hook failed: aborting\n')
185 185 proto.write_sideband(1, self.EMPTY_PACK)
186 186
187 187 # writes 0000
188 188 proto.write_pkt_line(None)
189 189
190 190 return response
191 191 else:
192 192 return [self.EMPTY_PACK]
193 193
194 194 def _write_sideband_to_proto(self, data, proto, capabilities):
195 195 """
196 196 Write the data to the proto's sideband number 2.
197 197
198 198 We do not use dulwich's write_sideband directly as it only supports
199 199 side-band-64k.
200 200 """
201 201 if not data:
202 202 return
203 203
204 204 # N.B.(skreft): The values below are explained in the pack protocol
205 205 # documentation, section Packfile Data.
206 206 # https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt
207 207 if 'side-band-64k' in capabilities:
208 208 chunk_size = 65515
209 209 elif 'side-band' in capabilities:
210 210 chunk_size = 995
211 211 else:
212 212 return
213 213
214 214 chunker = (
215 215 data[i:i + chunk_size] for i in xrange(0, len(data), chunk_size))
216 216
217 217 for chunk in chunker:
218 218 proto.write_sideband(2, chunk)
219 219
220 220 def _get_messages(self, data, capabilities):
221 221 """Return a list with packets for sending data in sideband number 2."""
222 222 response = []
223 223 proto = dulwich.protocol.Protocol(None, response.append)
224 224
225 225 self._write_sideband_to_proto(data, proto, capabilities)
226 226
227 227 return response
228 228
229 229 def _inject_messages_to_response(self, response, capabilities,
230 230 start_messages, end_messages):
231 231 """
232 232 Given a list response we inject the pre/post-pull messages.
233 233
234 234 We only inject the messages if the client supports sideband, and the
235 235 response has the format:
236 236 0008NAK\n...0000
237 237
238 238 Note that we do not check the no-progress capability as by default, git
239 239 sends it, which effectively would block all messages.
240 240 """
241 241 if not self.SIDE_BAND_CAPS.intersection(capabilities):
242 242 return response
243 243
244 244 if not start_messages and not end_messages:
245 245 return response
246 246
247 247 # make a list out of response if it's an iterator
248 248 # so we can investigate it for message injection.
249 249 if hasattr(response, '__iter__'):
250 250 response = list(response)
251 251
252 252 if (not response[0].startswith('0008NAK\n') or
253 253 not response[-1].endswith('0000')):
254 254 return response
255 255
256 256 new_response = ['0008NAK\n']
257 257 new_response.extend(self._get_messages(start_messages, capabilities))
258 258 if len(response) == 1:
259 259 new_response.append(response[0][8:-4])
260 260 else:
261 261 new_response.append(response[0][8:])
262 262 new_response.extend(response[1:-1])
263 263 new_response.append(response[-1][:-4])
264 264 new_response.extend(self._get_messages(end_messages, capabilities))
265 265 new_response.append('0000')
266 266
267 267 return new_response
268 268
269 269 def backend(self, request, environ):
270 270 """
271 271 WSGI Response producer for HTTP POST Git Smart HTTP requests.
272 272 Reads commands and data from HTTP POST's body.
273 273 returns an iterator obj with contents of git command's
274 274 response to stdout
275 275 """
276 276 # TODO(skreft): think how we could detect an HTTPLockedException, as
277 277 # we probably want to have the same mechanism used by mercurial and
278 278 # simplevcs.
279 279 # For that we would need to parse the output of the command looking for
280 280 # some signs of the HTTPLockedError, parse the data and reraise it in
281 281 # pygrack. However, that would interfere with the streaming.
282 282 #
283 283 # Now the output of a blocked push is:
284 284 # Pushing to http://test_regular:test12@127.0.0.1:5001/vcs_test_git
285 285 # POST git-receive-pack (1047 bytes)
286 286 # remote: ERROR: Repository `vcs_test_git` locked by user `test_admin`. Reason:`lock_auto`
287 287 # To http://test_regular:test12@127.0.0.1:5001/vcs_test_git
288 288 # ! [remote rejected] master -> master (pre-receive hook declined)
289 289 # error: failed to push some refs to 'http://test_regular:test12@127.0.0.1:5001/vcs_test_git'
290 290
291 291 git_command = self._get_fixedpath(request.path_info)
292 292 if git_command not in self.commands:
293 293 log.debug('command %s not allowed', git_command)
294 294 return exc.HTTPForbidden()
295 295
296 296 capabilities = None
297 297 if git_command == 'git-upload-pack':
298 298 capabilities = self._get_want_capabilities(request)
299 299
300 300 if 'CONTENT_LENGTH' in environ:
301 301 inputstream = FileWrapper(request.body_file_seekable,
302 302 request.content_length)
303 303 else:
304 304 inputstream = request.body_file_seekable
305 305
306 306 resp = Response()
307 307 resp.content_type = ('application/x-%s-result' %
308 308 git_command.encode('utf8'))
309 309 resp.charset = None
310 310
311 311 pre_pull_messages = ''
312 312 if git_command == 'git-upload-pack':
313 313 status, pre_pull_messages = hooks.git_pre_pull(self.extras)
314 314 if status != 0:
315 315 resp.app_iter = self._build_failed_pre_pull_response(
316 316 capabilities, pre_pull_messages)
317 317 return resp
318 318
319 319 gitenv = dict(os.environ)
320 320 # forget all configs
321 321 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
322 322 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
323 323 cmd = [self.git_path, git_command[4:], '--stateless-rpc',
324 324 self.content_path]
325 325 log.debug('handling cmd %s', cmd)
326 326
327 327 out = subprocessio.SubprocessIOChunker(
328 328 cmd,
329 329 inputstream=inputstream,
330 330 env=gitenv,
331 331 cwd=self.content_path,
332 332 shell=False,
333 333 fail_on_stderr=False,
334 334 fail_on_return_code=False
335 335 )
336 336
337 337 if self.update_server_info and git_command == 'git-receive-pack':
338 338 # We need to fully consume the iterator here, as the
339 339 # update-server-info command needs to be run after the push.
340 340 out = list(out)
341 341
342 342 # Updating refs manually after each push.
343 343 # This is required as some clients are exposing Git repos internally
344 344 # with the dumb protocol.
345 345 cmd = [self.git_path, 'update-server-info']
346 346 log.debug('handling cmd %s', cmd)
347 347 output = subprocessio.SubprocessIOChunker(
348 348 cmd,
349 349 inputstream=inputstream,
350 350 env=gitenv,
351 351 cwd=self.content_path,
352 352 shell=False,
353 353 fail_on_stderr=False,
354 354 fail_on_return_code=False
355 355 )
356 356 # Consume all the output so the subprocess finishes
357 357 for _ in output:
358 358 pass
359 359
360 360 if git_command == 'git-upload-pack':
361 361 unused_status, post_pull_messages = hooks.git_post_pull(self.extras)
362 362 resp.app_iter = self._inject_messages_to_response(
363 363 out, capabilities, pre_pull_messages, post_pull_messages)
364 364 else:
365 365 resp.app_iter = out
366 366
367 367 return resp
368 368
369 369 def __call__(self, environ, start_response):
370 370 request = Request(environ)
371 371 _path = self._get_fixedpath(request.path_info)
372 372 if _path.startswith('info/refs'):
373 373 app = self.inforefs
374 374 else:
375 375 app = self.backend
376 376
377 377 try:
378 378 resp = app(request, environ)
379 379 except exc.HTTPException as error:
380 380 log.exception('HTTP Error')
381 381 resp = error
382 382 except Exception:
383 383 log.exception('Unknown error')
384 384 resp = exc.HTTPInternalServerError()
385 385
386 386 return resp(environ, start_response)
@@ -1,34 +1,34 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 from vcsserver import scm_app, wsgi_app_caller
19 19
20 20
21 21 class GitRemoteWsgi(object):
22 22 def handle(self, environ, input_data, *args, **kwargs):
23 23 app = wsgi_app_caller.WSGIAppCaller(
24 24 scm_app.create_git_wsgi_app(*args, **kwargs))
25 25
26 26 return app.handle(environ, input_data)
27 27
28 28
29 29 class HgRemoteWsgi(object):
30 30 def handle(self, environ, input_data, *args, **kwargs):
31 31 app = wsgi_app_caller.WSGIAppCaller(
32 32 scm_app.create_hg_wsgi_app(*args, **kwargs))
33 33
34 34 return app.handle(environ, input_data)
@@ -1,234 +1,234 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import logging
20 20 import itertools
21 21
22 22 import mercurial
23 23 import mercurial.error
24 24 import mercurial.wireprotoserver
25 25 import mercurial.hgweb.common
26 26 import mercurial.hgweb.hgweb_mod
27 27 import webob.exc
28 28
29 29 from vcsserver import pygrack, exceptions, settings, git_lfs
30 30
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 # propagated from mercurial documentation
36 36 HG_UI_SECTIONS = [
37 37 'alias', 'auth', 'decode/encode', 'defaults', 'diff', 'email', 'extensions',
38 38 'format', 'merge-patterns', 'merge-tools', 'hooks', 'http_proxy', 'smtp',
39 39 'patch', 'paths', 'profiling', 'server', 'trusted', 'ui', 'web',
40 40 ]
41 41
42 42
43 43 class HgWeb(mercurial.hgweb.hgweb_mod.hgweb):
44 44 """Extension of hgweb that simplifies some functions."""
45 45
46 46 def _get_view(self, repo):
47 47 """Views are not supported."""
48 48 return repo
49 49
50 50 def loadsubweb(self):
51 51 """The result is only used in the templater method which is not used."""
52 52 return None
53 53
54 54 def run(self):
55 55 """Unused function so raise an exception if accidentally called."""
56 56 raise NotImplementedError
57 57
58 58 def templater(self, req):
59 59 """Function used in an unreachable code path.
60 60
61 61 This code is unreachable because we guarantee that the HTTP request,
62 62 corresponds to a Mercurial command. See the is_hg method. So, we are
63 63 never going to get a user-visible url.
64 64 """
65 65 raise NotImplementedError
66 66
67 67 def archivelist(self, nodeid):
68 68 """Unused function so raise an exception if accidentally called."""
69 69 raise NotImplementedError
70 70
71 71 def __call__(self, environ, start_response):
72 72 """Run the WSGI application.
73 73
74 74 This may be called by multiple threads.
75 75 """
76 76 from mercurial.hgweb import request as requestmod
77 77 req = requestmod.parserequestfromenv(environ)
78 78 res = requestmod.wsgiresponse(req, start_response)
79 79 gen = self.run_wsgi(req, res)
80 80
81 81 first_chunk = None
82 82
83 83 try:
84 84 data = gen.next()
85 85
86 86 def first_chunk():
87 87 yield data
88 88 except StopIteration:
89 89 pass
90 90
91 91 if first_chunk:
92 92 return itertools.chain(first_chunk(), gen)
93 93 return gen
94 94
95 95 def _runwsgi(self, req, res, repo):
96 96
97 97 cmd = req.qsparams.get('cmd', '')
98 98 if not mercurial.wireprotoserver.iscmd(cmd):
99 99 # NOTE(marcink): for unsupported commands, we return bad request
100 100 # internally from HG
101 101 from mercurial.hgweb.common import statusmessage
102 102 res.status = statusmessage(mercurial.hgweb.common.HTTP_BAD_REQUEST)
103 103 res.setbodybytes('')
104 104 return res.sendresponse()
105 105
106 106 return super(HgWeb, self)._runwsgi(req, res, repo)
107 107
108 108
109 109 def make_hg_ui_from_config(repo_config):
110 110 baseui = mercurial.ui.ui()
111 111
112 112 # clean the baseui object
113 113 baseui._ocfg = mercurial.config.config()
114 114 baseui._ucfg = mercurial.config.config()
115 115 baseui._tcfg = mercurial.config.config()
116 116
117 117 for section, option, value in repo_config:
118 118 baseui.setconfig(section, option, value)
119 119
120 120 # make our hgweb quiet so it doesn't print output
121 121 baseui.setconfig('ui', 'quiet', 'true')
122 122
123 123 return baseui
124 124
125 125
126 126 def update_hg_ui_from_hgrc(baseui, repo_path):
127 127 path = os.path.join(repo_path, '.hg', 'hgrc')
128 128
129 129 if not os.path.isfile(path):
130 130 log.debug('hgrc file is not present at %s, skipping...', path)
131 131 return
132 132 log.debug('reading hgrc from %s', path)
133 133 cfg = mercurial.config.config()
134 134 cfg.read(path)
135 135 for section in HG_UI_SECTIONS:
136 136 for k, v in cfg.items(section):
137 137 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
138 138 baseui.setconfig(section, k, v)
139 139
140 140
141 141 def create_hg_wsgi_app(repo_path, repo_name, config):
142 142 """
143 143 Prepares a WSGI application to handle Mercurial requests.
144 144
145 145 :param config: is a list of 3-item tuples representing a ConfigObject
146 146 (it is the serialized version of the config object).
147 147 """
148 148 log.debug("Creating Mercurial WSGI application")
149 149
150 150 baseui = make_hg_ui_from_config(config)
151 151 update_hg_ui_from_hgrc(baseui, repo_path)
152 152
153 153 try:
154 154 return HgWeb(repo_path, name=repo_name, baseui=baseui)
155 155 except mercurial.error.RequirementError as e:
156 156 raise exceptions.RequirementException(e)(e)
157 157
158 158
159 159 class GitHandler(object):
160 160 """
161 161 Handler for Git operations like push/pull etc
162 162 """
163 163 def __init__(self, repo_location, repo_name, git_path, update_server_info,
164 164 extras):
165 165 if not os.path.isdir(repo_location):
166 166 raise OSError(repo_location)
167 167 self.content_path = repo_location
168 168 self.repo_name = repo_name
169 169 self.repo_location = repo_location
170 170 self.extras = extras
171 171 self.git_path = git_path
172 172 self.update_server_info = update_server_info
173 173
174 174 def __call__(self, environ, start_response):
175 175 app = webob.exc.HTTPNotFound()
176 176 candidate_paths = (
177 177 self.content_path, os.path.join(self.content_path, '.git'))
178 178
179 179 for content_path in candidate_paths:
180 180 try:
181 181 app = pygrack.GitRepository(
182 182 self.repo_name, content_path, self.git_path,
183 183 self.update_server_info, self.extras)
184 184 break
185 185 except OSError:
186 186 continue
187 187
188 188 return app(environ, start_response)
189 189
190 190
191 191 def create_git_wsgi_app(repo_path, repo_name, config):
192 192 """
193 193 Creates a WSGI application to handle Git requests.
194 194
195 195 :param config: is a dictionary holding the extras.
196 196 """
197 197 git_path = settings.GIT_EXECUTABLE
198 198 update_server_info = config.pop('git_update_server_info')
199 199 app = GitHandler(
200 200 repo_path, repo_name, git_path, update_server_info, config)
201 201
202 202 return app
203 203
204 204
205 205 class GitLFSHandler(object):
206 206 """
207 207 Handler for Git LFS operations
208 208 """
209 209
210 210 def __init__(self, repo_location, repo_name, git_path, update_server_info,
211 211 extras):
212 212 if not os.path.isdir(repo_location):
213 213 raise OSError(repo_location)
214 214 self.content_path = repo_location
215 215 self.repo_name = repo_name
216 216 self.repo_location = repo_location
217 217 self.extras = extras
218 218 self.git_path = git_path
219 219 self.update_server_info = update_server_info
220 220
221 221 def get_app(self, git_lfs_enabled, git_lfs_store_path):
222 222 app = git_lfs.create_app(git_lfs_enabled, git_lfs_store_path)
223 223 return app
224 224
225 225
226 226 def create_git_lfs_wsgi_app(repo_path, repo_name, config):
227 227 git_path = settings.GIT_EXECUTABLE
228 228 update_server_info = config.pop('git_update_server_info')
229 229 git_lfs_enabled = config.pop('git_lfs_enabled')
230 230 git_lfs_store_path = config.pop('git_lfs_store_path')
231 231 app = GitLFSHandler(
232 232 repo_path, repo_name, git_path, update_server_info, config)
233 233
234 234 return app.get_app(git_lfs_enabled, git_lfs_store_path)
@@ -1,78 +1,78 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import gc
19 19 import logging
20 20 import os
21 21 import time
22 22
23 23
24 24 log = logging.getLogger(__name__)
25 25
26 26
27 27 class VcsServer(object):
28 28 """
29 29 Exposed remote interface of the vcsserver itself.
30 30
31 31 This object can be used to manage the server remotely. Right now the main
32 32 use case is to allow to shut down the server.
33 33 """
34 34
35 35 _shutdown = False
36 36
37 37 def shutdown(self):
38 38 self._shutdown = True
39 39
40 40 def ping(self):
41 41 """
42 42 Utility to probe a server connection.
43 43 """
44 44 log.debug("Received server ping.")
45 45
46 46 def echo(self, data):
47 47 """
48 48 Utility for performance testing.
49 49
50 50 Allows to pass in arbitrary data and will return this data.
51 51 """
52 52 log.debug("Received server echo.")
53 53 return data
54 54
55 55 def sleep(self, seconds):
56 56 """
57 57 Utility to simulate long running server interaction.
58 58 """
59 59 log.debug("Sleeping %s seconds", seconds)
60 60 time.sleep(seconds)
61 61
62 62 def get_pid(self):
63 63 """
64 64 Allows to discover the PID based on a proxy object.
65 65 """
66 66 return os.getpid()
67 67
68 68 def run_gc(self):
69 69 """
70 70 Allows to trigger the garbage collector.
71 71
72 72 Main intention is to support statistics gathering during test runs.
73 73 """
74 74 freed_objects = gc.collect()
75 75 return {
76 76 'freed_objects': freed_objects,
77 77 'garbage': len(gc.garbage),
78 78 }
@@ -1,20 +1,20 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 WIRE_ENCODING = 'UTF-8'
19 19 GIT_EXECUTABLE = 'git'
20 20 BINARY_DIR = ''
@@ -1,722 +1,722 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 from __future__ import absolute_import
19 19
20 20 import os
21 21 import subprocess
22 22 from urllib2 import URLError
23 23 import urlparse
24 24 import logging
25 25 import posixpath as vcspath
26 26 import StringIO
27 27 import urllib
28 28 import traceback
29 29
30 30 import svn.client
31 31 import svn.core
32 32 import svn.delta
33 33 import svn.diff
34 34 import svn.fs
35 35 import svn.repos
36 36
37 37 from vcsserver import svn_diff, exceptions, subprocessio, settings
38 38 from vcsserver.base import RepoFactory, raise_from_original
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 # Set of svn compatible version flags.
44 44 # Compare with subversion/svnadmin/svnadmin.c
45 45 svn_compatible_versions = {
46 46 'pre-1.4-compatible',
47 47 'pre-1.5-compatible',
48 48 'pre-1.6-compatible',
49 49 'pre-1.8-compatible',
50 50 'pre-1.9-compatible'
51 51 }
52 52
53 53 svn_compatible_versions_map = {
54 54 'pre-1.4-compatible': '1.3',
55 55 'pre-1.5-compatible': '1.4',
56 56 'pre-1.6-compatible': '1.5',
57 57 'pre-1.8-compatible': '1.7',
58 58 'pre-1.9-compatible': '1.8',
59 59 }
60 60
61 61
62 62 def reraise_safe_exceptions(func):
63 63 """Decorator for converting svn exceptions to something neutral."""
64 64 def wrapper(*args, **kwargs):
65 65 try:
66 66 return func(*args, **kwargs)
67 67 except Exception as e:
68 68 if not hasattr(e, '_vcs_kind'):
69 69 log.exception("Unhandled exception in svn remote call")
70 70 raise_from_original(exceptions.UnhandledException(e))
71 71 raise
72 72 return wrapper
73 73
74 74
75 75 class SubversionFactory(RepoFactory):
76 76 repo_type = 'svn'
77 77
78 78 def _create_repo(self, wire, create, compatible_version):
79 79 path = svn.core.svn_path_canonicalize(wire['path'])
80 80 if create:
81 81 fs_config = {'compatible-version': '1.9'}
82 82 if compatible_version:
83 83 if compatible_version not in svn_compatible_versions:
84 84 raise Exception('Unknown SVN compatible version "{}"'
85 85 .format(compatible_version))
86 86 fs_config['compatible-version'] = \
87 87 svn_compatible_versions_map[compatible_version]
88 88
89 89 log.debug('Create SVN repo with config "%s"', fs_config)
90 90 repo = svn.repos.create(path, "", "", None, fs_config)
91 91 else:
92 92 repo = svn.repos.open(path)
93 93
94 94 log.debug('Got SVN object: %s', repo)
95 95 return repo
96 96
97 97 def repo(self, wire, create=False, compatible_version=None):
98 98 """
99 99 Get a repository instance for the given path.
100 100
101 101 Uses internally the low level beaker API since the decorators introduce
102 102 significant overhead.
103 103 """
104 104 region = self._cache_region
105 105 context = wire.get('context', None)
106 106 repo_path = wire.get('path', '')
107 107 context_uid = '{}'.format(context)
108 108 cache = wire.get('cache', True)
109 109 cache_on = context and cache
110 110
111 111 @region.conditional_cache_on_arguments(condition=cache_on)
112 112 def create_new_repo(_repo_type, _repo_path, _context_uid, compatible_version_id):
113 113 return self._create_repo(wire, create, compatible_version)
114 114
115 115 return create_new_repo(self.repo_type, repo_path, context_uid,
116 116 compatible_version)
117 117
118 118
119 119 NODE_TYPE_MAPPING = {
120 120 svn.core.svn_node_file: 'file',
121 121 svn.core.svn_node_dir: 'dir',
122 122 }
123 123
124 124
125 125 class SvnRemote(object):
126 126
127 127 def __init__(self, factory, hg_factory=None):
128 128 self._factory = factory
129 129 # TODO: Remove once we do not use internal Mercurial objects anymore
130 130 # for subversion
131 131 self._hg_factory = hg_factory
132 132
133 133 @reraise_safe_exceptions
134 134 def discover_svn_version(self):
135 135 try:
136 136 import svn.core
137 137 svn_ver = svn.core.SVN_VERSION
138 138 except ImportError:
139 139 svn_ver = None
140 140 return svn_ver
141 141
142 142 def check_url(self, url, config_items):
143 143 # this can throw exception if not installed, but we detect this
144 144 from hgsubversion import svnrepo
145 145
146 146 baseui = self._hg_factory._create_config(config_items)
147 147 # uuid function get's only valid UUID from proper repo, else
148 148 # throws exception
149 149 try:
150 150 svnrepo.svnremoterepo(baseui, url).svn.uuid
151 151 except Exception:
152 152 tb = traceback.format_exc()
153 153 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
154 154 raise URLError(
155 155 '"%s" is not a valid Subversion source url.' % (url, ))
156 156 return True
157 157
158 158 def is_path_valid_repository(self, wire, path):
159 159
160 160 # NOTE(marcink): short circuit the check for SVN repo
161 161 # the repos.open might be expensive to check, but we have one cheap
162 162 # pre condition that we can use, to check for 'format' file
163 163
164 164 if not os.path.isfile(os.path.join(path, 'format')):
165 165 return False
166 166
167 167 try:
168 168 svn.repos.open(path)
169 169 except svn.core.SubversionException:
170 170 tb = traceback.format_exc()
171 171 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
172 172 return False
173 173 return True
174 174
175 175 @reraise_safe_exceptions
176 176 def verify(self, wire,):
177 177 repo_path = wire['path']
178 178 if not self.is_path_valid_repository(wire, repo_path):
179 179 raise Exception(
180 180 "Path %s is not a valid Subversion repository." % repo_path)
181 181
182 182 cmd = ['svnadmin', 'info', repo_path]
183 183 stdout, stderr = subprocessio.run_command(cmd)
184 184 return stdout
185 185
186 186 def lookup(self, wire, revision):
187 187 if revision not in [-1, None, 'HEAD']:
188 188 raise NotImplementedError
189 189 repo = self._factory.repo(wire)
190 190 fs_ptr = svn.repos.fs(repo)
191 191 head = svn.fs.youngest_rev(fs_ptr)
192 192 return head
193 193
194 194 def lookup_interval(self, wire, start_ts, end_ts):
195 195 repo = self._factory.repo(wire)
196 196 fsobj = svn.repos.fs(repo)
197 197 start_rev = None
198 198 end_rev = None
199 199 if start_ts:
200 200 start_ts_svn = apr_time_t(start_ts)
201 201 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
202 202 else:
203 203 start_rev = 1
204 204 if end_ts:
205 205 end_ts_svn = apr_time_t(end_ts)
206 206 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
207 207 else:
208 208 end_rev = svn.fs.youngest_rev(fsobj)
209 209 return start_rev, end_rev
210 210
211 211 def revision_properties(self, wire, revision):
212 212 repo = self._factory.repo(wire)
213 213 fs_ptr = svn.repos.fs(repo)
214 214 return svn.fs.revision_proplist(fs_ptr, revision)
215 215
216 216 def revision_changes(self, wire, revision):
217 217
218 218 repo = self._factory.repo(wire)
219 219 fsobj = svn.repos.fs(repo)
220 220 rev_root = svn.fs.revision_root(fsobj, revision)
221 221
222 222 editor = svn.repos.ChangeCollector(fsobj, rev_root)
223 223 editor_ptr, editor_baton = svn.delta.make_editor(editor)
224 224 base_dir = ""
225 225 send_deltas = False
226 226 svn.repos.replay2(
227 227 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
228 228 editor_ptr, editor_baton, None)
229 229
230 230 added = []
231 231 changed = []
232 232 removed = []
233 233
234 234 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
235 235 for path, change in editor.changes.iteritems():
236 236 # TODO: Decide what to do with directory nodes. Subversion can add
237 237 # empty directories.
238 238
239 239 if change.item_kind == svn.core.svn_node_dir:
240 240 continue
241 241 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
242 242 added.append(path)
243 243 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
244 244 svn.repos.CHANGE_ACTION_REPLACE]:
245 245 changed.append(path)
246 246 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
247 247 removed.append(path)
248 248 else:
249 249 raise NotImplementedError(
250 250 "Action %s not supported on path %s" % (
251 251 change.action, path))
252 252
253 253 changes = {
254 254 'added': added,
255 255 'changed': changed,
256 256 'removed': removed,
257 257 }
258 258 return changes
259 259
260 260 def node_history(self, wire, path, revision, limit):
261 261 cross_copies = False
262 262 repo = self._factory.repo(wire)
263 263 fsobj = svn.repos.fs(repo)
264 264 rev_root = svn.fs.revision_root(fsobj, revision)
265 265
266 266 history_revisions = []
267 267 history = svn.fs.node_history(rev_root, path)
268 268 history = svn.fs.history_prev(history, cross_copies)
269 269 while history:
270 270 __, node_revision = svn.fs.history_location(history)
271 271 history_revisions.append(node_revision)
272 272 if limit and len(history_revisions) >= limit:
273 273 break
274 274 history = svn.fs.history_prev(history, cross_copies)
275 275 return history_revisions
276 276
277 277 def node_properties(self, wire, path, revision):
278 278 repo = self._factory.repo(wire)
279 279 fsobj = svn.repos.fs(repo)
280 280 rev_root = svn.fs.revision_root(fsobj, revision)
281 281 return svn.fs.node_proplist(rev_root, path)
282 282
283 283 def file_annotate(self, wire, path, revision):
284 284 abs_path = 'file://' + urllib.pathname2url(
285 285 vcspath.join(wire['path'], path))
286 286 file_uri = svn.core.svn_path_canonicalize(abs_path)
287 287
288 288 start_rev = svn_opt_revision_value_t(0)
289 289 peg_rev = svn_opt_revision_value_t(revision)
290 290 end_rev = peg_rev
291 291
292 292 annotations = []
293 293
294 294 def receiver(line_no, revision, author, date, line, pool):
295 295 annotations.append((line_no, revision, line))
296 296
297 297 # TODO: Cannot use blame5, missing typemap function in the swig code
298 298 try:
299 299 svn.client.blame2(
300 300 file_uri, peg_rev, start_rev, end_rev,
301 301 receiver, svn.client.create_context())
302 302 except svn.core.SubversionException as exc:
303 303 log.exception("Error during blame operation.")
304 304 raise Exception(
305 305 "Blame not supported or file does not exist at path %s. "
306 306 "Error %s." % (path, exc))
307 307
308 308 return annotations
309 309
310 310 def get_node_type(self, wire, path, rev=None):
311 311 repo = self._factory.repo(wire)
312 312 fs_ptr = svn.repos.fs(repo)
313 313 if rev is None:
314 314 rev = svn.fs.youngest_rev(fs_ptr)
315 315 root = svn.fs.revision_root(fs_ptr, rev)
316 316 node = svn.fs.check_path(root, path)
317 317 return NODE_TYPE_MAPPING.get(node, None)
318 318
319 319 def get_nodes(self, wire, path, revision=None):
320 320 repo = self._factory.repo(wire)
321 321 fsobj = svn.repos.fs(repo)
322 322 if revision is None:
323 323 revision = svn.fs.youngest_rev(fsobj)
324 324 root = svn.fs.revision_root(fsobj, revision)
325 325 entries = svn.fs.dir_entries(root, path)
326 326 result = []
327 327 for entry_path, entry_info in entries.iteritems():
328 328 result.append(
329 329 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
330 330 return result
331 331
332 332 def get_file_content(self, wire, path, rev=None):
333 333 repo = self._factory.repo(wire)
334 334 fsobj = svn.repos.fs(repo)
335 335 if rev is None:
336 336 rev = svn.fs.youngest_revision(fsobj)
337 337 root = svn.fs.revision_root(fsobj, rev)
338 338 content = svn.core.Stream(svn.fs.file_contents(root, path))
339 339 return content.read()
340 340
341 341 def get_file_size(self, wire, path, revision=None):
342 342 repo = self._factory.repo(wire)
343 343 fsobj = svn.repos.fs(repo)
344 344 if revision is None:
345 345 revision = svn.fs.youngest_revision(fsobj)
346 346 root = svn.fs.revision_root(fsobj, revision)
347 347 size = svn.fs.file_length(root, path)
348 348 return size
349 349
350 350 def create_repository(self, wire, compatible_version=None):
351 351 log.info('Creating Subversion repository in path "%s"', wire['path'])
352 352 self._factory.repo(wire, create=True,
353 353 compatible_version=compatible_version)
354 354
355 355 def get_url_and_credentials(self, src_url):
356 356 obj = urlparse.urlparse(src_url)
357 357 username = obj.username or None
358 358 password = obj.password or None
359 359 return username, password, src_url
360 360
361 361 def import_remote_repository(self, wire, src_url):
362 362 repo_path = wire['path']
363 363 if not self.is_path_valid_repository(wire, repo_path):
364 364 raise Exception(
365 365 "Path %s is not a valid Subversion repository." % repo_path)
366 366
367 367 username, password, src_url = self.get_url_and_credentials(src_url)
368 368 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
369 369 '--trust-server-cert-failures=unknown-ca']
370 370 if username and password:
371 371 rdump_cmd += ['--username', username, '--password', password]
372 372 rdump_cmd += [src_url]
373 373
374 374 rdump = subprocess.Popen(
375 375 rdump_cmd,
376 376 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
377 377 load = subprocess.Popen(
378 378 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
379 379
380 380 # TODO: johbo: This can be a very long operation, might be better
381 381 # to track some kind of status and provide an api to check if the
382 382 # import is done.
383 383 rdump.wait()
384 384 load.wait()
385 385
386 386 log.debug('Return process ended with code: %s', rdump.returncode)
387 387 if rdump.returncode != 0:
388 388 errors = rdump.stderr.read()
389 389 log.error('svnrdump dump failed: statuscode %s: message: %s',
390 390 rdump.returncode, errors)
391 391 reason = 'UNKNOWN'
392 392 if 'svnrdump: E230001:' in errors:
393 393 reason = 'INVALID_CERTIFICATE'
394 394
395 395 if reason == 'UNKNOWN':
396 396 reason = 'UNKNOWN:{}'.format(errors)
397 397 raise Exception(
398 398 'Failed to dump the remote repository from %s. Reason:%s' % (
399 399 src_url, reason))
400 400 if load.returncode != 0:
401 401 raise Exception(
402 402 'Failed to load the dump of remote repository from %s.' %
403 403 (src_url, ))
404 404
405 405 def commit(self, wire, message, author, timestamp, updated, removed):
406 406 assert isinstance(message, str)
407 407 assert isinstance(author, str)
408 408
409 409 repo = self._factory.repo(wire)
410 410 fsobj = svn.repos.fs(repo)
411 411
412 412 rev = svn.fs.youngest_rev(fsobj)
413 413 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
414 414 txn_root = svn.fs.txn_root(txn)
415 415
416 416 for node in updated:
417 417 TxnNodeProcessor(node, txn_root).update()
418 418 for node in removed:
419 419 TxnNodeProcessor(node, txn_root).remove()
420 420
421 421 commit_id = svn.repos.fs_commit_txn(repo, txn)
422 422
423 423 if timestamp:
424 424 apr_time = apr_time_t(timestamp)
425 425 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
426 426 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
427 427
428 428 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
429 429 return commit_id
430 430
431 431 def diff(self, wire, rev1, rev2, path1=None, path2=None,
432 432 ignore_whitespace=False, context=3):
433 433
434 434 wire.update(cache=False)
435 435 repo = self._factory.repo(wire)
436 436 diff_creator = SvnDiffer(
437 437 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
438 438 try:
439 439 return diff_creator.generate_diff()
440 440 except svn.core.SubversionException as e:
441 441 log.exception(
442 442 "Error during diff operation operation. "
443 443 "Path might not exist %s, %s" % (path1, path2))
444 444 return ""
445 445
446 446 @reraise_safe_exceptions
447 447 def is_large_file(self, wire, path):
448 448 return False
449 449
450 450 @reraise_safe_exceptions
451 451 def install_hooks(self, wire, force=False):
452 452 from vcsserver.hook_utils import install_svn_hooks
453 453 repo_path = wire['path']
454 454 binary_dir = settings.BINARY_DIR
455 455 executable = None
456 456 if binary_dir:
457 457 executable = os.path.join(binary_dir, 'python')
458 458 return install_svn_hooks(
459 459 repo_path, executable=executable, force_create=force)
460 460
461 461
462 462 class SvnDiffer(object):
463 463 """
464 464 Utility to create diffs based on difflib and the Subversion api
465 465 """
466 466
467 467 binary_content = False
468 468
469 469 def __init__(
470 470 self, repo, src_rev, src_path, tgt_rev, tgt_path,
471 471 ignore_whitespace, context):
472 472 self.repo = repo
473 473 self.ignore_whitespace = ignore_whitespace
474 474 self.context = context
475 475
476 476 fsobj = svn.repos.fs(repo)
477 477
478 478 self.tgt_rev = tgt_rev
479 479 self.tgt_path = tgt_path or ''
480 480 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
481 481 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
482 482
483 483 self.src_rev = src_rev
484 484 self.src_path = src_path or self.tgt_path
485 485 self.src_root = svn.fs.revision_root(fsobj, src_rev)
486 486 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
487 487
488 488 self._validate()
489 489
490 490 def _validate(self):
491 491 if (self.tgt_kind != svn.core.svn_node_none and
492 492 self.src_kind != svn.core.svn_node_none and
493 493 self.src_kind != self.tgt_kind):
494 494 # TODO: johbo: proper error handling
495 495 raise Exception(
496 496 "Source and target are not compatible for diff generation. "
497 497 "Source type: %s, target type: %s" %
498 498 (self.src_kind, self.tgt_kind))
499 499
500 500 def generate_diff(self):
501 501 buf = StringIO.StringIO()
502 502 if self.tgt_kind == svn.core.svn_node_dir:
503 503 self._generate_dir_diff(buf)
504 504 else:
505 505 self._generate_file_diff(buf)
506 506 return buf.getvalue()
507 507
508 508 def _generate_dir_diff(self, buf):
509 509 editor = DiffChangeEditor()
510 510 editor_ptr, editor_baton = svn.delta.make_editor(editor)
511 511 svn.repos.dir_delta2(
512 512 self.src_root,
513 513 self.src_path,
514 514 '', # src_entry
515 515 self.tgt_root,
516 516 self.tgt_path,
517 517 editor_ptr, editor_baton,
518 518 authorization_callback_allow_all,
519 519 False, # text_deltas
520 520 svn.core.svn_depth_infinity, # depth
521 521 False, # entry_props
522 522 False, # ignore_ancestry
523 523 )
524 524
525 525 for path, __, change in sorted(editor.changes):
526 526 self._generate_node_diff(
527 527 buf, change, path, self.tgt_path, path, self.src_path)
528 528
529 529 def _generate_file_diff(self, buf):
530 530 change = None
531 531 if self.src_kind == svn.core.svn_node_none:
532 532 change = "add"
533 533 elif self.tgt_kind == svn.core.svn_node_none:
534 534 change = "delete"
535 535 tgt_base, tgt_path = vcspath.split(self.tgt_path)
536 536 src_base, src_path = vcspath.split(self.src_path)
537 537 self._generate_node_diff(
538 538 buf, change, tgt_path, tgt_base, src_path, src_base)
539 539
540 540 def _generate_node_diff(
541 541 self, buf, change, tgt_path, tgt_base, src_path, src_base):
542 542
543 543 if self.src_rev == self.tgt_rev and tgt_base == src_base:
544 544 # makes consistent behaviour with git/hg to return empty diff if
545 545 # we compare same revisions
546 546 return
547 547
548 548 tgt_full_path = vcspath.join(tgt_base, tgt_path)
549 549 src_full_path = vcspath.join(src_base, src_path)
550 550
551 551 self.binary_content = False
552 552 mime_type = self._get_mime_type(tgt_full_path)
553 553
554 554 if mime_type and not mime_type.startswith('text'):
555 555 self.binary_content = True
556 556 buf.write("=" * 67 + '\n')
557 557 buf.write("Cannot display: file marked as a binary type.\n")
558 558 buf.write("svn:mime-type = %s\n" % mime_type)
559 559 buf.write("Index: %s\n" % (tgt_path, ))
560 560 buf.write("=" * 67 + '\n')
561 561 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
562 562 'tgt_path': tgt_path})
563 563
564 564 if change == 'add':
565 565 # TODO: johbo: SVN is missing a zero here compared to git
566 566 buf.write("new file mode 10644\n")
567 567
568 568 #TODO(marcink): intro to binary detection of svn patches
569 569 # if self.binary_content:
570 570 # buf.write('GIT binary patch\n')
571 571
572 572 buf.write("--- /dev/null\t(revision 0)\n")
573 573 src_lines = []
574 574 else:
575 575 if change == 'delete':
576 576 buf.write("deleted file mode 10644\n")
577 577
578 578 #TODO(marcink): intro to binary detection of svn patches
579 579 # if self.binary_content:
580 580 # buf.write('GIT binary patch\n')
581 581
582 582 buf.write("--- a/%s\t(revision %s)\n" % (
583 583 src_path, self.src_rev))
584 584 src_lines = self._svn_readlines(self.src_root, src_full_path)
585 585
586 586 if change == 'delete':
587 587 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
588 588 tgt_lines = []
589 589 else:
590 590 buf.write("+++ b/%s\t(revision %s)\n" % (
591 591 tgt_path, self.tgt_rev))
592 592 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
593 593
594 594 if not self.binary_content:
595 595 udiff = svn_diff.unified_diff(
596 596 src_lines, tgt_lines, context=self.context,
597 597 ignore_blank_lines=self.ignore_whitespace,
598 598 ignore_case=False,
599 599 ignore_space_changes=self.ignore_whitespace)
600 600 buf.writelines(udiff)
601 601
602 602 def _get_mime_type(self, path):
603 603 try:
604 604 mime_type = svn.fs.node_prop(
605 605 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
606 606 except svn.core.SubversionException:
607 607 mime_type = svn.fs.node_prop(
608 608 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
609 609 return mime_type
610 610
611 611 def _svn_readlines(self, fs_root, node_path):
612 612 if self.binary_content:
613 613 return []
614 614 node_kind = svn.fs.check_path(fs_root, node_path)
615 615 if node_kind not in (
616 616 svn.core.svn_node_file, svn.core.svn_node_symlink):
617 617 return []
618 618 content = svn.core.Stream(
619 619 svn.fs.file_contents(fs_root, node_path)).read()
620 620 return content.splitlines(True)
621 621
622 622
623 623
624 624 class DiffChangeEditor(svn.delta.Editor):
625 625 """
626 626 Records changes between two given revisions
627 627 """
628 628
629 629 def __init__(self):
630 630 self.changes = []
631 631
632 632 def delete_entry(self, path, revision, parent_baton, pool=None):
633 633 self.changes.append((path, None, 'delete'))
634 634
635 635 def add_file(
636 636 self, path, parent_baton, copyfrom_path, copyfrom_revision,
637 637 file_pool=None):
638 638 self.changes.append((path, 'file', 'add'))
639 639
640 640 def open_file(self, path, parent_baton, base_revision, file_pool=None):
641 641 self.changes.append((path, 'file', 'change'))
642 642
643 643
644 644 def authorization_callback_allow_all(root, path, pool):
645 645 return True
646 646
647 647
648 648 class TxnNodeProcessor(object):
649 649 """
650 650 Utility to process the change of one node within a transaction root.
651 651
652 652 It encapsulates the knowledge of how to add, update or remove
653 653 a node for a given transaction root. The purpose is to support the method
654 654 `SvnRemote.commit`.
655 655 """
656 656
657 657 def __init__(self, node, txn_root):
658 658 assert isinstance(node['path'], str)
659 659
660 660 self.node = node
661 661 self.txn_root = txn_root
662 662
663 663 def update(self):
664 664 self._ensure_parent_dirs()
665 665 self._add_file_if_node_does_not_exist()
666 666 self._update_file_content()
667 667 self._update_file_properties()
668 668
669 669 def remove(self):
670 670 svn.fs.delete(self.txn_root, self.node['path'])
671 671 # TODO: Clean up directory if empty
672 672
673 673 def _ensure_parent_dirs(self):
674 674 curdir = vcspath.dirname(self.node['path'])
675 675 dirs_to_create = []
676 676 while not self._svn_path_exists(curdir):
677 677 dirs_to_create.append(curdir)
678 678 curdir = vcspath.dirname(curdir)
679 679
680 680 for curdir in reversed(dirs_to_create):
681 681 log.debug('Creating missing directory "%s"', curdir)
682 682 svn.fs.make_dir(self.txn_root, curdir)
683 683
684 684 def _svn_path_exists(self, path):
685 685 path_status = svn.fs.check_path(self.txn_root, path)
686 686 return path_status != svn.core.svn_node_none
687 687
688 688 def _add_file_if_node_does_not_exist(self):
689 689 kind = svn.fs.check_path(self.txn_root, self.node['path'])
690 690 if kind == svn.core.svn_node_none:
691 691 svn.fs.make_file(self.txn_root, self.node['path'])
692 692
693 693 def _update_file_content(self):
694 694 assert isinstance(self.node['content'], str)
695 695 handler, baton = svn.fs.apply_textdelta(
696 696 self.txn_root, self.node['path'], None, None)
697 697 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
698 698
699 699 def _update_file_properties(self):
700 700 properties = self.node.get('properties', {})
701 701 for key, value in properties.iteritems():
702 702 svn.fs.change_node_prop(
703 703 self.txn_root, self.node['path'], key, value)
704 704
705 705
706 706 def apr_time_t(timestamp):
707 707 """
708 708 Convert a Python timestamp into APR timestamp type apr_time_t
709 709 """
710 710 return timestamp * 1E6
711 711
712 712
713 713 def svn_opt_revision_value_t(num):
714 714 """
715 715 Put `num` into a `svn_opt_revision_value_t` structure.
716 716 """
717 717 value = svn.core.svn_opt_revision_value_t()
718 718 value.number = num
719 719 revision = svn.core.svn_opt_revision_t()
720 720 revision.kind = svn.core.svn_opt_revision_number
721 721 revision.value = value
722 722 return revision
@@ -1,16 +1,16 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
@@ -1,57 +1,57 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import socket
19 19
20 20 import pytest
21 21
22 22
23 23 def pytest_addoption(parser):
24 24 parser.addoption(
25 25 '--repeat', type=int, default=100,
26 26 help="Number of repetitions in performance tests.")
27 27
28 28
29 29 @pytest.fixture(scope='session')
30 30 def repeat(request):
31 31 """
32 32 The number of repetitions is based on this fixture.
33 33
34 34 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
35 35 tests are not too slow in our default test suite.
36 36 """
37 37 return request.config.getoption('--repeat')
38 38
39 39
40 40 @pytest.fixture(scope='session')
41 41 def vcsserver_port(request):
42 42 port = get_available_port()
43 43 print('Using vcsserver port %s' % (port, ))
44 44 return port
45 45
46 46
47 47 def get_available_port():
48 48 family = socket.AF_INET
49 49 socktype = socket.SOCK_STREAM
50 50 host = '127.0.0.1'
51 51
52 52 mysocket = socket.socket(family, socktype)
53 53 mysocket.bind((host, 0))
54 54 port = mysocket.getsockname()[1]
55 55 mysocket.close()
56 56 del mysocket
57 57 return port
@@ -1,86 +1,86 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import shutil
20 20 import tempfile
21 21
22 22 import configobj
23 23
24 24
25 25 class ContextINI(object):
26 26 """
27 27 Allows to create a new test.ini file as a copy of existing one with edited
28 28 data. If existing file is not present, it creates a new one. Example usage::
29 29
30 30 with TestINI('test.ini', [{'section': {'key': 'val'}}]) as new_test_ini_path:
31 31 print 'vcsserver --config=%s' % new_test_ini
32 32 """
33 33
34 34 def __init__(self, ini_file_path, ini_params, new_file_prefix=None,
35 35 destroy=True):
36 36 self.ini_file_path = ini_file_path
37 37 self.ini_params = ini_params
38 38 self.new_path = None
39 39 self.new_path_prefix = new_file_prefix or 'test'
40 40 self.destroy = destroy
41 41
42 42 def __enter__(self):
43 43 _, pref = tempfile.mkstemp()
44 44 loc = tempfile.gettempdir()
45 45 self.new_path = os.path.join(loc, '{}_{}_{}'.format(
46 46 pref, self.new_path_prefix, self.ini_file_path))
47 47
48 48 # copy ini file and modify according to the params, if we re-use a file
49 49 if os.path.isfile(self.ini_file_path):
50 50 shutil.copy(self.ini_file_path, self.new_path)
51 51 else:
52 52 # create new dump file for configObj to write to.
53 53 with open(self.new_path, 'wb'):
54 54 pass
55 55
56 56 config = configobj.ConfigObj(
57 57 self.new_path, file_error=True, write_empty_values=True)
58 58
59 59 for data in self.ini_params:
60 60 section, ini_params = data.items()[0]
61 61 key, val = ini_params.items()[0]
62 62 if section not in config:
63 63 config[section] = {}
64 64 config[section][key] = val
65 65
66 66 config.write()
67 67 return self.new_path
68 68
69 69 def __exit__(self, exc_type, exc_val, exc_tb):
70 70 if self.destroy:
71 71 os.remove(self.new_path)
72 72
73 73
74 74 def no_newline_id_generator(test_name):
75 75 """
76 76 Generates a test name without spaces or newlines characters. Used for
77 77 nicer output of progress of test
78 78 """
79 79 org_name = test_name
80 80 test_name = str(test_name)\
81 81 .replace('\n', '_N') \
82 82 .replace('\r', '_N') \
83 83 .replace('\t', '_T') \
84 84 .replace(' ', '_S')
85 85
86 86 return test_name or 'test-with-empty-name'
@@ -1,165 +1,165 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import inspect
19 19
20 20 import pytest
21 21 import dulwich.errors
22 22 from mock import Mock, patch
23 23
24 24 from vcsserver import git
25 25
26 26
27 27 SAMPLE_REFS = {
28 28 'HEAD': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
29 29 'refs/tags/v0.1.9': '341d28f0eec5ddf0b6b77871e13c2bbd6bec685c',
30 30 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
31 31 'refs/tags/v0.1.1': 'e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0',
32 32 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
33 33 }
34 34
35 35
36 36 @pytest.fixture
37 37 def git_remote():
38 38 """
39 39 A GitRemote instance with a mock factory.
40 40 """
41 41 factory = Mock()
42 42 remote = git.GitRemote(factory)
43 43 return remote
44 44
45 45
46 46 def test_discover_git_version(git_remote):
47 47 version = git_remote.discover_git_version()
48 48 assert version
49 49
50 50
51 51 class TestGitFetch(object):
52 52 def setup(self):
53 53 self.mock_repo = Mock()
54 54 factory = Mock()
55 55 factory.repo = Mock(return_value=self.mock_repo)
56 56 self.remote_git = git.GitRemote(factory)
57 57
58 58 def test_fetches_all_when_no_commit_ids_specified(self):
59 59 def side_effect(determine_wants, *args, **kwargs):
60 60 determine_wants(SAMPLE_REFS)
61 61
62 62 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
63 63 mock_fetch.side_effect = side_effect
64 64 self.remote_git.pull(wire=None, url='/tmp/', apply_refs=False)
65 65 determine_wants = self.mock_repo.object_store.determine_wants_all
66 66 determine_wants.assert_called_once_with(SAMPLE_REFS)
67 67
68 68 def test_fetches_specified_commits(self):
69 69 selected_refs = {
70 70 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
71 71 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
72 72 }
73 73
74 74 def side_effect(determine_wants, *args, **kwargs):
75 75 result = determine_wants(SAMPLE_REFS)
76 76 assert sorted(result) == sorted(selected_refs.values())
77 77 return result
78 78
79 79 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
80 80 mock_fetch.side_effect = side_effect
81 81 self.remote_git.pull(
82 82 wire=None, url='/tmp/', apply_refs=False,
83 83 refs=selected_refs.keys())
84 84 determine_wants = self.mock_repo.object_store.determine_wants_all
85 85 assert determine_wants.call_count == 0
86 86
87 87 def test_get_remote_refs(self):
88 88 factory = Mock()
89 89 remote_git = git.GitRemote(factory)
90 90 url = 'http://example.com/test/test.git'
91 91 sample_refs = {
92 92 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
93 93 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
94 94 }
95 95
96 96 with patch('vcsserver.git.Repo', create=False) as mock_repo:
97 97 mock_repo().get_refs.return_value = sample_refs
98 98 remote_refs = remote_git.get_remote_refs(wire=None, url=url)
99 99 mock_repo().get_refs.assert_called_once_with()
100 100 assert remote_refs == sample_refs
101 101
102 102 def test_remove_ref(self):
103 103 ref_to_remove = 'refs/tags/v0.1.9'
104 104 self.mock_repo.refs = SAMPLE_REFS.copy()
105 105 self.remote_git.remove_ref(None, ref_to_remove)
106 106 assert ref_to_remove not in self.mock_repo.refs
107 107
108 108
109 109 class TestReraiseSafeExceptions(object):
110 110 def test_method_decorated_with_reraise_safe_exceptions(self):
111 111 factory = Mock()
112 112 git_remote = git.GitRemote(factory)
113 113
114 114 def fake_function():
115 115 return None
116 116
117 117 decorator = git.reraise_safe_exceptions(fake_function)
118 118
119 119 methods = inspect.getmembers(git_remote, predicate=inspect.ismethod)
120 120 for method_name, method in methods:
121 121 if not method_name.startswith('_'):
122 122 assert method.im_func.__code__ == decorator.__code__
123 123
124 124 @pytest.mark.parametrize('side_effect, expected_type', [
125 125 (dulwich.errors.ChecksumMismatch('0000000', 'deadbeef'), 'lookup'),
126 126 (dulwich.errors.NotCommitError('deadbeef'), 'lookup'),
127 127 (dulwich.errors.MissingCommitError('deadbeef'), 'lookup'),
128 128 (dulwich.errors.ObjectMissing('deadbeef'), 'lookup'),
129 129 (dulwich.errors.HangupException(), 'error'),
130 130 (dulwich.errors.UnexpectedCommandError('test-cmd'), 'error'),
131 131 ])
132 132 def test_safe_exceptions_reraised(self, side_effect, expected_type):
133 133 @git.reraise_safe_exceptions
134 134 def fake_method():
135 135 raise side_effect
136 136
137 137 with pytest.raises(Exception) as exc_info:
138 138 fake_method()
139 139 assert type(exc_info.value) == Exception
140 140 assert exc_info.value._vcs_kind == expected_type
141 141
142 142
143 143 class TestDulwichRepoWrapper(object):
144 144 def test_calls_close_on_delete(self):
145 145 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
146 146 with isdir_patcher:
147 147 repo = git.Repo('/tmp/abcde')
148 148 with patch.object(git.DulwichRepo, 'close') as close_mock:
149 149 del repo
150 150 close_mock.assert_called_once_with()
151 151
152 152
153 153 class TestGitFactory(object):
154 154 def test_create_repo_returns_dulwich_wrapper(self):
155 155
156 156 with patch('vcsserver.lib.rc_cache.region_meta.dogpile_cache_regions') as mock:
157 157 mock.side_effect = {'repo_objects': ''}
158 158 factory = git.GitFactory()
159 159 wire = {
160 160 'path': '/tmp/abcde'
161 161 }
162 162 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
163 163 with isdir_patcher:
164 164 result = factory._create_repo(wire, True)
165 165 assert isinstance(result, git.Repo)
@@ -1,127 +1,127 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import inspect
19 19 import sys
20 20 import traceback
21 21
22 22 import pytest
23 23 from mercurial.error import LookupError
24 24 from mock import Mock, MagicMock, patch
25 25
26 26 from vcsserver import exceptions, hg, hgcompat
27 27
28 28
29 29 class TestHGLookup(object):
30 30 def setup(self):
31 31 self.mock_repo = MagicMock()
32 32 self.mock_repo.__getitem__.side_effect = LookupError(
33 33 'revision_or_commit_id', 'index', 'message')
34 34 factory = Mock()
35 35 factory.repo = Mock(return_value=self.mock_repo)
36 36 self.remote_hg = hg.HgRemote(factory)
37 37
38 38 def test_fail_lookup_hg(self):
39 39 with pytest.raises(Exception) as exc_info:
40 40 self.remote_hg.lookup(
41 41 wire=None, revision='revision_or_commit_id', both=True)
42 42
43 43 assert exc_info.value._vcs_kind == 'lookup'
44 44 assert 'revision_or_commit_id' in exc_info.value.args
45 45
46 46
47 47 class TestDiff(object):
48 48 def test_raising_safe_exception_when_lookup_failed(self):
49 49 repo = Mock()
50 50 factory = Mock()
51 51 factory.repo = Mock(return_value=repo)
52 52 hg_remote = hg.HgRemote(factory)
53 53 with patch('mercurial.patch.diff') as diff_mock:
54 54 diff_mock.side_effect = LookupError(
55 55 'deadbeef', 'index', 'message')
56 56 with pytest.raises(Exception) as exc_info:
57 57 hg_remote.diff(
58 58 wire=None, rev1='deadbeef', rev2='deadbee1',
59 59 file_filter=None, opt_git=True, opt_ignorews=True,
60 60 context=3)
61 61 assert type(exc_info.value) == Exception
62 62 assert exc_info.value._vcs_kind == 'lookup'
63 63
64 64
65 65 class TestReraiseSafeExceptions(object):
66 66 def test_method_decorated_with_reraise_safe_exceptions(self):
67 67 factory = Mock()
68 68 hg_remote = hg.HgRemote(factory)
69 69 methods = inspect.getmembers(hg_remote, predicate=inspect.ismethod)
70 70 decorator = hg.reraise_safe_exceptions(None)
71 71 for method_name, method in methods:
72 72 if not method_name.startswith('_'):
73 73 assert method.im_func.__code__ == decorator.__code__
74 74
75 75 @pytest.mark.parametrize('side_effect, expected_type', [
76 76 (hgcompat.Abort(), 'abort'),
77 77 (hgcompat.InterventionRequired(), 'abort'),
78 78 (hgcompat.RepoLookupError(), 'lookup'),
79 79 (hgcompat.LookupError('deadbeef', 'index', 'message'), 'lookup'),
80 80 (hgcompat.RepoError(), 'error'),
81 81 (hgcompat.RequirementError(), 'requirement'),
82 82 ])
83 83 def test_safe_exceptions_reraised(self, side_effect, expected_type):
84 84 @hg.reraise_safe_exceptions
85 85 def fake_method():
86 86 raise side_effect
87 87
88 88 with pytest.raises(Exception) as exc_info:
89 89 fake_method()
90 90 assert type(exc_info.value) == Exception
91 91 assert exc_info.value._vcs_kind == expected_type
92 92
93 93 def test_keeps_original_traceback(self):
94 94 @hg.reraise_safe_exceptions
95 95 def fake_method():
96 96 try:
97 97 raise hgcompat.Abort()
98 98 except:
99 99 self.original_traceback = traceback.format_tb(
100 100 sys.exc_info()[2])
101 101 raise
102 102
103 103 try:
104 104 fake_method()
105 105 except Exception:
106 106 new_traceback = traceback.format_tb(sys.exc_info()[2])
107 107
108 108 new_traceback_tail = new_traceback[-len(self.original_traceback):]
109 109 assert new_traceback_tail == self.original_traceback
110 110
111 111 def test_maps_unknow_exceptions_to_unhandled(self):
112 112 @hg.reraise_safe_exceptions
113 113 def stub_method():
114 114 raise ValueError('stub')
115 115
116 116 with pytest.raises(Exception) as exc_info:
117 117 stub_method()
118 118 assert exc_info.value._vcs_kind == 'unhandled'
119 119
120 120 def test_does_not_map_known_exceptions(self):
121 121 @hg.reraise_safe_exceptions
122 122 def stub_method():
123 123 raise exceptions.LookupException()('stub')
124 124
125 125 with pytest.raises(Exception) as exc_info:
126 126 stub_method()
127 127 assert exc_info.value._vcs_kind == 'lookup'
@@ -1,124 +1,124 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import mock
19 19 import pytest
20 20
21 21 from vcsserver import hgcompat, hgpatches
22 22
23 23
24 24 LARGEFILES_CAPABILITY = 'largefiles=serve'
25 25
26 26
27 27 def test_patch_largefiles_capabilities_applies_patch(
28 28 patched_capabilities):
29 29 lfproto = hgcompat.largefiles.proto
30 30 hgpatches.patch_largefiles_capabilities()
31 31 assert lfproto._capabilities.func_name == '_dynamic_capabilities'
32 32
33 33
34 34 def test_dynamic_capabilities_uses_original_function_if_not_enabled(
35 35 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
36 36 orig_capabilities):
37 37 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
38 38 hgcompat.largefiles.proto, stub_extensions)
39 39
40 40 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
41 41
42 42 stub_extensions.assert_called_once_with(stub_ui)
43 43 assert LARGEFILES_CAPABILITY not in caps
44 44
45 45
46 46 def test_dynamic_capabilities_ignores_updated_capabilities(
47 47 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
48 48 orig_capabilities):
49 49 stub_extensions.return_value = [('largefiles', mock.Mock())]
50 50 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
51 51 hgcompat.largefiles.proto, stub_extensions)
52 52
53 53 # This happens when the extension is loaded for the first time, important
54 54 # to ensure that an updated function is correctly picked up.
55 55 hgcompat.largefiles.proto._capabilities = mock.Mock(
56 56 side_effect=Exception('Must not be called'))
57 57
58 58 dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
59 59
60 60
61 61 def test_dynamic_capabilities_uses_largefiles_if_enabled(
62 62 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
63 63 orig_capabilities):
64 64 stub_extensions.return_value = [('largefiles', mock.Mock())]
65 65
66 66 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
67 67 hgcompat.largefiles.proto, stub_extensions)
68 68
69 69 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
70 70
71 71 stub_extensions.assert_called_once_with(stub_ui)
72 72 assert LARGEFILES_CAPABILITY in caps
73 73
74 74
75 75 def test_hgsubversion_import():
76 76 from hgsubversion import svnrepo
77 77 assert svnrepo
78 78
79 79
80 80 @pytest.fixture
81 81 def patched_capabilities(request):
82 82 """
83 83 Patch in `capabilitiesorig` and restore both capability functions.
84 84 """
85 85 lfproto = hgcompat.largefiles.proto
86 86 orig_capabilities = lfproto._capabilities
87 87
88 88 @request.addfinalizer
89 89 def restore():
90 90 lfproto._capabilities = orig_capabilities
91 91
92 92
93 93 @pytest.fixture
94 94 def stub_repo(stub_ui):
95 95 repo = mock.Mock()
96 96 repo.ui = stub_ui
97 97 return repo
98 98
99 99
100 100 @pytest.fixture
101 101 def stub_proto(stub_ui):
102 102 proto = mock.Mock()
103 103 proto.ui = stub_ui
104 104 return proto
105 105
106 106
107 107 @pytest.fixture
108 108 def orig_capabilities():
109 109 from mercurial.wireprotov1server import wireprotocaps
110 110
111 111 def _capabilities(repo, proto):
112 112 return wireprotocaps
113 113 return _capabilities
114 114
115 115
116 116 @pytest.fixture
117 117 def stub_ui():
118 118 return hgcompat.ui.ui()
119 119
120 120
121 121 @pytest.fixture
122 122 def stub_extensions():
123 123 extensions = mock.Mock(return_value=tuple())
124 124 return extensions
@@ -1,241 +1,241 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import contextlib
19 19 import io
20 20 import threading
21 21 from BaseHTTPServer import BaseHTTPRequestHandler
22 22 from SocketServer import TCPServer
23 23
24 24 import mercurial.ui
25 25 import mock
26 26 import pytest
27 27 import simplejson as json
28 28
29 29 from vcsserver import hooks
30 30
31 31
32 32 def get_hg_ui(extras=None):
33 33 """Create a Config object with a valid RC_SCM_DATA entry."""
34 34 extras = extras or {}
35 35 required_extras = {
36 36 'username': '',
37 37 'repository': '',
38 38 'locked_by': '',
39 39 'scm': '',
40 40 'make_lock': '',
41 41 'action': '',
42 42 'ip': '',
43 43 'hooks_uri': 'fake_hooks_uri',
44 44 }
45 45 required_extras.update(extras)
46 46 hg_ui = mercurial.ui.ui()
47 47 hg_ui.setconfig('rhodecode', 'RC_SCM_DATA', json.dumps(required_extras))
48 48
49 49 return hg_ui
50 50
51 51
52 52 def test_git_pre_receive_is_disabled():
53 53 extras = {'hooks': ['pull']}
54 54 response = hooks.git_pre_receive(None, None,
55 55 {'RC_SCM_DATA': json.dumps(extras)})
56 56
57 57 assert response == 0
58 58
59 59
60 60 def test_git_post_receive_is_disabled():
61 61 extras = {'hooks': ['pull']}
62 62 response = hooks.git_post_receive(None, '',
63 63 {'RC_SCM_DATA': json.dumps(extras)})
64 64
65 65 assert response == 0
66 66
67 67
68 68 def test_git_post_receive_calls_repo_size():
69 69 extras = {'hooks': ['push', 'repo_size']}
70 70 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
71 71 hooks.git_post_receive(
72 72 None, '', {'RC_SCM_DATA': json.dumps(extras)})
73 73 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
74 74 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
75 75 expected_calls = [
76 76 mock.call('repo_size', extras, mock.ANY),
77 77 mock.call('post_push', extras, mock.ANY),
78 78 ]
79 79 assert call_hook_mock.call_args_list == expected_calls
80 80
81 81
82 82 def test_git_post_receive_does_not_call_disabled_repo_size():
83 83 extras = {'hooks': ['push']}
84 84 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
85 85 hooks.git_post_receive(
86 86 None, '', {'RC_SCM_DATA': json.dumps(extras)})
87 87 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
88 88 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
89 89 expected_calls = [
90 90 mock.call('post_push', extras, mock.ANY)
91 91 ]
92 92 assert call_hook_mock.call_args_list == expected_calls
93 93
94 94
95 95 def test_repo_size_exception_does_not_affect_git_post_receive():
96 96 extras = {'hooks': ['push', 'repo_size']}
97 97 status = 0
98 98
99 99 def side_effect(name, *args, **kwargs):
100 100 if name == 'repo_size':
101 101 raise Exception('Fake exception')
102 102 else:
103 103 return status
104 104
105 105 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
106 106 call_hook_mock.side_effect = side_effect
107 107 result = hooks.git_post_receive(
108 108 None, '', {'RC_SCM_DATA': json.dumps(extras)})
109 109 assert result == status
110 110
111 111
112 112 def test_git_pre_pull_is_disabled():
113 113 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
114 114
115 115
116 116 def test_git_post_pull_is_disabled():
117 117 assert (
118 118 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
119 119
120 120
121 121 class TestGetHooksClient(object):
122 122
123 123 def test_returns_http_client_when_protocol_matches(self):
124 124 hooks_uri = 'localhost:8000'
125 125 result = hooks._get_hooks_client({
126 126 'hooks_uri': hooks_uri,
127 127 'hooks_protocol': 'http'
128 128 })
129 129 assert isinstance(result, hooks.HooksHttpClient)
130 130 assert result.hooks_uri == hooks_uri
131 131
132 132 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
133 133 fake_module = mock.Mock()
134 134 import_patcher = mock.patch.object(
135 135 hooks.importlib, 'import_module', return_value=fake_module)
136 136 fake_module_name = 'fake.module'
137 137 with import_patcher as import_mock:
138 138 result = hooks._get_hooks_client(
139 139 {'hooks_module': fake_module_name})
140 140
141 141 import_mock.assert_called_once_with(fake_module_name)
142 142 assert isinstance(result, hooks.HooksDummyClient)
143 143 assert result._hooks_module == fake_module
144 144
145 145
146 146 class TestHooksHttpClient(object):
147 147 def test_init_sets_hooks_uri(self):
148 148 uri = 'localhost:3000'
149 149 client = hooks.HooksHttpClient(uri)
150 150 assert client.hooks_uri == uri
151 151
152 152 def test_serialize_returns_json_string(self):
153 153 client = hooks.HooksHttpClient('localhost:3000')
154 154 hook_name = 'test'
155 155 extras = {
156 156 'first': 1,
157 157 'second': 'two'
158 158 }
159 159 result = client._serialize(hook_name, extras)
160 160 expected_result = json.dumps({
161 161 'method': hook_name,
162 162 'extras': extras
163 163 })
164 164 assert result == expected_result
165 165
166 166 def test_call_queries_http_server(self, http_mirror):
167 167 client = hooks.HooksHttpClient(http_mirror.uri)
168 168 hook_name = 'test'
169 169 extras = {
170 170 'first': 1,
171 171 'second': 'two'
172 172 }
173 173 result = client(hook_name, extras)
174 174 expected_result = {
175 175 'method': hook_name,
176 176 'extras': extras
177 177 }
178 178 assert result == expected_result
179 179
180 180
181 181 class TestHooksDummyClient(object):
182 182 def test_init_imports_hooks_module(self):
183 183 hooks_module_name = 'rhodecode.fake.module'
184 184 hooks_module = mock.MagicMock()
185 185
186 186 import_patcher = mock.patch.object(
187 187 hooks.importlib, 'import_module', return_value=hooks_module)
188 188 with import_patcher as import_mock:
189 189 client = hooks.HooksDummyClient(hooks_module_name)
190 190 import_mock.assert_called_once_with(hooks_module_name)
191 191 assert client._hooks_module == hooks_module
192 192
193 193 def test_call_returns_hook_result(self):
194 194 hooks_module_name = 'rhodecode.fake.module'
195 195 hooks_module = mock.MagicMock()
196 196 import_patcher = mock.patch.object(
197 197 hooks.importlib, 'import_module', return_value=hooks_module)
198 198 with import_patcher:
199 199 client = hooks.HooksDummyClient(hooks_module_name)
200 200
201 201 result = client('post_push', {})
202 202 hooks_module.Hooks.assert_called_once_with()
203 203 assert result == hooks_module.Hooks().__enter__().post_push()
204 204
205 205
206 206 @pytest.fixture
207 207 def http_mirror(request):
208 208 server = MirrorHttpServer()
209 209 request.addfinalizer(server.stop)
210 210 return server
211 211
212 212
213 213 class MirrorHttpHandler(BaseHTTPRequestHandler):
214 214 def do_POST(self):
215 215 length = int(self.headers['Content-Length'])
216 216 body = self.rfile.read(length).decode('utf-8')
217 217 self.send_response(200)
218 218 self.end_headers()
219 219 self.wfile.write(body)
220 220
221 221
222 222 class MirrorHttpServer(object):
223 223 ip_address = '127.0.0.1'
224 224 port = 0
225 225
226 226 def __init__(self):
227 227 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
228 228 _, self.port = self._daemon.server_address
229 229 self._thread = threading.Thread(target=self._daemon.serve_forever)
230 230 self._thread.daemon = True
231 231 self._thread.start()
232 232
233 233 def stop(self):
234 234 self._daemon.shutdown()
235 235 self._thread.join()
236 236 self._daemon = None
237 237 self._thread = None
238 238
239 239 @property
240 240 def uri(self):
241 241 return '{}:{}'.format(self.ip_address, self.port)
@@ -1,206 +1,206 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import sys
20 20 import stat
21 21 import pytest
22 22 import vcsserver
23 23 import tempfile
24 24 from vcsserver import hook_utils
25 25 from vcsserver.tests.fixture import no_newline_id_generator
26 26 from vcsserver.utils import AttributeDict
27 27
28 28
29 29 class TestCheckRhodecodeHook(object):
30 30
31 31 def test_returns_false_when_hook_file_is_wrong_found(self, tmpdir):
32 32 hook = os.path.join(str(tmpdir), 'fake_hook_file.py')
33 33 with open(hook, 'wb') as f:
34 34 f.write('dummy test')
35 35 result = hook_utils.check_rhodecode_hook(hook)
36 36 assert result is False
37 37
38 38 def test_returns_true_when_no_hook_file_found(self, tmpdir):
39 39 hook = os.path.join(str(tmpdir), 'fake_hook_file_not_existing.py')
40 40 result = hook_utils.check_rhodecode_hook(hook)
41 41 assert result
42 42
43 43 @pytest.mark.parametrize("file_content, expected_result", [
44 44 ("RC_HOOK_VER = '3.3.3'\n", True),
45 45 ("RC_HOOK = '3.3.3'\n", False),
46 46 ], ids=no_newline_id_generator)
47 47 def test_signatures(self, file_content, expected_result, tmpdir):
48 48 hook = os.path.join(str(tmpdir), 'fake_hook_file_1.py')
49 49 with open(hook, 'wb') as f:
50 50 f.write(file_content)
51 51
52 52 result = hook_utils.check_rhodecode_hook(hook)
53 53
54 54 assert result is expected_result
55 55
56 56
57 57 class BaseInstallHooks(object):
58 58 HOOK_FILES = ()
59 59
60 60 def _check_hook_file_mode(self, file_path):
61 61 assert os.path.exists(file_path), 'path %s missing' % file_path
62 62 stat_info = os.stat(file_path)
63 63
64 64 file_mode = stat.S_IMODE(stat_info.st_mode)
65 65 expected_mode = int('755', 8)
66 66 assert expected_mode == file_mode
67 67
68 68 def _check_hook_file_content(self, file_path, executable):
69 69 executable = executable or sys.executable
70 70 with open(file_path, 'rt') as hook_file:
71 71 content = hook_file.read()
72 72
73 73 expected_env = '#!{}'.format(executable)
74 74 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(
75 75 vcsserver.__version__)
76 76 assert content.strip().startswith(expected_env)
77 77 assert expected_rc_version in content
78 78
79 79 def _create_fake_hook(self, file_path, content):
80 80 with open(file_path, 'w') as hook_file:
81 81 hook_file.write(content)
82 82
83 83 def create_dummy_repo(self, repo_type):
84 84 tmpdir = tempfile.mkdtemp()
85 85 repo = AttributeDict()
86 86 if repo_type == 'git':
87 87 repo.path = os.path.join(tmpdir, 'test_git_hooks_installation_repo')
88 88 os.makedirs(repo.path)
89 89 os.makedirs(os.path.join(repo.path, 'hooks'))
90 90 repo.bare = True
91 91
92 92 elif repo_type == 'svn':
93 93 repo.path = os.path.join(tmpdir, 'test_svn_hooks_installation_repo')
94 94 os.makedirs(repo.path)
95 95 os.makedirs(os.path.join(repo.path, 'hooks'))
96 96
97 97 return repo
98 98
99 99 def check_hooks(self, repo_path, repo_bare=True):
100 100 for file_name in self.HOOK_FILES:
101 101 if repo_bare:
102 102 file_path = os.path.join(repo_path, 'hooks', file_name)
103 103 else:
104 104 file_path = os.path.join(repo_path, '.git', 'hooks', file_name)
105 105 self._check_hook_file_mode(file_path)
106 106 self._check_hook_file_content(file_path, sys.executable)
107 107
108 108
109 109 class TestInstallGitHooks(BaseInstallHooks):
110 110 HOOK_FILES = ('pre-receive', 'post-receive')
111 111
112 112 def test_hooks_are_installed(self):
113 113 repo = self.create_dummy_repo('git')
114 114 result = hook_utils.install_git_hooks(repo.path, repo.bare)
115 115 assert result
116 116 self.check_hooks(repo.path, repo.bare)
117 117
118 118 def test_hooks_are_replaced(self):
119 119 repo = self.create_dummy_repo('git')
120 120 hooks_path = os.path.join(repo.path, 'hooks')
121 121 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
122 122 self._create_fake_hook(
123 123 file_path, content="RC_HOOK_VER = 'abcde'\n")
124 124
125 125 result = hook_utils.install_git_hooks(repo.path, repo.bare)
126 126 assert result
127 127 self.check_hooks(repo.path, repo.bare)
128 128
129 129 def test_non_rc_hooks_are_not_replaced(self):
130 130 repo = self.create_dummy_repo('git')
131 131 hooks_path = os.path.join(repo.path, 'hooks')
132 132 non_rc_content = 'echo "non rc hook"\n'
133 133 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
134 134 self._create_fake_hook(
135 135 file_path, content=non_rc_content)
136 136
137 137 result = hook_utils.install_git_hooks(repo.path, repo.bare)
138 138 assert result
139 139
140 140 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
141 141 with open(file_path, 'rt') as hook_file:
142 142 content = hook_file.read()
143 143 assert content == non_rc_content
144 144
145 145 def test_non_rc_hooks_are_replaced_with_force_flag(self):
146 146 repo = self.create_dummy_repo('git')
147 147 hooks_path = os.path.join(repo.path, 'hooks')
148 148 non_rc_content = 'echo "non rc hook"\n'
149 149 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
150 150 self._create_fake_hook(
151 151 file_path, content=non_rc_content)
152 152
153 153 result = hook_utils.install_git_hooks(
154 154 repo.path, repo.bare, force_create=True)
155 155 assert result
156 156 self.check_hooks(repo.path, repo.bare)
157 157
158 158
159 159 class TestInstallSvnHooks(BaseInstallHooks):
160 160 HOOK_FILES = ('pre-commit', 'post-commit')
161 161
162 162 def test_hooks_are_installed(self):
163 163 repo = self.create_dummy_repo('svn')
164 164 result = hook_utils.install_svn_hooks(repo.path)
165 165 assert result
166 166 self.check_hooks(repo.path)
167 167
168 168 def test_hooks_are_replaced(self):
169 169 repo = self.create_dummy_repo('svn')
170 170 hooks_path = os.path.join(repo.path, 'hooks')
171 171 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
172 172 self._create_fake_hook(
173 173 file_path, content="RC_HOOK_VER = 'abcde'\n")
174 174
175 175 result = hook_utils.install_svn_hooks(repo.path)
176 176 assert result
177 177 self.check_hooks(repo.path)
178 178
179 179 def test_non_rc_hooks_are_not_replaced(self):
180 180 repo = self.create_dummy_repo('svn')
181 181 hooks_path = os.path.join(repo.path, 'hooks')
182 182 non_rc_content = 'echo "non rc hook"\n'
183 183 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
184 184 self._create_fake_hook(
185 185 file_path, content=non_rc_content)
186 186
187 187 result = hook_utils.install_svn_hooks(repo.path)
188 188 assert result
189 189
190 190 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
191 191 with open(file_path, 'rt') as hook_file:
192 192 content = hook_file.read()
193 193 assert content == non_rc_content
194 194
195 195 def test_non_rc_hooks_are_replaced_with_force_flag(self):
196 196 repo = self.create_dummy_repo('svn')
197 197 hooks_path = os.path.join(repo.path, 'hooks')
198 198 non_rc_content = 'echo "non rc hook"\n'
199 199 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
200 200 self._create_fake_hook(
201 201 file_path, content=non_rc_content)
202 202
203 203 result = hook_utils.install_svn_hooks(
204 204 repo.path, force_create=True)
205 205 assert result
206 206 self.check_hooks(repo.path, )
@@ -1,57 +1,57 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import mock
19 19 import pytest
20 20
21 21 from vcsserver import http_main
22 22 from vcsserver.base import obfuscate_qs
23 23
24 24
25 25 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
26 26 @mock.patch('vcsserver.hgpatches.patch_largefiles_capabilities')
27 27 def test_applies_largefiles_patch(patch_largefiles_capabilities):
28 28 http_main.main({})
29 29 patch_largefiles_capabilities.assert_called_once_with()
30 30
31 31
32 32 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
33 33 @mock.patch('vcsserver.http_main.MercurialFactory', None)
34 34 @mock.patch(
35 35 'vcsserver.hgpatches.patch_largefiles_capabilities',
36 36 mock.Mock(side_effect=Exception("Must not be called")))
37 37 def test_applies_largefiles_patch_only_if_mercurial_is_available():
38 38 http_main.main({})
39 39
40 40
41 41 @pytest.mark.parametrize('given, expected', [
42 42 ('bad', 'bad'),
43 43 ('query&foo=bar', 'query&foo=bar'),
44 44 ('equery&auth_token=bar', 'equery&auth_token=*****'),
45 45 ('a;b;c;query&foo=bar&auth_token=secret',
46 46 'a&b&c&query&foo=bar&auth_token=*****'),
47 47 ('', ''),
48 48 (None, None),
49 49 ('foo=bar', 'foo=bar'),
50 50 ('auth_token=secret', 'auth_token=*****'),
51 51 ('auth_token=secret&api_key=secret2',
52 52 'auth_token=*****&api_key=*****'),
53 53 ('auth_token=secret&api_key=secret2&param=value',
54 54 'auth_token=*****&api_key=*****&param=value'),
55 55 ])
56 56 def test_obfuscate_qs(given, expected):
57 57 assert expected == obfuscate_qs(given)
@@ -1,249 +1,249 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import io
19 19
20 20 import dulwich.protocol
21 21 import mock
22 22 import pytest
23 23 import webob
24 24 import webtest
25 25
26 26 from vcsserver import hooks, pygrack
27 27
28 28 # pylint: disable=redefined-outer-name,protected-access
29 29
30 30
31 31 @pytest.fixture()
32 32 def pygrack_instance(tmpdir):
33 33 """
34 34 Creates a pygrack app instance.
35 35
36 36 Right now, it does not much helpful regarding the passed directory.
37 37 It just contains the required folders to pass the signature test.
38 38 """
39 39 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
40 40 tmpdir.mkdir(dir_name)
41 41
42 42 return pygrack.GitRepository('repo_name', str(tmpdir), 'git', False, {})
43 43
44 44
45 45 @pytest.fixture()
46 46 def pygrack_app(pygrack_instance):
47 47 """
48 48 Creates a pygrack app wrapped in webtest.TestApp.
49 49 """
50 50 return webtest.TestApp(pygrack_instance)
51 51
52 52
53 53 def test_invalid_service_info_refs_returns_403(pygrack_app):
54 54 response = pygrack_app.get('/info/refs?service=git-upload-packs',
55 55 expect_errors=True)
56 56
57 57 assert response.status_int == 403
58 58
59 59
60 60 def test_invalid_endpoint_returns_403(pygrack_app):
61 61 response = pygrack_app.post('/git-upload-packs', expect_errors=True)
62 62
63 63 assert response.status_int == 403
64 64
65 65
66 66 @pytest.mark.parametrize('sideband', [
67 67 'side-band-64k',
68 68 'side-band',
69 69 'side-band no-progress',
70 70 ])
71 71 def test_pre_pull_hook_fails_with_sideband(pygrack_app, sideband):
72 72 request = ''.join([
73 73 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ',
74 74 'multi_ack %s ofs-delta\n' % sideband,
75 75 '0000',
76 76 '0009done\n',
77 77 ])
78 78 with mock.patch('vcsserver.hooks.git_pre_pull',
79 79 return_value=hooks.HookResponse(1, 'foo')):
80 80 response = pygrack_app.post(
81 81 '/git-upload-pack', params=request,
82 82 content_type='application/x-git-upload-pack')
83 83
84 84 data = io.BytesIO(response.body)
85 85 proto = dulwich.protocol.Protocol(data.read, None)
86 86 packets = list(proto.read_pkt_seq())
87 87
88 88 expected_packets = [
89 89 'NAK\n', '\x02foo', '\x02Pre pull hook failed: aborting\n',
90 90 '\x01' + pygrack.GitRepository.EMPTY_PACK,
91 91 ]
92 92 assert packets == expected_packets
93 93
94 94
95 95 def test_pre_pull_hook_fails_no_sideband(pygrack_app):
96 96 request = ''.join([
97 97 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
98 98 'multi_ack ofs-delta\n'
99 99 '0000',
100 100 '0009done\n',
101 101 ])
102 102 with mock.patch('vcsserver.hooks.git_pre_pull',
103 103 return_value=hooks.HookResponse(1, 'foo')):
104 104 response = pygrack_app.post(
105 105 '/git-upload-pack', params=request,
106 106 content_type='application/x-git-upload-pack')
107 107
108 108 assert response.body == pygrack.GitRepository.EMPTY_PACK
109 109
110 110
111 111 def test_pull_has_hook_messages(pygrack_app):
112 112 request = ''.join([
113 113 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
114 114 'multi_ack side-band-64k ofs-delta\n'
115 115 '0000',
116 116 '0009done\n',
117 117 ])
118 118 with mock.patch('vcsserver.hooks.git_pre_pull',
119 119 return_value=hooks.HookResponse(0, 'foo')):
120 120 with mock.patch('vcsserver.hooks.git_post_pull',
121 121 return_value=hooks.HookResponse(1, 'bar')):
122 122 with mock.patch('vcsserver.subprocessio.SubprocessIOChunker',
123 123 return_value=['0008NAK\n0009subp\n0000']):
124 124 response = pygrack_app.post(
125 125 '/git-upload-pack', params=request,
126 126 content_type='application/x-git-upload-pack')
127 127
128 128 data = io.BytesIO(response.body)
129 129 proto = dulwich.protocol.Protocol(data.read, None)
130 130 packets = list(proto.read_pkt_seq())
131 131
132 132 assert packets == ['NAK\n', '\x02foo', 'subp\n', '\x02bar']
133 133
134 134
135 135 def test_get_want_capabilities(pygrack_instance):
136 136 data = io.BytesIO(
137 137 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
138 138 'multi_ack side-band-64k ofs-delta\n00000009done\n')
139 139
140 140 request = webob.Request({
141 141 'wsgi.input': data,
142 142 'REQUEST_METHOD': 'POST',
143 143 'webob.is_body_seekable': True
144 144 })
145 145
146 146 capabilities = pygrack_instance._get_want_capabilities(request)
147 147
148 148 assert capabilities == frozenset(
149 149 ('ofs-delta', 'multi_ack', 'side-band-64k'))
150 150 assert data.tell() == 0
151 151
152 152
153 153 @pytest.mark.parametrize('data,capabilities,expected', [
154 154 ('foo', [], []),
155 155 ('', ['side-band-64k'], []),
156 156 ('', ['side-band'], []),
157 157 ('foo', ['side-band-64k'], ['0008\x02foo']),
158 158 ('foo', ['side-band'], ['0008\x02foo']),
159 159 ('f'*1000, ['side-band-64k'], ['03ed\x02' + 'f' * 1000]),
160 160 ('f'*1000, ['side-band'], ['03e8\x02' + 'f' * 995, '000a\x02fffff']),
161 161 ('f'*65520, ['side-band-64k'], ['fff0\x02' + 'f' * 65515, '000a\x02fffff']),
162 162 ('f'*65520, ['side-band'], ['03e8\x02' + 'f' * 995] * 65 + ['0352\x02' + 'f' * 845]),
163 163 ], ids=[
164 164 'foo-empty',
165 165 'empty-64k', 'empty',
166 166 'foo-64k', 'foo',
167 167 'f-1000-64k', 'f-1000',
168 168 'f-65520-64k', 'f-65520'])
169 169 def test_get_messages(pygrack_instance, data, capabilities, expected):
170 170 messages = pygrack_instance._get_messages(data, capabilities)
171 171
172 172 assert messages == expected
173 173
174 174
175 175 @pytest.mark.parametrize('response,capabilities,pre_pull_messages,post_pull_messages', [
176 176 # Unexpected response
177 177 ('unexpected_response', ['side-band-64k'], 'foo', 'bar'),
178 178 # No sideband
179 179 ('no-sideband', [], 'foo', 'bar'),
180 180 # No messages
181 181 ('no-messages', ['side-band-64k'], '', ''),
182 182 ])
183 183 def test_inject_messages_to_response_nothing_to_do(
184 184 pygrack_instance, response, capabilities, pre_pull_messages,
185 185 post_pull_messages):
186 186 new_response = pygrack_instance._inject_messages_to_response(
187 187 response, capabilities, pre_pull_messages, post_pull_messages)
188 188
189 189 assert new_response == response
190 190
191 191
192 192 @pytest.mark.parametrize('capabilities', [
193 193 ['side-band'],
194 194 ['side-band-64k'],
195 195 ])
196 196 def test_inject_messages_to_response_single_element(pygrack_instance,
197 197 capabilities):
198 198 response = ['0008NAK\n0009subp\n0000']
199 199 new_response = pygrack_instance._inject_messages_to_response(
200 200 response, capabilities, 'foo', 'bar')
201 201
202 202 expected_response = [
203 203 '0008NAK\n', '0008\x02foo', '0009subp\n', '0008\x02bar', '0000']
204 204
205 205 assert new_response == expected_response
206 206
207 207
208 208 @pytest.mark.parametrize('capabilities', [
209 209 ['side-band'],
210 210 ['side-band-64k'],
211 211 ])
212 212 def test_inject_messages_to_response_multi_element(pygrack_instance,
213 213 capabilities):
214 214 response = [
215 215 '0008NAK\n000asubp1\n', '000asubp2\n', '000asubp3\n', '000asubp4\n0000']
216 216 new_response = pygrack_instance._inject_messages_to_response(
217 217 response, capabilities, 'foo', 'bar')
218 218
219 219 expected_response = [
220 220 '0008NAK\n', '0008\x02foo', '000asubp1\n', '000asubp2\n', '000asubp3\n',
221 221 '000asubp4\n', '0008\x02bar', '0000'
222 222 ]
223 223
224 224 assert new_response == expected_response
225 225
226 226
227 227 def test_build_failed_pre_pull_response_no_sideband(pygrack_instance):
228 228 response = pygrack_instance._build_failed_pre_pull_response([], 'foo')
229 229
230 230 assert response == [pygrack.GitRepository.EMPTY_PACK]
231 231
232 232
233 233 @pytest.mark.parametrize('capabilities', [
234 234 ['side-band'],
235 235 ['side-band-64k'],
236 236 ['side-band-64k', 'no-progress'],
237 237 ])
238 238 def test_build_failed_pre_pull_response(pygrack_instance, capabilities):
239 239 response = pygrack_instance._build_failed_pre_pull_response(
240 240 capabilities, 'foo')
241 241
242 242 expected_response = [
243 243 '0008NAK\n', '0008\x02foo', '0024\x02Pre pull hook failed: aborting\n',
244 244 '%04x\x01%s' % (len(pygrack.GitRepository.EMPTY_PACK) + 5,
245 245 pygrack.GitRepository.EMPTY_PACK),
246 246 '0000',
247 247 ]
248 248
249 249 assert response == expected_response
@@ -1,86 +1,86 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19
20 20 import mercurial.hg
21 21 import mercurial.ui
22 22 import mercurial.error
23 23 import mock
24 24 import pytest
25 25 import webtest
26 26
27 27 from vcsserver import scm_app
28 28
29 29
30 30 def test_hg_does_not_accept_invalid_cmd(tmpdir):
31 31 repo = mercurial.hg.repository(mercurial.ui.ui(), str(tmpdir), create=True)
32 32 app = webtest.TestApp(scm_app.HgWeb(repo))
33 33
34 34 response = app.get('/repo?cmd=invalidcmd', expect_errors=True)
35 35
36 36 assert response.status_int == 400
37 37
38 38
39 39 def test_create_hg_wsgi_app_requirement_error(tmpdir):
40 40 repo = mercurial.hg.repository(mercurial.ui.ui(), str(tmpdir), create=True)
41 41 config = (
42 42 ('paths', 'default', ''),
43 43 )
44 44 with mock.patch('vcsserver.scm_app.HgWeb') as hgweb_mock:
45 45 hgweb_mock.side_effect = mercurial.error.RequirementError()
46 46 with pytest.raises(Exception):
47 47 scm_app.create_hg_wsgi_app(str(tmpdir), repo, config)
48 48
49 49
50 50 def test_git_returns_not_found(tmpdir):
51 51 app = webtest.TestApp(
52 52 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
53 53
54 54 response = app.get('/repo_name/inforefs?service=git-upload-pack',
55 55 expect_errors=True)
56 56
57 57 assert response.status_int == 404
58 58
59 59
60 60 def test_git(tmpdir):
61 61 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
62 62 tmpdir.mkdir(dir_name)
63 63
64 64 app = webtest.TestApp(
65 65 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
66 66
67 67 # We set service to git-upload-packs to trigger a 403
68 68 response = app.get('/repo_name/inforefs?service=git-upload-packs',
69 69 expect_errors=True)
70 70
71 71 assert response.status_int == 403
72 72
73 73
74 74 def test_git_fallbacks_to_git_folder(tmpdir):
75 75 tmpdir.mkdir('.git')
76 76 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
77 77 tmpdir.mkdir(os.path.join('.git', dir_name))
78 78
79 79 app = webtest.TestApp(
80 80 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
81 81
82 82 # We set service to git-upload-packs to trigger a 403
83 83 response = app.get('/repo_name/inforefs?service=git-upload-packs',
84 84 expect_errors=True)
85 85
86 86 assert response.status_int == 403
@@ -1,39 +1,39 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 from vcsserver.server import VcsServer
24 24
25 25
26 26 def test_provides_the_pid(server):
27 27 pid = server.get_pid()
28 28 assert pid == os.getpid()
29 29
30 30
31 31 def test_allows_to_trigger_the_garbage_collector(server):
32 32 with mock.patch('gc.collect') as collect:
33 33 server.run_gc()
34 34 assert collect.called
35 35
36 36
37 37 @pytest.fixture
38 38 def server():
39 39 return VcsServer()
@@ -1,132 +1,132 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import io
19 19 import os
20 20 import sys
21 21
22 22 import pytest
23 23
24 24 from vcsserver import subprocessio
25 25
26 26
27 27 @pytest.fixture(scope='module')
28 28 def environ():
29 29 """Delete coverage variables, as they make the tests fail."""
30 30 env = dict(os.environ)
31 31 for key in env.keys():
32 32 if key.startswith('COV_CORE_'):
33 33 del env[key]
34 34
35 35 return env
36 36
37 37
38 38 def _get_python_args(script):
39 39 return [sys.executable, '-c', 'import sys; import time; import shutil; ' + script]
40 40
41 41
42 42 def test_raise_exception_on_non_zero_return_code(environ):
43 43 args = _get_python_args('sys.exit(1)')
44 44 with pytest.raises(EnvironmentError):
45 45 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
46 46
47 47
48 48 def test_does_not_fail_on_non_zero_return_code(environ):
49 49 args = _get_python_args('sys.exit(1)')
50 50 output = ''.join(
51 51 subprocessio.SubprocessIOChunker(
52 52 args, shell=False, fail_on_return_code=False, env=environ
53 53 )
54 54 )
55 55
56 56 assert output == ''
57 57
58 58
59 59 def test_raise_exception_on_stderr(environ):
60 60 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
61 61 with pytest.raises(EnvironmentError) as excinfo:
62 62 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
63 63
64 64 assert 'exited due to an error:\nX' in str(excinfo.value)
65 65
66 66
67 67 def test_does_not_fail_on_stderr(environ):
68 68 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
69 69 output = ''.join(
70 70 subprocessio.SubprocessIOChunker(
71 71 args, shell=False, fail_on_stderr=False, env=environ
72 72 )
73 73 )
74 74
75 75 assert output == ''
76 76
77 77
78 78 @pytest.mark.parametrize('size', [1, 10 ** 5])
79 79 def test_output_with_no_input(size, environ):
80 80 print(type(environ))
81 81 data = 'X'
82 82 args = _get_python_args('sys.stdout.write("%s" * %d)' % (data, size))
83 83 output = ''.join(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
84 84
85 85 assert output == data * size
86 86
87 87
88 88 @pytest.mark.parametrize('size', [1, 10 ** 5])
89 89 def test_output_with_no_input_does_not_fail(size, environ):
90 90 data = 'X'
91 91 args = _get_python_args('sys.stdout.write("%s" * %d); sys.exit(1)' % (data, size))
92 92 output = ''.join(
93 93 subprocessio.SubprocessIOChunker(
94 94 args, shell=False, fail_on_return_code=False, env=environ
95 95 )
96 96 )
97 97
98 98 print("{} {}".format(len(data * size), len(output)))
99 99 assert output == data * size
100 100
101 101
102 102 @pytest.mark.parametrize('size', [1, 10 ** 5])
103 103 def test_output_with_input(size, environ):
104 104 data = 'X' * size
105 105 inputstream = io.BytesIO(data)
106 106 # This acts like the cat command.
107 107 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
108 108 output = ''.join(
109 109 subprocessio.SubprocessIOChunker(
110 110 args, shell=False, inputstream=inputstream, env=environ
111 111 )
112 112 )
113 113
114 114 print("{} {}".format(len(data * size), len(output)))
115 115 assert output == data
116 116
117 117
118 118 @pytest.mark.parametrize('size', [1, 10 ** 5])
119 119 def test_output_with_input_skipping_iterator(size, environ):
120 120 data = 'X' * size
121 121 inputstream = io.BytesIO(data)
122 122 # This acts like the cat command.
123 123 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
124 124
125 125 # Note: assigning the chunker makes sure that it is not deleted too early
126 126 chunker = subprocessio.SubprocessIOChunker(
127 127 args, shell=False, inputstream=inputstream, env=environ
128 128 )
129 129 output = ''.join(chunker.output)
130 130
131 131 print("{} {}".format(len(data * size), len(output)))
132 132 assert output == data
@@ -1,82 +1,82 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import io
19 19 import mock
20 20 import pytest
21 21 import sys
22 22
23 23
24 24 class MockPopen(object):
25 25 def __init__(self, stderr):
26 26 self.stdout = io.BytesIO('')
27 27 self.stderr = io.BytesIO(stderr)
28 28 self.returncode = 1
29 29
30 30 def wait(self):
31 31 pass
32 32
33 33
34 34 INVALID_CERTIFICATE_STDERR = '\n'.join([
35 35 'svnrdump: E230001: Unable to connect to a repository at URL url',
36 36 'svnrdump: E230001: Server SSL certificate verification failed: issuer is not trusted',
37 37 ])
38 38
39 39
40 40 @pytest.mark.parametrize('stderr,expected_reason', [
41 41 (INVALID_CERTIFICATE_STDERR, 'INVALID_CERTIFICATE'),
42 42 ('svnrdump: E123456', 'UNKNOWN:svnrdump: E123456'),
43 43 ], ids=['invalid-cert-stderr', 'svnrdump-err-123456'])
44 44 @pytest.mark.xfail(sys.platform == "cygwin",
45 45 reason="SVN not packaged for Cygwin")
46 46 def test_import_remote_repository_certificate_error(stderr, expected_reason):
47 47 from vcsserver import svn
48 48
49 49 remote = svn.SvnRemote(None)
50 50 remote.is_path_valid_repository = lambda wire, path: True
51 51
52 52 with mock.patch('subprocess.Popen',
53 53 return_value=MockPopen(stderr)):
54 54 with pytest.raises(Exception) as excinfo:
55 55 remote.import_remote_repository({'path': 'path'}, 'url')
56 56
57 57 expected_error_args = (
58 58 'Failed to dump the remote repository from url. Reason:{}'.format(expected_reason),)
59 59
60 60 assert excinfo.value.args == expected_error_args
61 61
62 62
63 63 def test_svn_libraries_can_be_imported():
64 64 import svn
65 65 import svn.client
66 66 assert svn.client is not None
67 67
68 68
69 69 @pytest.mark.parametrize('example_url, parts', [
70 70 ('http://server.com', (None, None, 'http://server.com')),
71 71 ('http://user@server.com', ('user', None, 'http://user@server.com')),
72 72 ('http://user:pass@server.com', ('user', 'pass', 'http://user:pass@server.com')),
73 73 ('<script>', (None, None, '<script>')),
74 74 ('http://', (None, None, 'http://')),
75 75 ])
76 76 def test_username_password_extraction_from_url(example_url, parts):
77 77 from vcsserver import svn
78 78
79 79 remote = svn.SvnRemote(None)
80 80 remote.is_path_valid_repository = lambda wire, path: True
81 81
82 82 assert remote.get_url_and_credentials(example_url) == parts
@@ -1,96 +1,96 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import wsgiref.simple_server
19 19 import wsgiref.validate
20 20
21 21 from vcsserver import wsgi_app_caller
22 22
23 23
24 24 # pylint: disable=protected-access,too-many-public-methods
25 25
26 26
27 27 @wsgiref.validate.validator
28 28 def demo_app(environ, start_response):
29 29 """WSGI app used for testing."""
30 30 data = [
31 31 'Hello World!\n',
32 32 'input_data=%s\n' % environ['wsgi.input'].read(),
33 33 ]
34 34 for key, value in sorted(environ.items()):
35 35 data.append('%s=%s\n' % (key, value))
36 36
37 37 write = start_response("200 OK", [('Content-Type', 'text/plain')])
38 38 write('Old school write method\n')
39 39 write('***********************\n')
40 40 return data
41 41
42 42
43 43 BASE_ENVIRON = {
44 44 'REQUEST_METHOD': 'GET',
45 45 'SERVER_NAME': 'localhost',
46 46 'SERVER_PORT': '80',
47 47 'SCRIPT_NAME': '',
48 48 'PATH_INFO': '/',
49 49 'QUERY_STRING': '',
50 50 'foo.var': 'bla',
51 51 }
52 52
53 53
54 54 def test_complete_environ():
55 55 environ = dict(BASE_ENVIRON)
56 56 data = "data"
57 57 wsgi_app_caller._complete_environ(environ, data)
58 58 wsgiref.validate.check_environ(environ)
59 59
60 60 assert data == environ['wsgi.input'].read()
61 61
62 62
63 63 def test_start_response():
64 64 start_response = wsgi_app_caller._StartResponse()
65 65 status = '200 OK'
66 66 headers = [('Content-Type', 'text/plain')]
67 67 start_response(status, headers)
68 68
69 69 assert status == start_response.status
70 70 assert headers == start_response.headers
71 71
72 72
73 73 def test_start_response_with_error():
74 74 start_response = wsgi_app_caller._StartResponse()
75 75 status = '500 Internal Server Error'
76 76 headers = [('Content-Type', 'text/plain')]
77 77 start_response(status, headers, (None, None, None))
78 78
79 79 assert status == start_response.status
80 80 assert headers == start_response.headers
81 81
82 82
83 83 def test_wsgi_app_caller():
84 84 caller = wsgi_app_caller.WSGIAppCaller(demo_app)
85 85 environ = dict(BASE_ENVIRON)
86 86 input_data = 'some text'
87 87 responses, status, headers = caller.handle(environ, input_data)
88 88 response = ''.join(responses)
89 89
90 90 assert status == '200 OK'
91 91 assert headers == [('Content-Type', 'text/plain')]
92 92 assert response.startswith(
93 93 'Old school write method\n***********************\n')
94 94 assert 'Hello World!\n' in response
95 95 assert 'foo.var=bla\n' in response
96 96 assert 'input_data=%s\n' % input_data in response
@@ -1,58 +1,58 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18
19 19
20 20 import time
21 21 import logging
22 22
23 23
24 24 from vcsserver.utils import safe_str
25 25
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29
30 30 def get_access_path(request):
31 31 environ = request.environ
32 32 return environ.get('PATH_INFO')
33 33
34 34
35 35 class RequestWrapperTween(object):
36 36 def __init__(self, handler, registry):
37 37 self.handler = handler
38 38 self.registry = registry
39 39
40 40 # one-time configuration code goes here
41 41
42 42 def __call__(self, request):
43 43 start = time.time()
44 44 try:
45 45 response = self.handler(request)
46 46 finally:
47 47 end = time.time()
48 48
49 49 log.info('IP: %s Request to path: `%s` time: %.3fs',
50 50 '127.0.0.1', safe_str(get_access_path(request)), end - start)
51 51
52 52 return response
53 53
54 54
55 55 def includeme(config):
56 56 config.add_tween(
57 57 'vcsserver.tweens.RequestWrapperTween',
58 58 )
@@ -1,89 +1,89 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17 import logging
18 18 import hashlib
19 19
20 20 log = logging.getLogger(__name__)
21 21
22 22
23 23 def safe_int(val, default=None):
24 24 """
25 25 Returns int() of val if val is not convertable to int use default
26 26 instead
27 27
28 28 :param val:
29 29 :param default:
30 30 """
31 31
32 32 try:
33 33 val = int(val)
34 34 except (ValueError, TypeError):
35 35 val = default
36 36
37 37 return val
38 38
39 39
40 40 def safe_str(unicode_, to_encoding=['utf8']):
41 41 """
42 42 safe str function. Does few trick to turn unicode_ into string
43 43
44 44 In case of UnicodeEncodeError, we try to return it with encoding detected
45 45 by chardet library if it fails fallback to string with errors replaced
46 46
47 47 :param unicode_: unicode to encode
48 48 :rtype: str
49 49 :returns: str object
50 50 """
51 51
52 52 # if it's not basestr cast to str
53 53 if not isinstance(unicode_, basestring):
54 54 return str(unicode_)
55 55
56 56 if isinstance(unicode_, str):
57 57 return unicode_
58 58
59 59 if not isinstance(to_encoding, (list, tuple)):
60 60 to_encoding = [to_encoding]
61 61
62 62 for enc in to_encoding:
63 63 try:
64 64 return unicode_.encode(enc)
65 65 except UnicodeEncodeError:
66 66 pass
67 67
68 68 try:
69 69 import chardet
70 70 encoding = chardet.detect(unicode_)['encoding']
71 71 if encoding is None:
72 72 raise UnicodeEncodeError()
73 73
74 74 return unicode_.encode(encoding)
75 75 except (ImportError, UnicodeEncodeError):
76 76 return unicode_.encode(to_encoding[0], 'replace')
77 77
78 78
79 79 class AttributeDict(dict):
80 80 def __getattr__(self, attr):
81 81 return self.get(attr, None)
82 82 __setattr__ = dict.__setitem__
83 83 __delattr__ = dict.__delitem__
84 84
85 85
86 86 def sha1(val):
87 87 return hashlib.sha1(val).hexdigest()
88 88
89 89
@@ -1,116 +1,116 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 """Extract the responses of a WSGI app."""
19 19
20 20 __all__ = ('WSGIAppCaller',)
21 21
22 22 import io
23 23 import logging
24 24 import os
25 25
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29 DEV_NULL = open(os.devnull)
30 30
31 31
32 32 def _complete_environ(environ, input_data):
33 33 """Update the missing wsgi.* variables of a WSGI environment.
34 34
35 35 :param environ: WSGI environment to update
36 36 :type environ: dict
37 37 :param input_data: data to be read by the app
38 38 :type input_data: str
39 39 """
40 40 environ.update({
41 41 'wsgi.version': (1, 0),
42 42 'wsgi.url_scheme': 'http',
43 43 'wsgi.multithread': True,
44 44 'wsgi.multiprocess': True,
45 45 'wsgi.run_once': False,
46 46 'wsgi.input': io.BytesIO(input_data),
47 47 'wsgi.errors': DEV_NULL,
48 48 })
49 49
50 50
51 51 # pylint: disable=too-few-public-methods
52 52 class _StartResponse(object):
53 53 """Save the arguments of a start_response call."""
54 54
55 55 __slots__ = ['status', 'headers', 'content']
56 56
57 57 def __init__(self):
58 58 self.status = None
59 59 self.headers = None
60 60 self.content = []
61 61
62 62 def __call__(self, status, headers, exc_info=None):
63 63 # TODO(skreft): do something meaningful with the exc_info
64 64 exc_info = None # avoid dangling circular reference
65 65 self.status = status
66 66 self.headers = headers
67 67
68 68 return self.write
69 69
70 70 def write(self, content):
71 71 """Write method returning when calling this object.
72 72
73 73 All the data written is then available in content.
74 74 """
75 75 self.content.append(content)
76 76
77 77
78 78 class WSGIAppCaller(object):
79 79 """Calls a WSGI app."""
80 80
81 81 def __init__(self, app):
82 82 """
83 83 :param app: WSGI app to call
84 84 """
85 85 self.app = app
86 86
87 87 def handle(self, environ, input_data):
88 88 """Process a request with the WSGI app.
89 89
90 90 The returned data of the app is fully consumed into a list.
91 91
92 92 :param environ: WSGI environment to update
93 93 :type environ: dict
94 94 :param input_data: data to be read by the app
95 95 :type input_data: str
96 96
97 97 :returns: a tuple with the contents, status and headers
98 98 :rtype: (list<str>, str, list<(str, str)>)
99 99 """
100 100 _complete_environ(environ, input_data)
101 101 start_response = _StartResponse()
102 102 log.debug("Calling wrapped WSGI application")
103 103 responses = self.app(environ, start_response)
104 104 responses_list = list(responses)
105 105 existing_responses = start_response.content
106 106 if existing_responses:
107 107 log.debug(
108 108 "Adding returned response to response written via write()")
109 109 existing_responses.extend(responses_list)
110 110 responses_list = existing_responses
111 111 if hasattr(responses, 'close'):
112 112 log.debug("Closing iterator from WSGI application")
113 113 responses.close()
114 114
115 115 log.debug("Handling of WSGI request done, returning response")
116 116 return responses_list, start_response.status, start_response.headers
General Comments 0
You need to be logged in to leave comments. Login now