##// END OF EJS Templates
core: udpate copyright string to 2018
ergo -
r352:b25f7b7c default
parent child Browse files
Show More
@@ -1,21 +1,21 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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()
@@ -1,98 +1,98 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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 log = logging.getLogger(__name__)
24 24
25 25
26 26 class RepoFactory(object):
27 27 """
28 28 Utility to create instances of repository
29 29
30 30 It provides internal caching of the `repo` object based on
31 31 the :term:`call context`.
32 32 """
33 33
34 34 def __init__(self, repo_cache):
35 35 self._cache = repo_cache
36 36
37 37 def _create_config(self, path, config):
38 38 config = {}
39 39 return config
40 40
41 41 def _create_repo(self, wire, create):
42 42 raise NotImplementedError()
43 43
44 44 def repo(self, wire, create=False):
45 45 """
46 46 Get a repository instance for the given path.
47 47
48 48 Uses internally the low level beaker API since the decorators introduce
49 49 significant overhead.
50 50 """
51 51 def create_new_repo():
52 52 return self._create_repo(wire, create)
53 53
54 54 return self._repo(wire, create_new_repo)
55 55
56 56 def _repo(self, wire, createfunc):
57 57 context = wire.get('context', None)
58 58 cache = wire.get('cache', True)
59 59
60 60 if context and cache:
61 61 cache_key = (context, wire['path'])
62 62 log.debug(
63 63 'FETCH %s@%s repo object from cache. Context: %s',
64 64 self.__class__.__name__, wire['path'], context)
65 65 return self._cache.get(key=cache_key, createfunc=createfunc)
66 66 else:
67 67 log.debug(
68 68 'INIT %s@%s repo object based on wire %s. Context: %s',
69 69 self.__class__.__name__, wire['path'], wire, context)
70 70 return createfunc()
71 71
72 72
73 73 def obfuscate_qs(query_string):
74 74 if query_string is None:
75 75 return None
76 76
77 77 parsed = []
78 78 for k, v in urlparse.parse_qsl(query_string, keep_blank_values=True):
79 79 if k in ['auth_token', 'api_key']:
80 80 v = "*****"
81 81 parsed.append((k, v))
82 82
83 83 return '&'.join('{}{}'.format(
84 84 k, '={}'.format(v) if v else '') for k, v in parsed)
85 85
86 86
87 87 def raise_from_original(new_type):
88 88 """
89 89 Raise a new exception type with original args and traceback.
90 90 """
91 91 exc_type, exc_value, exc_traceback = sys.exc_info()
92 92
93 93 traceback.format_exception(exc_type, exc_value, exc_traceback)
94 94
95 95 try:
96 96 raise new_type(*exc_value.args), None, exc_traceback
97 97 finally:
98 98 del exc_traceback
@@ -1,70 +1,70 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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 import functools
28 28 from pyramid.httpexceptions import HTTPLocked
29 29
30 30
31 31 def _make_exception(kind, *args):
32 32 """
33 33 Prepares a base `Exception` instance to be sent over the wire.
34 34
35 35 To give our caller a hint what this is about, it will attach an attribute
36 36 `_vcs_kind` to the exception.
37 37 """
38 38 exc = Exception(*args)
39 39 exc._vcs_kind = kind
40 40 return exc
41 41
42 42
43 43 AbortException = functools.partial(_make_exception, 'abort')
44 44
45 45 ArchiveException = functools.partial(_make_exception, 'archive')
46 46
47 47 LookupException = functools.partial(_make_exception, 'lookup')
48 48
49 49 VcsException = functools.partial(_make_exception, 'error')
50 50
51 51 RepositoryLockedException = functools.partial(_make_exception, 'repo_locked')
52 52
53 53 RequirementException = functools.partial(_make_exception, 'requirement')
54 54
55 55 UnhandledException = functools.partial(_make_exception, 'unhandled')
56 56
57 57 URLError = functools.partial(_make_exception, 'url_error')
58 58
59 59 SubrepoMergeException = functools.partial(_make_exception, 'subrepo_merge_error')
60 60
61 61
62 62 class HTTPRepoLocked(HTTPLocked):
63 63 """
64 64 Subclass of HTTPLocked response that allows to set the title and status
65 65 code via constructor arguments.
66 66 """
67 67 def __init__(self, title, status_code=None, **kwargs):
68 68 self.code = status_code or HTTPLocked.code
69 69 self.title = title
70 70 super(HTTPRepoLocked, self).__init__(**kwargs)
@@ -1,658 +1,658 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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 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 raise exceptions.LookupException(e.message)
60 60 except (HangupException, UnexpectedCommandError) as e:
61 61 raise exceptions.VcsException(e.message)
62 62 except Exception as e:
63 63 # NOTE(marcink): becuase of how dulwich handles some exceptions
64 64 # (KeyError on empty repos), we cannot track this and catch all
65 65 # exceptions, it's an exceptions from other handlers
66 66 #if not hasattr(e, '_vcs_kind'):
67 67 #log.exception("Unhandled exception in git remote call")
68 68 #raise_from_original(exceptions.UnhandledException)
69 69 raise
70 70 return wrapper
71 71
72 72
73 73 class Repo(DulwichRepo):
74 74 """
75 75 A wrapper for dulwich Repo class.
76 76
77 77 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
78 78 "Too many open files" error. We need to close all opened file descriptors
79 79 once the repo object is destroyed.
80 80
81 81 TODO: mikhail: please check if we need this wrapper after updating dulwich
82 82 to 0.12.0 +
83 83 """
84 84 def __del__(self):
85 85 if hasattr(self, 'object_store'):
86 86 self.close()
87 87
88 88
89 89 class GitFactory(RepoFactory):
90 90
91 91 def _create_repo(self, wire, create):
92 92 repo_path = str_to_dulwich(wire['path'])
93 93 return Repo(repo_path)
94 94
95 95
96 96 class GitRemote(object):
97 97
98 98 def __init__(self, factory):
99 99 self._factory = factory
100 100
101 101 self._bulk_methods = {
102 102 "author": self.commit_attribute,
103 103 "date": self.get_object_attrs,
104 104 "message": self.commit_attribute,
105 105 "parents": self.commit_attribute,
106 106 "_commit": self.revision,
107 107 }
108 108
109 109 def _wire_to_config(self, wire):
110 110 if 'config' in wire:
111 111 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
112 112 return {}
113 113
114 114 def _assign_ref(self, wire, ref, commit_id):
115 115 repo = self._factory.repo(wire)
116 116 repo[ref] = commit_id
117 117
118 118 @reraise_safe_exceptions
119 119 def add_object(self, wire, content):
120 120 repo = self._factory.repo(wire)
121 121 blob = objects.Blob()
122 122 blob.set_raw_string(content)
123 123 repo.object_store.add_object(blob)
124 124 return blob.id
125 125
126 126 @reraise_safe_exceptions
127 127 def assert_correct_path(self, wire):
128 128 path = wire.get('path')
129 129 try:
130 130 self._factory.repo(wire)
131 131 except NotGitRepository as e:
132 132 tb = traceback.format_exc()
133 133 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
134 134 return False
135 135
136 136 return True
137 137
138 138 @reraise_safe_exceptions
139 139 def bare(self, wire):
140 140 repo = self._factory.repo(wire)
141 141 return repo.bare
142 142
143 143 @reraise_safe_exceptions
144 144 def blob_as_pretty_string(self, wire, sha):
145 145 repo = self._factory.repo(wire)
146 146 return repo[sha].as_pretty_string()
147 147
148 148 @reraise_safe_exceptions
149 149 def blob_raw_length(self, wire, sha):
150 150 repo = self._factory.repo(wire)
151 151 blob = repo[sha]
152 152 return blob.raw_length()
153 153
154 154 def _parse_lfs_pointer(self, raw_content):
155 155
156 156 spec_string = 'version https://git-lfs.github.com/spec'
157 157 if raw_content and raw_content.startswith(spec_string):
158 158 pattern = re.compile(r"""
159 159 (?:\n)?
160 160 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
161 161 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
162 162 ^size[ ](?P<oid_size>[0-9]+)\n
163 163 (?:\n)?
164 164 """, re.VERBOSE | re.MULTILINE)
165 165 match = pattern.match(raw_content)
166 166 if match:
167 167 return match.groupdict()
168 168
169 169 return {}
170 170
171 171 @reraise_safe_exceptions
172 172 def is_large_file(self, wire, sha):
173 173 repo = self._factory.repo(wire)
174 174 blob = repo[sha]
175 175 return self._parse_lfs_pointer(blob.as_raw_string())
176 176
177 177 @reraise_safe_exceptions
178 178 def in_largefiles_store(self, wire, oid):
179 179 repo = self._factory.repo(wire)
180 180 conf = self._wire_to_config(wire)
181 181
182 182 store_location = conf.get('vcs_git_lfs_store_location')
183 183 if store_location:
184 184 repo_name = repo.path
185 185 store = LFSOidStore(
186 186 oid=oid, repo=repo_name, store_location=store_location)
187 187 return store.has_oid()
188 188
189 189 return False
190 190
191 191 @reraise_safe_exceptions
192 192 def store_path(self, wire, oid):
193 193 repo = self._factory.repo(wire)
194 194 conf = self._wire_to_config(wire)
195 195
196 196 store_location = conf.get('vcs_git_lfs_store_location')
197 197 if store_location:
198 198 repo_name = repo.path
199 199 store = LFSOidStore(
200 200 oid=oid, repo=repo_name, store_location=store_location)
201 201 return store.oid_path
202 202 raise ValueError('Unable to fetch oid with path {}'.format(oid))
203 203
204 204 @reraise_safe_exceptions
205 205 def bulk_request(self, wire, rev, pre_load):
206 206 result = {}
207 207 for attr in pre_load:
208 208 try:
209 209 method = self._bulk_methods[attr]
210 210 args = [wire, rev]
211 211 if attr == "date":
212 212 args.extend(["commit_time", "commit_timezone"])
213 213 elif attr in ["author", "message", "parents"]:
214 214 args.append(attr)
215 215 result[attr] = method(*args)
216 216 except KeyError:
217 217 raise exceptions.VcsException(
218 218 "Unknown bulk attribute: %s" % attr)
219 219 return result
220 220
221 221 def _build_opener(self, url):
222 222 handlers = []
223 223 url_obj = url_parser(url)
224 224 _, authinfo = url_obj.authinfo()
225 225
226 226 if authinfo:
227 227 # create a password manager
228 228 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
229 229 passmgr.add_password(*authinfo)
230 230
231 231 handlers.extend((httpbasicauthhandler(passmgr),
232 232 httpdigestauthhandler(passmgr)))
233 233
234 234 return urllib2.build_opener(*handlers)
235 235
236 236 @reraise_safe_exceptions
237 237 def check_url(self, url, config):
238 238 url_obj = url_parser(url)
239 239 test_uri, _ = url_obj.authinfo()
240 240 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
241 241 url_obj.query = obfuscate_qs(url_obj.query)
242 242 cleaned_uri = str(url_obj)
243 243 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
244 244
245 245 if not test_uri.endswith('info/refs'):
246 246 test_uri = test_uri.rstrip('/') + '/info/refs'
247 247
248 248 o = self._build_opener(url)
249 249 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
250 250
251 251 q = {"service": 'git-upload-pack'}
252 252 qs = '?%s' % urllib.urlencode(q)
253 253 cu = "%s%s" % (test_uri, qs)
254 254 req = urllib2.Request(cu, None, {})
255 255
256 256 try:
257 257 log.debug("Trying to open URL %s", cleaned_uri)
258 258 resp = o.open(req)
259 259 if resp.code != 200:
260 260 raise exceptions.URLError('Return Code is not 200')
261 261 except Exception as e:
262 262 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
263 263 # means it cannot be cloned
264 264 raise exceptions.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
265 265
266 266 # now detect if it's proper git repo
267 267 gitdata = resp.read()
268 268 if 'service=git-upload-pack' in gitdata:
269 269 pass
270 270 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
271 271 # old style git can return some other format !
272 272 pass
273 273 else:
274 274 raise exceptions.URLError(
275 275 "url [%s] does not look like an git" % (cleaned_uri,))
276 276
277 277 return True
278 278
279 279 @reraise_safe_exceptions
280 280 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
281 281 remote_refs = self.fetch(wire, url, apply_refs=False)
282 282 repo = self._factory.repo(wire)
283 283 if isinstance(valid_refs, list):
284 284 valid_refs = tuple(valid_refs)
285 285
286 286 for k in remote_refs:
287 287 # only parse heads/tags and skip so called deferred tags
288 288 if k.startswith(valid_refs) and not k.endswith(deferred):
289 289 repo[k] = remote_refs[k]
290 290
291 291 if update_after_clone:
292 292 # we want to checkout HEAD
293 293 repo["HEAD"] = remote_refs["HEAD"]
294 294 index.build_index_from_tree(repo.path, repo.index_path(),
295 295 repo.object_store, repo["HEAD"].tree)
296 296
297 297 # TODO: this is quite complex, check if that can be simplified
298 298 @reraise_safe_exceptions
299 299 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
300 300 repo = self._factory.repo(wire)
301 301 object_store = repo.object_store
302 302
303 303 # Create tree and populates it with blobs
304 304 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
305 305
306 306 for node in updated:
307 307 # Compute subdirs if needed
308 308 dirpath, nodename = vcspath.split(node['path'])
309 309 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
310 310 parent = commit_tree
311 311 ancestors = [('', parent)]
312 312
313 313 # Tries to dig for the deepest existing tree
314 314 while dirnames:
315 315 curdir = dirnames.pop(0)
316 316 try:
317 317 dir_id = parent[curdir][1]
318 318 except KeyError:
319 319 # put curdir back into dirnames and stops
320 320 dirnames.insert(0, curdir)
321 321 break
322 322 else:
323 323 # If found, updates parent
324 324 parent = repo[dir_id]
325 325 ancestors.append((curdir, parent))
326 326 # Now parent is deepest existing tree and we need to create
327 327 # subtrees for dirnames (in reverse order)
328 328 # [this only applies for nodes from added]
329 329 new_trees = []
330 330
331 331 blob = objects.Blob.from_string(node['content'])
332 332
333 333 if dirnames:
334 334 # If there are trees which should be created we need to build
335 335 # them now (in reverse order)
336 336 reversed_dirnames = list(reversed(dirnames))
337 337 curtree = objects.Tree()
338 338 curtree[node['node_path']] = node['mode'], blob.id
339 339 new_trees.append(curtree)
340 340 for dirname in reversed_dirnames[:-1]:
341 341 newtree = objects.Tree()
342 342 newtree[dirname] = (DIR_STAT, curtree.id)
343 343 new_trees.append(newtree)
344 344 curtree = newtree
345 345 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
346 346 else:
347 347 parent.add(
348 348 name=node['node_path'], mode=node['mode'], hexsha=blob.id)
349 349
350 350 new_trees.append(parent)
351 351 # Update ancestors
352 352 reversed_ancestors = reversed(
353 353 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
354 354 for parent, tree, path in reversed_ancestors:
355 355 parent[path] = (DIR_STAT, tree.id)
356 356 object_store.add_object(tree)
357 357
358 358 object_store.add_object(blob)
359 359 for tree in new_trees:
360 360 object_store.add_object(tree)
361 361
362 362 for node_path in removed:
363 363 paths = node_path.split('/')
364 364 tree = commit_tree
365 365 trees = [tree]
366 366 # Traverse deep into the forest...
367 367 for path in paths:
368 368 try:
369 369 obj = repo[tree[path][1]]
370 370 if isinstance(obj, objects.Tree):
371 371 trees.append(obj)
372 372 tree = obj
373 373 except KeyError:
374 374 break
375 375 # Cut down the blob and all rotten trees on the way back...
376 376 for path, tree in reversed(zip(paths, trees)):
377 377 del tree[path]
378 378 if tree:
379 379 # This tree still has elements - don't remove it or any
380 380 # of it's parents
381 381 break
382 382
383 383 object_store.add_object(commit_tree)
384 384
385 385 # Create commit
386 386 commit = objects.Commit()
387 387 commit.tree = commit_tree.id
388 388 for k, v in commit_data.iteritems():
389 389 setattr(commit, k, v)
390 390 object_store.add_object(commit)
391 391
392 392 ref = 'refs/heads/%s' % branch
393 393 repo.refs[ref] = commit.id
394 394
395 395 return commit.id
396 396
397 397 @reraise_safe_exceptions
398 398 def fetch(self, wire, url, apply_refs=True, refs=None):
399 399 if url != 'default' and '://' not in url:
400 400 client = LocalGitClient(url)
401 401 else:
402 402 url_obj = url_parser(url)
403 403 o = self._build_opener(url)
404 404 url, _ = url_obj.authinfo()
405 405 client = HttpGitClient(base_url=url, opener=o)
406 406 repo = self._factory.repo(wire)
407 407
408 408 determine_wants = repo.object_store.determine_wants_all
409 409 if refs:
410 410 def determine_wants_requested(references):
411 411 return [references[r] for r in references if r in refs]
412 412 determine_wants = determine_wants_requested
413 413
414 414 try:
415 415 remote_refs = client.fetch(
416 416 path=url, target=repo, determine_wants=determine_wants)
417 417 except NotGitRepository as e:
418 418 log.warning(
419 419 'Trying to fetch from "%s" failed, not a Git repository.', url)
420 420 # Exception can contain unicode which we convert
421 421 raise exceptions.AbortException(repr(e))
422 422
423 423 # mikhail: client.fetch() returns all the remote refs, but fetches only
424 424 # refs filtered by `determine_wants` function. We need to filter result
425 425 # as well
426 426 if refs:
427 427 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
428 428
429 429 if apply_refs:
430 430 # TODO: johbo: Needs proper test coverage with a git repository
431 431 # that contains a tag object, so that we would end up with
432 432 # a peeled ref at this point.
433 433 PEELED_REF_MARKER = '^{}'
434 434 for k in remote_refs:
435 435 if k.endswith(PEELED_REF_MARKER):
436 436 log.info("Skipping peeled reference %s", k)
437 437 continue
438 438 repo[k] = remote_refs[k]
439 439
440 440 if refs:
441 441 # mikhail: explicitly set the head to the last ref.
442 442 repo['HEAD'] = remote_refs[refs[-1]]
443 443
444 444 # TODO: mikhail: should we return remote_refs here to be
445 445 # consistent?
446 446 else:
447 447 return remote_refs
448 448
449 449 @reraise_safe_exceptions
450 450 def sync_push(self, wire, url, refs=None):
451 451 if self.check_url(url, wire):
452 452 repo = self._factory.repo(wire)
453 453 self.run_git_command(
454 454 wire, ['push', url, '--mirror'], fail_on_stderr=False)
455 455
456 456
457 457 @reraise_safe_exceptions
458 458 def get_remote_refs(self, wire, url):
459 459 repo = Repo(url)
460 460 return repo.get_refs()
461 461
462 462 @reraise_safe_exceptions
463 463 def get_description(self, wire):
464 464 repo = self._factory.repo(wire)
465 465 return repo.get_description()
466 466
467 467 @reraise_safe_exceptions
468 468 def get_file_history(self, wire, file_path, commit_id, limit):
469 469 repo = self._factory.repo(wire)
470 470 include = [commit_id]
471 471 paths = [file_path]
472 472
473 473 walker = repo.get_walker(include, paths=paths, max_entries=limit)
474 474 return [x.commit.id for x in walker]
475 475
476 476 @reraise_safe_exceptions
477 477 def get_missing_revs(self, wire, rev1, rev2, path2):
478 478 repo = self._factory.repo(wire)
479 479 LocalGitClient(thin_packs=False).fetch(path2, repo)
480 480
481 481 wire_remote = wire.copy()
482 482 wire_remote['path'] = path2
483 483 repo_remote = self._factory.repo(wire_remote)
484 484 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
485 485
486 486 revs = [
487 487 x.commit.id
488 488 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
489 489 return revs
490 490
491 491 @reraise_safe_exceptions
492 492 def get_object(self, wire, sha):
493 493 repo = self._factory.repo(wire)
494 494 obj = repo.get_object(sha)
495 495 commit_id = obj.id
496 496
497 497 if isinstance(obj, Tag):
498 498 commit_id = obj.object[1]
499 499
500 500 return {
501 501 'id': obj.id,
502 502 'type': obj.type_name,
503 503 'commit_id': commit_id
504 504 }
505 505
506 506 @reraise_safe_exceptions
507 507 def get_object_attrs(self, wire, sha, *attrs):
508 508 repo = self._factory.repo(wire)
509 509 obj = repo.get_object(sha)
510 510 return list(getattr(obj, a) for a in attrs)
511 511
512 512 @reraise_safe_exceptions
513 513 def get_refs(self, wire):
514 514 repo = self._factory.repo(wire)
515 515 result = {}
516 516 for ref, sha in repo.refs.as_dict().items():
517 517 peeled_sha = repo.get_peeled(ref)
518 518 result[ref] = peeled_sha
519 519 return result
520 520
521 521 @reraise_safe_exceptions
522 522 def get_refs_path(self, wire):
523 523 repo = self._factory.repo(wire)
524 524 return repo.refs.path
525 525
526 526 @reraise_safe_exceptions
527 527 def head(self, wire):
528 528 repo = self._factory.repo(wire)
529 529 return repo.head()
530 530
531 531 @reraise_safe_exceptions
532 532 def init(self, wire):
533 533 repo_path = str_to_dulwich(wire['path'])
534 534 self.repo = Repo.init(repo_path)
535 535
536 536 @reraise_safe_exceptions
537 537 def init_bare(self, wire):
538 538 repo_path = str_to_dulwich(wire['path'])
539 539 self.repo = Repo.init_bare(repo_path)
540 540
541 541 @reraise_safe_exceptions
542 542 def revision(self, wire, rev):
543 543 repo = self._factory.repo(wire)
544 544 obj = repo[rev]
545 545 obj_data = {
546 546 'id': obj.id,
547 547 }
548 548 try:
549 549 obj_data['tree'] = obj.tree
550 550 except AttributeError:
551 551 pass
552 552 return obj_data
553 553
554 554 @reraise_safe_exceptions
555 555 def commit_attribute(self, wire, rev, attr):
556 556 repo = self._factory.repo(wire)
557 557 obj = repo[rev]
558 558 return getattr(obj, attr)
559 559
560 560 @reraise_safe_exceptions
561 561 def set_refs(self, wire, key, value):
562 562 repo = self._factory.repo(wire)
563 563 repo.refs[key] = value
564 564
565 565 @reraise_safe_exceptions
566 566 def remove_ref(self, wire, key):
567 567 repo = self._factory.repo(wire)
568 568 del repo.refs[key]
569 569
570 570 @reraise_safe_exceptions
571 571 def tree_changes(self, wire, source_id, target_id):
572 572 repo = self._factory.repo(wire)
573 573 source = repo[source_id].tree if source_id else None
574 574 target = repo[target_id].tree
575 575 result = repo.object_store.tree_changes(source, target)
576 576 return list(result)
577 577
578 578 @reraise_safe_exceptions
579 579 def tree_items(self, wire, tree_id):
580 580 repo = self._factory.repo(wire)
581 581 tree = repo[tree_id]
582 582
583 583 result = []
584 584 for item in tree.iteritems():
585 585 item_sha = item.sha
586 586 item_mode = item.mode
587 587
588 588 if FILE_MODE(item_mode) == GIT_LINK:
589 589 item_type = "link"
590 590 else:
591 591 item_type = repo[item_sha].type_name
592 592
593 593 result.append((item.path, item_mode, item_sha, item_type))
594 594 return result
595 595
596 596 @reraise_safe_exceptions
597 597 def update_server_info(self, wire):
598 598 repo = self._factory.repo(wire)
599 599 update_server_info(repo)
600 600
601 601 @reraise_safe_exceptions
602 602 def discover_git_version(self):
603 603 stdout, _ = self.run_git_command(
604 604 {}, ['--version'], _bare=True, _safe=True)
605 605 prefix = 'git version'
606 606 if stdout.startswith(prefix):
607 607 stdout = stdout[len(prefix):]
608 608 return stdout.strip()
609 609
610 610 @reraise_safe_exceptions
611 611 def run_git_command(self, wire, cmd, **opts):
612 612 path = wire.get('path', None)
613 613
614 614 if path and os.path.isdir(path):
615 615 opts['cwd'] = path
616 616
617 617 if '_bare' in opts:
618 618 _copts = []
619 619 del opts['_bare']
620 620 else:
621 621 _copts = ['-c', 'core.quotepath=false', ]
622 622 safe_call = False
623 623 if '_safe' in opts:
624 624 # no exc on failure
625 625 del opts['_safe']
626 626 safe_call = True
627 627
628 628 gitenv = os.environ.copy()
629 629 gitenv.update(opts.pop('extra_env', {}))
630 630 # need to clean fix GIT_DIR !
631 631 if 'GIT_DIR' in gitenv:
632 632 del gitenv['GIT_DIR']
633 633 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
634 634
635 635 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
636 636
637 637 try:
638 638 _opts = {'env': gitenv, 'shell': False}
639 639 _opts.update(opts)
640 640 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
641 641
642 642 return ''.join(p), ''.join(p.error)
643 643 except (EnvironmentError, OSError) as err:
644 644 cmd = ' '.join(cmd) # human friendly CMD
645 645 tb_err = ("Couldn't run git command (%s).\n"
646 646 "Original error was:%s\n" % (cmd, err))
647 647 log.exception(tb_err)
648 648 if safe_call:
649 649 return '', err
650 650 else:
651 651 raise exceptions.VcsException(tb_err)
652 652
653 653
654 654 def str_to_dulwich(value):
655 655 """
656 656 Dulwich 0.10.1a requires `unicode` objects to be passed in.
657 657 """
658 658 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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,758 +1,758 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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 httppeer, 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):
78 78 raise_from_original(exceptions.AbortException)
79 79 except RepoLookupError:
80 80 raise_from_original(exceptions.LookupException)
81 81 except RequirementError:
82 82 raise_from_original(exceptions.RequirementException)
83 83 except RepoError:
84 84 raise_from_original(exceptions.VcsException)
85 85 except LookupError:
86 86 raise_from_original(exceptions.LookupException)
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)
91 91 raise
92 92 return wrapper
93 93
94 94
95 95 class MercurialFactory(RepoFactory):
96 96
97 97 def _create_config(self, config, hooks=True):
98 98 if not hooks:
99 99 hooks_to_clean = frozenset((
100 100 'changegroup.repo_size', 'preoutgoing.pre_pull',
101 101 'outgoing.pull_logger', 'prechangegroup.pre_push'))
102 102 new_config = []
103 103 for section, option, value in config:
104 104 if section == 'hooks' and option in hooks_to_clean:
105 105 continue
106 106 new_config.append((section, option, value))
107 107 config = new_config
108 108
109 109 baseui = make_ui_from_config(config)
110 110 return baseui
111 111
112 112 def _create_repo(self, wire, create):
113 113 baseui = self._create_config(wire["config"])
114 114 return localrepository(baseui, wire["path"], create)
115 115
116 116
117 117 class HgRemote(object):
118 118
119 119 def __init__(self, factory):
120 120 self._factory = factory
121 121
122 122 self._bulk_methods = {
123 123 "affected_files": self.ctx_files,
124 124 "author": self.ctx_user,
125 125 "branch": self.ctx_branch,
126 126 "children": self.ctx_children,
127 127 "date": self.ctx_date,
128 128 "message": self.ctx_description,
129 129 "parents": self.ctx_parents,
130 130 "status": self.ctx_status,
131 131 "obsolete": self.ctx_obsolete,
132 132 "phase": self.ctx_phase,
133 133 "hidden": self.ctx_hidden,
134 134 "_file_paths": self.ctx_list,
135 135 }
136 136
137 137 @reraise_safe_exceptions
138 138 def discover_hg_version(self):
139 139 from mercurial import util
140 140 return util.version()
141 141
142 142 @reraise_safe_exceptions
143 143 def archive_repo(self, archive_path, mtime, file_info, kind):
144 144 if kind == "tgz":
145 145 archiver = archival.tarit(archive_path, mtime, "gz")
146 146 elif kind == "tbz2":
147 147 archiver = archival.tarit(archive_path, mtime, "bz2")
148 148 elif kind == 'zip':
149 149 archiver = archival.zipit(archive_path, mtime)
150 150 else:
151 151 raise exceptions.ArchiveException(
152 152 'Remote does not support: "%s".' % kind)
153 153
154 154 for f_path, f_mode, f_is_link, f_content in file_info:
155 155 archiver.addfile(f_path, f_mode, f_is_link, f_content)
156 156 archiver.done()
157 157
158 158 @reraise_safe_exceptions
159 159 def bookmarks(self, wire):
160 160 repo = self._factory.repo(wire)
161 161 return dict(repo._bookmarks)
162 162
163 163 @reraise_safe_exceptions
164 164 def branches(self, wire, normal, closed):
165 165 repo = self._factory.repo(wire)
166 166 iter_branches = repo.branchmap().iterbranches()
167 167 bt = {}
168 168 for branch_name, _heads, tip, is_closed in iter_branches:
169 169 if normal and not is_closed:
170 170 bt[branch_name] = tip
171 171 if closed and is_closed:
172 172 bt[branch_name] = tip
173 173
174 174 return bt
175 175
176 176 @reraise_safe_exceptions
177 177 def bulk_request(self, wire, rev, pre_load):
178 178 result = {}
179 179 for attr in pre_load:
180 180 try:
181 181 method = self._bulk_methods[attr]
182 182 result[attr] = method(wire, rev)
183 183 except KeyError:
184 184 raise exceptions.VcsException(
185 185 'Unknown bulk attribute: "%s"' % attr)
186 186 return result
187 187
188 188 @reraise_safe_exceptions
189 189 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
190 190 baseui = self._factory._create_config(wire["config"], hooks=hooks)
191 191 clone(baseui, source, dest, noupdate=not update_after_clone)
192 192
193 193 @reraise_safe_exceptions
194 194 def commitctx(
195 195 self, wire, message, parents, commit_time, commit_timezone,
196 196 user, files, extra, removed, updated):
197 197
198 198 def _filectxfn(_repo, memctx, path):
199 199 """
200 200 Marks given path as added/changed/removed in a given _repo. This is
201 201 for internal mercurial commit function.
202 202 """
203 203
204 204 # check if this path is removed
205 205 if path in removed:
206 206 # returning None is a way to mark node for removal
207 207 return None
208 208
209 209 # check if this path is added
210 210 for node in updated:
211 211 if node['path'] == path:
212 212 return memfilectx(
213 213 _repo,
214 214 path=node['path'],
215 215 data=node['content'],
216 216 islink=False,
217 217 isexec=bool(node['mode'] & stat.S_IXUSR),
218 218 copied=False,
219 219 memctx=memctx)
220 220
221 221 raise exceptions.AbortException(
222 222 "Given path haven't been marked as added, "
223 223 "changed or removed (%s)" % path)
224 224
225 225 repo = self._factory.repo(wire)
226 226
227 227 commit_ctx = memctx(
228 228 repo=repo,
229 229 parents=parents,
230 230 text=message,
231 231 files=files,
232 232 filectxfn=_filectxfn,
233 233 user=user,
234 234 date=(commit_time, commit_timezone),
235 235 extra=extra)
236 236
237 237 n = repo.commitctx(commit_ctx)
238 238 new_id = hex(n)
239 239
240 240 return new_id
241 241
242 242 @reraise_safe_exceptions
243 243 def ctx_branch(self, wire, revision):
244 244 repo = self._factory.repo(wire)
245 245 ctx = repo[revision]
246 246 return ctx.branch()
247 247
248 248 @reraise_safe_exceptions
249 249 def ctx_children(self, wire, revision):
250 250 repo = self._factory.repo(wire)
251 251 ctx = repo[revision]
252 252 return [child.rev() for child in ctx.children()]
253 253
254 254 @reraise_safe_exceptions
255 255 def ctx_date(self, wire, revision):
256 256 repo = self._factory.repo(wire)
257 257 ctx = repo[revision]
258 258 return ctx.date()
259 259
260 260 @reraise_safe_exceptions
261 261 def ctx_description(self, wire, revision):
262 262 repo = self._factory.repo(wire)
263 263 ctx = repo[revision]
264 264 return ctx.description()
265 265
266 266 @reraise_safe_exceptions
267 267 def ctx_diff(
268 268 self, wire, revision, git=True, ignore_whitespace=True, context=3):
269 269 repo = self._factory.repo(wire)
270 270 ctx = repo[revision]
271 271 result = ctx.diff(
272 272 git=git, ignore_whitespace=ignore_whitespace, context=context)
273 273 return list(result)
274 274
275 275 @reraise_safe_exceptions
276 276 def ctx_files(self, wire, revision):
277 277 repo = self._factory.repo(wire)
278 278 ctx = repo[revision]
279 279 return ctx.files()
280 280
281 281 @reraise_safe_exceptions
282 282 def ctx_list(self, path, revision):
283 283 repo = self._factory.repo(path)
284 284 ctx = repo[revision]
285 285 return list(ctx)
286 286
287 287 @reraise_safe_exceptions
288 288 def ctx_parents(self, wire, revision):
289 289 repo = self._factory.repo(wire)
290 290 ctx = repo[revision]
291 291 return [parent.rev() for parent in ctx.parents()]
292 292
293 293 @reraise_safe_exceptions
294 294 def ctx_phase(self, wire, revision):
295 295 repo = self._factory.repo(wire)
296 296 ctx = repo[revision]
297 297 # public=0, draft=1, secret=3
298 298 return ctx.phase()
299 299
300 300 @reraise_safe_exceptions
301 301 def ctx_obsolete(self, wire, revision):
302 302 repo = self._factory.repo(wire)
303 303 ctx = repo[revision]
304 304 return ctx.obsolete()
305 305
306 306 @reraise_safe_exceptions
307 307 def ctx_hidden(self, wire, revision):
308 308 repo = self._factory.repo(wire)
309 309 ctx = repo[revision]
310 310 return ctx.hidden()
311 311
312 312 @reraise_safe_exceptions
313 313 def ctx_substate(self, wire, revision):
314 314 repo = self._factory.repo(wire)
315 315 ctx = repo[revision]
316 316 return ctx.substate
317 317
318 318 @reraise_safe_exceptions
319 319 def ctx_status(self, wire, revision):
320 320 repo = self._factory.repo(wire)
321 321 ctx = repo[revision]
322 322 status = repo[ctx.p1().node()].status(other=ctx.node())
323 323 # object of status (odd, custom named tuple in mercurial) is not
324 324 # correctly serializable, we make it a list, as the underling
325 325 # API expects this to be a list
326 326 return list(status)
327 327
328 328 @reraise_safe_exceptions
329 329 def ctx_user(self, wire, revision):
330 330 repo = self._factory.repo(wire)
331 331 ctx = repo[revision]
332 332 return ctx.user()
333 333
334 334 @reraise_safe_exceptions
335 335 def check_url(self, url, config):
336 336 _proto = None
337 337 if '+' in url[:url.find('://')]:
338 338 _proto = url[0:url.find('+')]
339 339 url = url[url.find('+') + 1:]
340 340 handlers = []
341 341 url_obj = url_parser(url)
342 342 test_uri, authinfo = url_obj.authinfo()
343 343 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
344 344 url_obj.query = obfuscate_qs(url_obj.query)
345 345
346 346 cleaned_uri = str(url_obj)
347 347 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
348 348
349 349 if authinfo:
350 350 # create a password manager
351 351 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
352 352 passmgr.add_password(*authinfo)
353 353
354 354 handlers.extend((httpbasicauthhandler(passmgr),
355 355 httpdigestauthhandler(passmgr)))
356 356
357 357 o = urllib2.build_opener(*handlers)
358 358 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
359 359 ('Accept', 'application/mercurial-0.1')]
360 360
361 361 q = {"cmd": 'between'}
362 362 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
363 363 qs = '?%s' % urllib.urlencode(q)
364 364 cu = "%s%s" % (test_uri, qs)
365 365 req = urllib2.Request(cu, None, {})
366 366
367 367 try:
368 368 log.debug("Trying to open URL %s", cleaned_uri)
369 369 resp = o.open(req)
370 370 if resp.code != 200:
371 371 raise exceptions.URLError('Return Code is not 200')
372 372 except Exception as e:
373 373 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
374 374 # means it cannot be cloned
375 375 raise exceptions.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
376 376
377 377 # now check if it's a proper hg repo, but don't do it for svn
378 378 try:
379 379 if _proto == 'svn':
380 380 pass
381 381 else:
382 382 # check for pure hg repos
383 383 log.debug(
384 384 "Verifying if URL is a Mercurial repository: %s",
385 385 cleaned_uri)
386 386 httppeer(make_ui_from_config(config), url).lookup('tip')
387 387 except Exception as e:
388 388 log.warning("URL is not a valid Mercurial repository: %s",
389 389 cleaned_uri)
390 390 raise exceptions.URLError(
391 391 "url [%s] does not look like an hg repo org_exc: %s"
392 392 % (cleaned_uri, e))
393 393
394 394 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
395 395 return True
396 396
397 397 @reraise_safe_exceptions
398 398 def diff(
399 399 self, wire, rev1, rev2, file_filter, opt_git, opt_ignorews,
400 400 context):
401 401 repo = self._factory.repo(wire)
402 402
403 403 if file_filter:
404 404 match_filter = match(file_filter[0], '', [file_filter[1]])
405 405 else:
406 406 match_filter = file_filter
407 407 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context)
408 408
409 409 try:
410 410 return "".join(patch.diff(
411 411 repo, node1=rev1, node2=rev2, match=match_filter, opts=opts))
412 412 except RepoLookupError:
413 413 raise exceptions.LookupException()
414 414
415 415 @reraise_safe_exceptions
416 416 def file_history(self, wire, revision, path, limit):
417 417 repo = self._factory.repo(wire)
418 418
419 419 ctx = repo[revision]
420 420 fctx = ctx.filectx(path)
421 421
422 422 def history_iter():
423 423 limit_rev = fctx.rev()
424 424 for obj in reversed(list(fctx.filelog())):
425 425 obj = fctx.filectx(obj)
426 426 if limit_rev >= obj.rev():
427 427 yield obj
428 428
429 429 history = []
430 430 for cnt, obj in enumerate(history_iter()):
431 431 if limit and cnt >= limit:
432 432 break
433 433 history.append(hex(obj.node()))
434 434
435 435 return [x for x in history]
436 436
437 437 @reraise_safe_exceptions
438 438 def file_history_untill(self, wire, revision, path, limit):
439 439 repo = self._factory.repo(wire)
440 440 ctx = repo[revision]
441 441 fctx = ctx.filectx(path)
442 442
443 443 file_log = list(fctx.filelog())
444 444 if limit:
445 445 # Limit to the last n items
446 446 file_log = file_log[-limit:]
447 447
448 448 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
449 449
450 450 @reraise_safe_exceptions
451 451 def fctx_annotate(self, wire, revision, path):
452 452 repo = self._factory.repo(wire)
453 453 ctx = repo[revision]
454 454 fctx = ctx.filectx(path)
455 455
456 456 result = []
457 457 for i, (a_line, content) in enumerate(fctx.annotate()):
458 458 ln_no = i + 1
459 459 sha = hex(a_line.fctx.node())
460 460 result.append((ln_no, sha, content))
461 461 return result
462 462
463 463 @reraise_safe_exceptions
464 464 def fctx_data(self, wire, revision, path):
465 465 repo = self._factory.repo(wire)
466 466 ctx = repo[revision]
467 467 fctx = ctx.filectx(path)
468 468 return fctx.data()
469 469
470 470 @reraise_safe_exceptions
471 471 def fctx_flags(self, wire, revision, path):
472 472 repo = self._factory.repo(wire)
473 473 ctx = repo[revision]
474 474 fctx = ctx.filectx(path)
475 475 return fctx.flags()
476 476
477 477 @reraise_safe_exceptions
478 478 def fctx_size(self, wire, revision, path):
479 479 repo = self._factory.repo(wire)
480 480 ctx = repo[revision]
481 481 fctx = ctx.filectx(path)
482 482 return fctx.size()
483 483
484 484 @reraise_safe_exceptions
485 485 def get_all_commit_ids(self, wire, name):
486 486 repo = self._factory.repo(wire)
487 487 revs = repo.filtered(name).changelog.index
488 488 return map(lambda x: hex(x[7]), revs)[:-1]
489 489
490 490 @reraise_safe_exceptions
491 491 def get_config_value(self, wire, section, name, untrusted=False):
492 492 repo = self._factory.repo(wire)
493 493 return repo.ui.config(section, name, untrusted=untrusted)
494 494
495 495 @reraise_safe_exceptions
496 496 def get_config_bool(self, wire, section, name, untrusted=False):
497 497 repo = self._factory.repo(wire)
498 498 return repo.ui.configbool(section, name, untrusted=untrusted)
499 499
500 500 @reraise_safe_exceptions
501 501 def get_config_list(self, wire, section, name, untrusted=False):
502 502 repo = self._factory.repo(wire)
503 503 return repo.ui.configlist(section, name, untrusted=untrusted)
504 504
505 505 @reraise_safe_exceptions
506 506 def is_large_file(self, wire, path):
507 507 return largefiles.lfutil.isstandin(path)
508 508
509 509 @reraise_safe_exceptions
510 510 def in_largefiles_store(self, wire, sha):
511 511 repo = self._factory.repo(wire)
512 512 return largefiles.lfutil.instore(repo, sha)
513 513
514 514 @reraise_safe_exceptions
515 515 def in_user_cache(self, wire, sha):
516 516 repo = self._factory.repo(wire)
517 517 return largefiles.lfutil.inusercache(repo.ui, sha)
518 518
519 519 @reraise_safe_exceptions
520 520 def store_path(self, wire, sha):
521 521 repo = self._factory.repo(wire)
522 522 return largefiles.lfutil.storepath(repo, sha)
523 523
524 524 @reraise_safe_exceptions
525 525 def link(self, wire, sha, path):
526 526 repo = self._factory.repo(wire)
527 527 largefiles.lfutil.link(
528 528 largefiles.lfutil.usercachepath(repo.ui, sha), path)
529 529
530 530 @reraise_safe_exceptions
531 531 def localrepository(self, wire, create=False):
532 532 self._factory.repo(wire, create=create)
533 533
534 534 @reraise_safe_exceptions
535 535 def lookup(self, wire, revision, both):
536 536 # TODO Paris: Ugly hack to "deserialize" long for msgpack
537 537 if isinstance(revision, float):
538 538 revision = long(revision)
539 539 repo = self._factory.repo(wire)
540 540 try:
541 541 ctx = repo[revision]
542 542 except RepoLookupError:
543 543 raise exceptions.LookupException(revision)
544 544 except LookupError as e:
545 545 raise exceptions.LookupException(e.name)
546 546
547 547 if not both:
548 548 return ctx.hex()
549 549
550 550 ctx = repo[ctx.hex()]
551 551 return ctx.hex(), ctx.rev()
552 552
553 553 @reraise_safe_exceptions
554 554 def pull(self, wire, url, commit_ids=None):
555 555 repo = self._factory.repo(wire)
556 556 remote = peer(repo, {}, url)
557 557 if commit_ids:
558 558 commit_ids = [bin(commit_id) for commit_id in commit_ids]
559 559
560 560 return exchange.pull(
561 561 repo, remote, heads=commit_ids, force=None).cgresult
562 562
563 563 @reraise_safe_exceptions
564 564 def sync_push(self, wire, url):
565 565 if self.check_url(url, wire['config']):
566 566 repo = self._factory.repo(wire)
567 567 bookmarks = dict(repo._bookmarks).keys()
568 568 remote = peer(repo, {}, url)
569 569 return exchange.push(
570 570 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
571 571
572 572 @reraise_safe_exceptions
573 573 def revision(self, wire, rev):
574 574 repo = self._factory.repo(wire)
575 575 ctx = repo[rev]
576 576 return ctx.rev()
577 577
578 578 @reraise_safe_exceptions
579 579 def rev_range(self, wire, filter):
580 580 repo = self._factory.repo(wire)
581 581 revisions = [rev for rev in revrange(repo, filter)]
582 582 return revisions
583 583
584 584 @reraise_safe_exceptions
585 585 def rev_range_hash(self, wire, node):
586 586 repo = self._factory.repo(wire)
587 587
588 588 def get_revs(repo, rev_opt):
589 589 if rev_opt:
590 590 revs = revrange(repo, rev_opt)
591 591 if len(revs) == 0:
592 592 return (nullrev, nullrev)
593 593 return max(revs), min(revs)
594 594 else:
595 595 return len(repo) - 1, 0
596 596
597 597 stop, start = get_revs(repo, [node + ':'])
598 598 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
599 599 return revs
600 600
601 601 @reraise_safe_exceptions
602 602 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
603 603 other_path = kwargs.pop('other_path', None)
604 604
605 605 # case when we want to compare two independent repositories
606 606 if other_path and other_path != wire["path"]:
607 607 baseui = self._factory._create_config(wire["config"])
608 608 repo = unionrepo.unionrepository(baseui, other_path, wire["path"])
609 609 else:
610 610 repo = self._factory.repo(wire)
611 611 return list(repo.revs(rev_spec, *args))
612 612
613 613 @reraise_safe_exceptions
614 614 def strip(self, wire, revision, update, backup):
615 615 repo = self._factory.repo(wire)
616 616 ctx = repo[revision]
617 617 hgext_strip(
618 618 repo.baseui, repo, ctx.node(), update=update, backup=backup)
619 619
620 620 @reraise_safe_exceptions
621 621 def verify(self, wire,):
622 622 repo = self._factory.repo(wire)
623 623 baseui = self._factory._create_config(wire['config'])
624 624 baseui.setconfig('ui', 'quiet', 'false')
625 625 output = io.BytesIO()
626 626
627 627 def write(data, **unused_kwargs):
628 628 output.write(data)
629 629 baseui.write = write
630 630
631 631 repo.ui = baseui
632 632 verify.verify(repo)
633 633 return output.getvalue()
634 634
635 635 @reraise_safe_exceptions
636 636 def tag(self, wire, name, revision, message, local, user,
637 637 tag_time, tag_timezone):
638 638 repo = self._factory.repo(wire)
639 639 ctx = repo[revision]
640 640 node = ctx.node()
641 641
642 642 date = (tag_time, tag_timezone)
643 643 try:
644 644 hg_tag.tag(repo, name, node, message, local, user, date)
645 645 except Abort as e:
646 646 log.exception("Tag operation aborted")
647 647 # Exception can contain unicode which we convert
648 648 raise exceptions.AbortException(repr(e))
649 649
650 650 @reraise_safe_exceptions
651 651 def tags(self, wire):
652 652 repo = self._factory.repo(wire)
653 653 return repo.tags()
654 654
655 655 @reraise_safe_exceptions
656 656 def update(self, wire, node=None, clean=False):
657 657 repo = self._factory.repo(wire)
658 658 baseui = self._factory._create_config(wire['config'])
659 659 commands.update(baseui, repo, node=node, clean=clean)
660 660
661 661 @reraise_safe_exceptions
662 662 def identify(self, wire):
663 663 repo = self._factory.repo(wire)
664 664 baseui = self._factory._create_config(wire['config'])
665 665 output = io.BytesIO()
666 666 baseui.write = output.write
667 667 # This is required to get a full node id
668 668 baseui.debugflag = True
669 669 commands.identify(baseui, repo, id=True)
670 670
671 671 return output.getvalue()
672 672
673 673 @reraise_safe_exceptions
674 674 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None,
675 675 hooks=True):
676 676 repo = self._factory.repo(wire)
677 677 baseui = self._factory._create_config(wire['config'], hooks=hooks)
678 678
679 679 # Mercurial internally has a lot of logic that checks ONLY if
680 680 # option is defined, we just pass those if they are defined then
681 681 opts = {}
682 682 if bookmark:
683 683 opts['bookmark'] = bookmark
684 684 if branch:
685 685 opts['branch'] = branch
686 686 if revision:
687 687 opts['rev'] = revision
688 688
689 689 commands.pull(baseui, repo, source, **opts)
690 690
691 691 @reraise_safe_exceptions
692 692 def heads(self, wire, branch=None):
693 693 repo = self._factory.repo(wire)
694 694 baseui = self._factory._create_config(wire['config'])
695 695 output = io.BytesIO()
696 696
697 697 def write(data, **unused_kwargs):
698 698 output.write(data)
699 699
700 700 baseui.write = write
701 701 if branch:
702 702 args = [branch]
703 703 else:
704 704 args = []
705 705 commands.heads(baseui, repo, template='{node} ', *args)
706 706
707 707 return output.getvalue()
708 708
709 709 @reraise_safe_exceptions
710 710 def ancestor(self, wire, revision1, revision2):
711 711 repo = self._factory.repo(wire)
712 712 changelog = repo.changelog
713 713 lookup = repo.lookup
714 714 a = changelog.ancestor(lookup(revision1), lookup(revision2))
715 715 return hex(a)
716 716
717 717 @reraise_safe_exceptions
718 718 def push(self, wire, revisions, dest_path, hooks=True,
719 719 push_branches=False):
720 720 repo = self._factory.repo(wire)
721 721 baseui = self._factory._create_config(wire['config'], hooks=hooks)
722 722 commands.push(baseui, repo, dest=dest_path, rev=revisions,
723 723 new_branch=push_branches)
724 724
725 725 @reraise_safe_exceptions
726 726 def merge(self, wire, revision):
727 727 repo = self._factory.repo(wire)
728 728 baseui = self._factory._create_config(wire['config'])
729 729 repo.ui.setconfig('ui', 'merge', 'internal:dump')
730 730
731 731 # In case of sub repositories are used mercurial prompts the user in
732 732 # case of merge conflicts or different sub repository sources. By
733 733 # setting the interactive flag to `False` mercurial doesn't prompt the
734 734 # used but instead uses a default value.
735 735 repo.ui.setconfig('ui', 'interactive', False)
736 736
737 737 commands.merge(baseui, repo, rev=revision)
738 738
739 739 @reraise_safe_exceptions
740 740 def commit(self, wire, message, username, close_branch=False):
741 741 repo = self._factory.repo(wire)
742 742 baseui = self._factory._create_config(wire['config'])
743 743 repo.ui.setconfig('ui', 'username', username)
744 744 commands.commit(baseui, repo, message=message, close_branch=close_branch)
745 745
746 746 @reraise_safe_exceptions
747 747 def rebase(self, wire, source=None, dest=None, abort=False):
748 748 repo = self._factory.repo(wire)
749 749 baseui = self._factory._create_config(wire['config'])
750 750 repo.ui.setconfig('ui', 'merge', 'internal:dump')
751 751 rebase.rebase(
752 752 baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
753 753
754 754 @reraise_safe_exceptions
755 755 def bookmark(self, wire, bookmark, revision=None):
756 756 repo = self._factory.repo(wire)
757 757 baseui = self._factory._create_config(wire['config'])
758 758 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
@@ -1,63 +1,63 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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 httppeer
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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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(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 else:
56 56 logger.debug('Extension largefiles disabled')
57 57 calc_capabilities = lfproto.capabilitiesorig
58 58 return calc_capabilities(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):
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,482 +1,482 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2017 RodeCode GmbH
4 # Copyright (C) 2014-2018 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 json
24 24 import logging
25 25 import collections
26 26 import importlib
27 27 import subprocess
28 28
29 29 from httplib import HTTPConnection
30 30
31 31
32 32 import mercurial.scmutil
33 33 import mercurial.node
34 34 import simplejson as json
35 35
36 36 from vcsserver import exceptions
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class HooksHttpClient(object):
42 42 connection = None
43 43
44 44 def __init__(self, hooks_uri):
45 45 self.hooks_uri = hooks_uri
46 46
47 47 def __call__(self, method, extras):
48 48 connection = HTTPConnection(self.hooks_uri)
49 49 body = self._serialize(method, extras)
50 50 connection.request('POST', '/', body)
51 51 response = connection.getresponse()
52 52 return json.loads(response.read())
53 53
54 54 def _serialize(self, hook_name, extras):
55 55 data = {
56 56 'method': hook_name,
57 57 'extras': extras
58 58 }
59 59 return json.dumps(data)
60 60
61 61
62 62 class HooksDummyClient(object):
63 63 def __init__(self, hooks_module):
64 64 self._hooks_module = importlib.import_module(hooks_module)
65 65
66 66 def __call__(self, hook_name, extras):
67 67 with self._hooks_module.Hooks() as hooks:
68 68 return getattr(hooks, hook_name)(extras)
69 69
70 70
71 71 class RemoteMessageWriter(object):
72 72 """Writer base class."""
73 73 def write(self, message):
74 74 raise NotImplementedError()
75 75
76 76
77 77 class HgMessageWriter(RemoteMessageWriter):
78 78 """Writer that knows how to send messages to mercurial clients."""
79 79
80 80 def __init__(self, ui):
81 81 self.ui = ui
82 82
83 83 def write(self, message):
84 84 # TODO: Check why the quiet flag is set by default.
85 85 old = self.ui.quiet
86 86 self.ui.quiet = False
87 87 self.ui.status(message.encode('utf-8'))
88 88 self.ui.quiet = old
89 89
90 90
91 91 class GitMessageWriter(RemoteMessageWriter):
92 92 """Writer that knows how to send messages to git clients."""
93 93
94 94 def __init__(self, stdout=None):
95 95 self.stdout = stdout or sys.stdout
96 96
97 97 def write(self, message):
98 98 self.stdout.write(message.encode('utf-8'))
99 99
100 100
101 101 def _handle_exception(result):
102 102 exception_class = result.get('exception')
103 103 exception_traceback = result.get('exception_traceback')
104 104
105 105 if exception_traceback:
106 106 log.error('Got traceback from remote call:%s', exception_traceback)
107 107
108 108 if exception_class == 'HTTPLockedRC':
109 109 raise exceptions.RepositoryLockedException(*result['exception_args'])
110 110 elif exception_class == 'RepositoryError':
111 111 raise exceptions.VcsException(*result['exception_args'])
112 112 elif exception_class:
113 113 raise Exception('Got remote exception "%s" with args "%s"' %
114 114 (exception_class, result['exception_args']))
115 115
116 116
117 117 def _get_hooks_client(extras):
118 118 if 'hooks_uri' in extras:
119 119 protocol = extras.get('hooks_protocol')
120 120 return HooksHttpClient(extras['hooks_uri'])
121 121 else:
122 122 return HooksDummyClient(extras['hooks_module'])
123 123
124 124
125 125 def _call_hook(hook_name, extras, writer):
126 126 hooks = _get_hooks_client(extras)
127 127 result = hooks(hook_name, extras)
128 128 log.debug('Hooks got result: %s', result)
129 129 writer.write(result['output'])
130 130 _handle_exception(result)
131 131
132 132 return result['status']
133 133
134 134
135 135 def _extras_from_ui(ui):
136 136 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
137 137 if not hook_data:
138 138 # maybe it's inside environ ?
139 139 env_hook_data = os.environ.get('RC_SCM_DATA')
140 140 if env_hook_data:
141 141 hook_data = env_hook_data
142 142
143 143 extras = {}
144 144 if hook_data:
145 145 extras = json.loads(hook_data)
146 146 return extras
147 147
148 148
149 149 def _rev_range_hash(repo, node):
150 150
151 151 commits = []
152 152 for rev in xrange(repo[node], len(repo)):
153 153 ctx = repo[rev]
154 154 commit_id = mercurial.node.hex(ctx.node())
155 155 branch = ctx.branch()
156 156 commits.append((commit_id, branch))
157 157
158 158 return commits
159 159
160 160
161 161 def repo_size(ui, repo, **kwargs):
162 162 extras = _extras_from_ui(ui)
163 163 return _call_hook('repo_size', extras, HgMessageWriter(ui))
164 164
165 165
166 166 def pre_pull(ui, repo, **kwargs):
167 167 extras = _extras_from_ui(ui)
168 168 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
169 169
170 170
171 171 def pre_pull_ssh(ui, repo, **kwargs):
172 172 extras = _extras_from_ui(ui)
173 173 if extras and extras.get('SSH'):
174 174 return pre_pull(ui, repo, **kwargs)
175 175 return 0
176 176
177 177
178 178 def post_pull(ui, repo, **kwargs):
179 179 extras = _extras_from_ui(ui)
180 180 return _call_hook('post_pull', extras, HgMessageWriter(ui))
181 181
182 182
183 183 def post_pull_ssh(ui, repo, **kwargs):
184 184 extras = _extras_from_ui(ui)
185 185 if extras and extras.get('SSH'):
186 186 return post_pull(ui, repo, **kwargs)
187 187 return 0
188 188
189 189
190 190 def pre_push(ui, repo, node=None, **kwargs):
191 191 extras = _extras_from_ui(ui)
192 192
193 193 rev_data = []
194 194 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
195 195 branches = collections.defaultdict(list)
196 196 for commit_id, branch in _rev_range_hash(repo, node):
197 197 branches[branch].append(commit_id)
198 198
199 199 for branch, commits in branches.iteritems():
200 200 old_rev = kwargs.get('node_last') or commits[0]
201 201 rev_data.append({
202 202 'old_rev': old_rev,
203 203 'new_rev': commits[-1],
204 204 'ref': '',
205 205 'type': 'branch',
206 206 'name': branch,
207 207 })
208 208
209 209 extras['commit_ids'] = rev_data
210 210 return _call_hook('pre_push', extras, HgMessageWriter(ui))
211 211
212 212
213 213 def pre_push_ssh(ui, repo, node=None, **kwargs):
214 214 if _extras_from_ui(ui).get('SSH'):
215 215 return pre_push(ui, repo, node, **kwargs)
216 216
217 217 return 0
218 218
219 219
220 220 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
221 221 extras = _extras_from_ui(ui)
222 222 if extras.get('SSH'):
223 223 permission = extras['SSH_PERMISSIONS']
224 224
225 225 if 'repository.write' == permission or 'repository.admin' == permission:
226 226 return 0
227 227
228 228 # non-zero ret code
229 229 return 1
230 230
231 231 return 0
232 232
233 233
234 234 def post_push(ui, repo, node, **kwargs):
235 235 extras = _extras_from_ui(ui)
236 236
237 237 commit_ids = []
238 238 branches = []
239 239 bookmarks = []
240 240 tags = []
241 241
242 242 for commit_id, branch in _rev_range_hash(repo, node):
243 243 commit_ids.append(commit_id)
244 244 if branch not in branches:
245 245 branches.append(branch)
246 246
247 247 if hasattr(ui, '_rc_pushkey_branches'):
248 248 bookmarks = ui._rc_pushkey_branches
249 249
250 250 extras['commit_ids'] = commit_ids
251 251 extras['new_refs'] = {
252 252 'branches': branches,
253 253 'bookmarks': bookmarks,
254 254 'tags': tags
255 255 }
256 256
257 257 return _call_hook('post_push', extras, HgMessageWriter(ui))
258 258
259 259
260 260 def post_push_ssh(ui, repo, node, **kwargs):
261 261 if _extras_from_ui(ui).get('SSH'):
262 262 return post_push(ui, repo, node, **kwargs)
263 263 return 0
264 264
265 265
266 266 def key_push(ui, repo, **kwargs):
267 267 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
268 268 # store new bookmarks in our UI object propagated later to post_push
269 269 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
270 270 return
271 271
272 272
273 273 # backward compat
274 274 log_pull_action = post_pull
275 275
276 276 # backward compat
277 277 log_push_action = post_push
278 278
279 279
280 280 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
281 281 """
282 282 Old hook name: keep here for backward compatibility.
283 283
284 284 This is only required when the installed git hooks are not upgraded.
285 285 """
286 286 pass
287 287
288 288
289 289 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
290 290 """
291 291 Old hook name: keep here for backward compatibility.
292 292
293 293 This is only required when the installed git hooks are not upgraded.
294 294 """
295 295 pass
296 296
297 297
298 298 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
299 299
300 300
301 301 def git_pre_pull(extras):
302 302 """
303 303 Pre pull hook.
304 304
305 305 :param extras: dictionary containing the keys defined in simplevcs
306 306 :type extras: dict
307 307
308 308 :return: status code of the hook. 0 for success.
309 309 :rtype: int
310 310 """
311 311 if 'pull' not in extras['hooks']:
312 312 return HookResponse(0, '')
313 313
314 314 stdout = io.BytesIO()
315 315 try:
316 316 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
317 317 except Exception as error:
318 318 status = 128
319 319 stdout.write('ERROR: %s\n' % str(error))
320 320
321 321 return HookResponse(status, stdout.getvalue())
322 322
323 323
324 324 def git_post_pull(extras):
325 325 """
326 326 Post pull hook.
327 327
328 328 :param extras: dictionary containing the keys defined in simplevcs
329 329 :type extras: dict
330 330
331 331 :return: status code of the hook. 0 for success.
332 332 :rtype: int
333 333 """
334 334 if 'pull' not in extras['hooks']:
335 335 return HookResponse(0, '')
336 336
337 337 stdout = io.BytesIO()
338 338 try:
339 339 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
340 340 except Exception as error:
341 341 status = 128
342 342 stdout.write('ERROR: %s\n' % error)
343 343
344 344 return HookResponse(status, stdout.getvalue())
345 345
346 346
347 347 def _parse_git_ref_lines(revision_lines):
348 348 rev_data = []
349 349 for revision_line in revision_lines or []:
350 350 old_rev, new_rev, ref = revision_line.strip().split(' ')
351 351 ref_data = ref.split('/', 2)
352 352 if ref_data[1] in ('tags', 'heads'):
353 353 rev_data.append({
354 354 'old_rev': old_rev,
355 355 'new_rev': new_rev,
356 356 'ref': ref,
357 357 'type': ref_data[1],
358 358 'name': ref_data[2],
359 359 })
360 360 return rev_data
361 361
362 362
363 363 def git_pre_receive(unused_repo_path, revision_lines, env):
364 364 """
365 365 Pre push hook.
366 366
367 367 :param extras: dictionary containing the keys defined in simplevcs
368 368 :type extras: dict
369 369
370 370 :return: status code of the hook. 0 for success.
371 371 :rtype: int
372 372 """
373 373 extras = json.loads(env['RC_SCM_DATA'])
374 374 rev_data = _parse_git_ref_lines(revision_lines)
375 375 if 'push' not in extras['hooks']:
376 376 return 0
377 377 extras['commit_ids'] = rev_data
378 378 return _call_hook('pre_push', extras, GitMessageWriter())
379 379
380 380
381 381 def _run_command(arguments):
382 382 """
383 383 Run the specified command and return the stdout.
384 384
385 385 :param arguments: sequence of program arguments (including the program name)
386 386 :type arguments: list[str]
387 387 """
388 388 # TODO(skreft): refactor this method and all the other similar ones.
389 389 # Probably this should be using subprocessio.
390 390 process = subprocess.Popen(
391 391 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
392 392 stdout, stderr = process.communicate()
393 393
394 394 if process.returncode != 0:
395 395 raise Exception(
396 396 'Command %s exited with exit code %s: stderr:%s' % (
397 397 arguments, process.returncode, stderr))
398 398
399 399 return stdout
400 400
401 401
402 402 def git_post_receive(unused_repo_path, revision_lines, env):
403 403 """
404 404 Post push hook.
405 405
406 406 :param extras: dictionary containing the keys defined in simplevcs
407 407 :type extras: dict
408 408
409 409 :return: status code of the hook. 0 for success.
410 410 :rtype: int
411 411 """
412 412 extras = json.loads(env['RC_SCM_DATA'])
413 413 if 'push' not in extras['hooks']:
414 414 return 0
415 415
416 416 rev_data = _parse_git_ref_lines(revision_lines)
417 417
418 418 git_revs = []
419 419
420 420 # N.B.(skreft): it is ok to just call git, as git before calling a
421 421 # subcommand sets the PATH environment variable so that it point to the
422 422 # correct version of the git executable.
423 423 empty_commit_id = '0' * 40
424 424 branches = []
425 425 tags = []
426 426 for push_ref in rev_data:
427 427 type_ = push_ref['type']
428 428
429 429 if type_ == 'heads':
430 430 if push_ref['old_rev'] == empty_commit_id:
431 431 # starting new branch case
432 432 if push_ref['name'] not in branches:
433 433 branches.append(push_ref['name'])
434 434
435 435 # Fix up head revision if needed
436 436 cmd = ['git', 'show', 'HEAD']
437 437 try:
438 438 _run_command(cmd)
439 439 except Exception:
440 440 cmd = ['git', 'symbolic-ref', 'HEAD',
441 441 'refs/heads/%s' % push_ref['name']]
442 442 print("Setting default branch to %s" % push_ref['name'])
443 443 _run_command(cmd)
444 444
445 445 cmd = ['git', 'for-each-ref', '--format=%(refname)',
446 446 'refs/heads/*']
447 447 heads = _run_command(cmd)
448 448 heads = heads.replace(push_ref['ref'], '')
449 449 heads = ' '.join(head for head in heads.splitlines() if head)
450 450 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
451 451 '--', push_ref['new_rev'], '--not', heads]
452 452 git_revs.extend(_run_command(cmd).splitlines())
453 453 elif push_ref['new_rev'] == empty_commit_id:
454 454 # delete branch case
455 455 git_revs.append('delete_branch=>%s' % push_ref['name'])
456 456 else:
457 457 if push_ref['name'] not in branches:
458 458 branches.append(push_ref['name'])
459 459
460 460 cmd = ['git', 'log',
461 461 '{old_rev}..{new_rev}'.format(**push_ref),
462 462 '--reverse', '--pretty=format:%H']
463 463 git_revs.extend(_run_command(cmd).splitlines())
464 464 elif type_ == 'tags':
465 465 if push_ref['name'] not in tags:
466 466 tags.append(push_ref['name'])
467 467 git_revs.append('tag=>%s' % push_ref['name'])
468 468
469 469 extras['commit_ids'] = git_revs
470 470 extras['new_refs'] = {
471 471 'branches': branches,
472 472 'bookmarks': [],
473 473 'tags': tags,
474 474 }
475 475
476 476 if 'repo_size' in extras['hooks']:
477 477 try:
478 478 _call_hook('repo_size', extras, GitMessageWriter())
479 479 except:
480 480 pass
481 481
482 482 return _call_hook('post_push', extras, GitMessageWriter())
@@ -1,476 +1,476 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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 base64
19 19 import locale
20 20 import logging
21 21 import uuid
22 22 import wsgiref.util
23 23 import traceback
24 24 from itertools import chain
25 25
26 26 import simplejson as json
27 27 import msgpack
28 28 from beaker.cache import CacheManager
29 29 from beaker.util import parse_cache_config_options
30 30 from pyramid.config import Configurator
31 31 from pyramid.wsgi import wsgiapp
32 32
33 33 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
34 34 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
35 35 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
36 36 from vcsserver.echo_stub.echo_app import EchoApp
37 37 from vcsserver.exceptions import HTTPRepoLocked
38 38 from vcsserver.server import VcsServer
39 39
40 40 try:
41 41 from vcsserver.git import GitFactory, GitRemote
42 42 except ImportError:
43 43 GitFactory = None
44 44 GitRemote = None
45 45
46 46 try:
47 47 from vcsserver.hg import MercurialFactory, HgRemote
48 48 except ImportError:
49 49 MercurialFactory = None
50 50 HgRemote = None
51 51
52 52 try:
53 53 from vcsserver.svn import SubversionFactory, SvnRemote
54 54 except ImportError:
55 55 SubversionFactory = None
56 56 SvnRemote = None
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 def _is_request_chunked(environ):
62 62 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
63 63 return stream
64 64
65 65
66 66 class VCS(object):
67 67 def __init__(self, locale=None, cache_config=None):
68 68 self.locale = locale
69 69 self.cache_config = cache_config
70 70 self._configure_locale()
71 71 self._initialize_cache()
72 72
73 73 if GitFactory and GitRemote:
74 74 git_repo_cache = self.cache.get_cache_region(
75 75 'git', region='repo_object')
76 76 git_factory = GitFactory(git_repo_cache)
77 77 self._git_remote = GitRemote(git_factory)
78 78 else:
79 79 log.info("Git client import failed")
80 80
81 81 if MercurialFactory and HgRemote:
82 82 hg_repo_cache = self.cache.get_cache_region(
83 83 'hg', region='repo_object')
84 84 hg_factory = MercurialFactory(hg_repo_cache)
85 85 self._hg_remote = HgRemote(hg_factory)
86 86 else:
87 87 log.info("Mercurial client import failed")
88 88
89 89 if SubversionFactory and SvnRemote:
90 90 svn_repo_cache = self.cache.get_cache_region(
91 91 'svn', region='repo_object')
92 92 svn_factory = SubversionFactory(svn_repo_cache)
93 93 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
94 94 else:
95 95 log.info("Subversion client import failed")
96 96
97 97 self._vcsserver = VcsServer()
98 98
99 99 def _initialize_cache(self):
100 100 cache_config = parse_cache_config_options(self.cache_config)
101 101 log.info('Initializing beaker cache: %s' % cache_config)
102 102 self.cache = CacheManager(**cache_config)
103 103
104 104 def _configure_locale(self):
105 105 if self.locale:
106 106 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
107 107 else:
108 108 log.info(
109 109 'Configuring locale subsystem based on environment variables')
110 110 try:
111 111 # If self.locale is the empty string, then the locale
112 112 # module will use the environment variables. See the
113 113 # documentation of the package `locale`.
114 114 locale.setlocale(locale.LC_ALL, self.locale)
115 115
116 116 language_code, encoding = locale.getlocale()
117 117 log.info(
118 118 'Locale set to language code "%s" with encoding "%s".',
119 119 language_code, encoding)
120 120 except locale.Error:
121 121 log.exception(
122 122 'Cannot set locale, not configuring the locale system')
123 123
124 124
125 125 class WsgiProxy(object):
126 126 def __init__(self, wsgi):
127 127 self.wsgi = wsgi
128 128
129 129 def __call__(self, environ, start_response):
130 130 input_data = environ['wsgi.input'].read()
131 131 input_data = msgpack.unpackb(input_data)
132 132
133 133 error = None
134 134 try:
135 135 data, status, headers = self.wsgi.handle(
136 136 input_data['environment'], input_data['input_data'],
137 137 *input_data['args'], **input_data['kwargs'])
138 138 except Exception as e:
139 139 data, status, headers = [], None, None
140 140 error = {
141 141 'message': str(e),
142 142 '_vcs_kind': getattr(e, '_vcs_kind', None)
143 143 }
144 144
145 145 start_response(200, {})
146 146 return self._iterator(error, status, headers, data)
147 147
148 148 def _iterator(self, error, status, headers, data):
149 149 initial_data = [
150 150 error,
151 151 status,
152 152 headers,
153 153 ]
154 154
155 155 for d in chain(initial_data, data):
156 156 yield msgpack.packb(d)
157 157
158 158
159 159 class HTTPApplication(object):
160 160 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
161 161
162 162 remote_wsgi = remote_wsgi
163 163 _use_echo_app = False
164 164
165 165 def __init__(self, settings=None, global_config=None):
166 166 self.config = Configurator(settings=settings)
167 167 self.global_config = global_config
168 168
169 169 locale = settings.get('locale', '') or 'en_US.UTF-8'
170 170 vcs = VCS(locale=locale, cache_config=settings)
171 171 self._remotes = {
172 172 'hg': vcs._hg_remote,
173 173 'git': vcs._git_remote,
174 174 'svn': vcs._svn_remote,
175 175 'server': vcs._vcsserver,
176 176 }
177 177 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
178 178 self._use_echo_app = True
179 179 log.warning("Using EchoApp for VCS operations.")
180 180 self.remote_wsgi = remote_wsgi_stub
181 181 self._configure_settings(settings)
182 182 self._configure()
183 183
184 184 def _configure_settings(self, app_settings):
185 185 """
186 186 Configure the settings module.
187 187 """
188 188 git_path = app_settings.get('git_path', None)
189 189 if git_path:
190 190 settings.GIT_EXECUTABLE = git_path
191 191
192 192 def _configure(self):
193 193 self.config.add_renderer(
194 194 name='msgpack',
195 195 factory=self._msgpack_renderer_factory)
196 196
197 197 self.config.add_route('service', '/_service')
198 198 self.config.add_route('status', '/status')
199 199 self.config.add_route('hg_proxy', '/proxy/hg')
200 200 self.config.add_route('git_proxy', '/proxy/git')
201 201 self.config.add_route('vcs', '/{backend}')
202 202 self.config.add_route('stream_git', '/stream/git/*repo_name')
203 203 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
204 204
205 205 self.config.add_view(
206 206 self.status_view, route_name='status', renderer='json')
207 207 self.config.add_view(
208 208 self.service_view, route_name='service', renderer='msgpack')
209 209
210 210 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
211 211 self.config.add_view(self.git_proxy(), route_name='git_proxy')
212 212 self.config.add_view(
213 213 self.vcs_view, route_name='vcs', renderer='msgpack',
214 214 custom_predicates=[self.is_vcs_view])
215 215
216 216 self.config.add_view(self.hg_stream(), route_name='stream_hg')
217 217 self.config.add_view(self.git_stream(), route_name='stream_git')
218 218
219 219 def notfound(request):
220 220 return {'status': '404 NOT FOUND'}
221 221 self.config.add_notfound_view(notfound, renderer='json')
222 222
223 223 self.config.add_view(self.handle_vcs_exception, context=Exception)
224 224
225 225 self.config.add_tween(
226 226 'vcsserver.tweens.RequestWrapperTween',
227 227 )
228 228
229 229 def wsgi_app(self):
230 230 return self.config.make_wsgi_app()
231 231
232 232 def vcs_view(self, request):
233 233 remote = self._remotes[request.matchdict['backend']]
234 234 payload = msgpack.unpackb(request.body, use_list=True)
235 235 method = payload.get('method')
236 236 params = payload.get('params')
237 237 wire = params.get('wire')
238 238 args = params.get('args')
239 239 kwargs = params.get('kwargs')
240 240 if wire:
241 241 try:
242 242 wire['context'] = uuid.UUID(wire['context'])
243 243 except KeyError:
244 244 pass
245 245 args.insert(0, wire)
246 246
247 247 log.debug('method called:%s with kwargs:%s', method, kwargs)
248 248 try:
249 249 resp = getattr(remote, method)(*args, **kwargs)
250 250 except Exception as e:
251 251 tb_info = traceback.format_exc()
252 252
253 253 type_ = e.__class__.__name__
254 254 if type_ not in self.ALLOWED_EXCEPTIONS:
255 255 type_ = None
256 256
257 257 resp = {
258 258 'id': payload.get('id'),
259 259 'error': {
260 260 'message': e.message,
261 261 'traceback': tb_info,
262 262 'type': type_
263 263 }
264 264 }
265 265 try:
266 266 resp['error']['_vcs_kind'] = e._vcs_kind
267 267 except AttributeError:
268 268 pass
269 269 else:
270 270 resp = {
271 271 'id': payload.get('id'),
272 272 'result': resp
273 273 }
274 274
275 275 return resp
276 276
277 277 def status_view(self, request):
278 278 import vcsserver
279 279 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__}
280 280
281 281 def service_view(self, request):
282 282 import vcsserver
283 283 import ConfigParser as configparser
284 284
285 285 payload = msgpack.unpackb(request.body, use_list=True)
286 286
287 287 try:
288 288 path = self.global_config['__file__']
289 289 config = configparser.ConfigParser()
290 290 config.read(path)
291 291 parsed_ini = config
292 292 if parsed_ini.has_section('server:main'):
293 293 parsed_ini = dict(parsed_ini.items('server:main'))
294 294 except Exception:
295 295 log.exception('Failed to read .ini file for display')
296 296 parsed_ini = {}
297 297
298 298 resp = {
299 299 'id': payload.get('id'),
300 300 'result': dict(
301 301 version=vcsserver.__version__,
302 302 config=parsed_ini,
303 303 payload=payload,
304 304 )
305 305 }
306 306 return resp
307 307
308 308 def _msgpack_renderer_factory(self, info):
309 309 def _render(value, system):
310 310 value = msgpack.packb(value)
311 311 request = system.get('request')
312 312 if request is not None:
313 313 response = request.response
314 314 ct = response.content_type
315 315 if ct == response.default_content_type:
316 316 response.content_type = 'application/x-msgpack'
317 317 return value
318 318 return _render
319 319
320 320 def set_env_from_config(self, environ, config):
321 321 dict_conf = {}
322 322 try:
323 323 for elem in config:
324 324 if elem[0] == 'rhodecode':
325 325 dict_conf = json.loads(elem[2])
326 326 break
327 327 except Exception:
328 328 log.exception('Failed to fetch SCM CONFIG')
329 329 return
330 330
331 331 username = dict_conf.get('username')
332 332 if username:
333 333 environ['REMOTE_USER'] = username
334 334
335 335 ip = dict_conf.get('ip')
336 336 if ip:
337 337 environ['REMOTE_HOST'] = ip
338 338
339 339 if _is_request_chunked(environ):
340 340 # set the compatibility flag for webob
341 341 environ['wsgi.input_terminated'] = True
342 342
343 343 def hg_proxy(self):
344 344 @wsgiapp
345 345 def _hg_proxy(environ, start_response):
346 346 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
347 347 return app(environ, start_response)
348 348 return _hg_proxy
349 349
350 350 def git_proxy(self):
351 351 @wsgiapp
352 352 def _git_proxy(environ, start_response):
353 353 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
354 354 return app(environ, start_response)
355 355 return _git_proxy
356 356
357 357 def hg_stream(self):
358 358 if self._use_echo_app:
359 359 @wsgiapp
360 360 def _hg_stream(environ, start_response):
361 361 app = EchoApp('fake_path', 'fake_name', None)
362 362 return app(environ, start_response)
363 363 return _hg_stream
364 364 else:
365 365 @wsgiapp
366 366 def _hg_stream(environ, start_response):
367 367 log.debug('http-app: handling hg stream')
368 368 repo_path = environ['HTTP_X_RC_REPO_PATH']
369 369 repo_name = environ['HTTP_X_RC_REPO_NAME']
370 370 packed_config = base64.b64decode(
371 371 environ['HTTP_X_RC_REPO_CONFIG'])
372 372 config = msgpack.unpackb(packed_config)
373 373 app = scm_app.create_hg_wsgi_app(
374 374 repo_path, repo_name, config)
375 375
376 376 # Consistent path information for hgweb
377 377 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
378 378 environ['REPO_NAME'] = repo_name
379 379 self.set_env_from_config(environ, config)
380 380
381 381 log.debug('http-app: starting app handler '
382 382 'with %s and process request', app)
383 383 return app(environ, ResponseFilter(start_response))
384 384 return _hg_stream
385 385
386 386 def git_stream(self):
387 387 if self._use_echo_app:
388 388 @wsgiapp
389 389 def _git_stream(environ, start_response):
390 390 app = EchoApp('fake_path', 'fake_name', None)
391 391 return app(environ, start_response)
392 392 return _git_stream
393 393 else:
394 394 @wsgiapp
395 395 def _git_stream(environ, start_response):
396 396 log.debug('http-app: handling git stream')
397 397 repo_path = environ['HTTP_X_RC_REPO_PATH']
398 398 repo_name = environ['HTTP_X_RC_REPO_NAME']
399 399 packed_config = base64.b64decode(
400 400 environ['HTTP_X_RC_REPO_CONFIG'])
401 401 config = msgpack.unpackb(packed_config)
402 402
403 403 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
404 404 self.set_env_from_config(environ, config)
405 405
406 406 content_type = environ.get('CONTENT_TYPE', '')
407 407
408 408 path = environ['PATH_INFO']
409 409 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
410 410 log.debug(
411 411 'LFS: Detecting if request `%s` is LFS server path based '
412 412 'on content type:`%s`, is_lfs:%s',
413 413 path, content_type, is_lfs_request)
414 414
415 415 if not is_lfs_request:
416 416 # fallback detection by path
417 417 if GIT_LFS_PROTO_PAT.match(path):
418 418 is_lfs_request = True
419 419 log.debug(
420 420 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
421 421 path, is_lfs_request)
422 422
423 423 if is_lfs_request:
424 424 app = scm_app.create_git_lfs_wsgi_app(
425 425 repo_path, repo_name, config)
426 426 else:
427 427 app = scm_app.create_git_wsgi_app(
428 428 repo_path, repo_name, config)
429 429
430 430 log.debug('http-app: starting app handler '
431 431 'with %s and process request', app)
432 432
433 433 return app(environ, start_response)
434 434
435 435 return _git_stream
436 436
437 437 def is_vcs_view(self, context, request):
438 438 """
439 439 View predicate that returns true if given backend is supported by
440 440 defined remotes.
441 441 """
442 442 backend = request.matchdict.get('backend')
443 443 return backend in self._remotes
444 444
445 445 def handle_vcs_exception(self, exception, request):
446 446 _vcs_kind = getattr(exception, '_vcs_kind', '')
447 447 if _vcs_kind == 'repo_locked':
448 448 # Get custom repo-locked status code if present.
449 449 status_code = request.headers.get('X-RC-Locked-Status-Code')
450 450 return HTTPRepoLocked(
451 451 title=exception.message, status_code=status_code)
452 452
453 453 # Re-raise exception if we can not handle it.
454 454 log.exception(
455 455 'error occurred handling this request for path: %s', request.path)
456 456 raise exception
457 457
458 458
459 459 class ResponseFilter(object):
460 460
461 461 def __init__(self, start_response):
462 462 self._start_response = start_response
463 463
464 464 def __call__(self, status, response_headers, exc_info=None):
465 465 headers = tuple(
466 466 (h, v) for h, v in response_headers
467 467 if not wsgiref.util.is_hop_by_hop(h))
468 468 return self._start_response(status, headers, exc_info)
469 469
470 470
471 471 def main(global_config, **settings):
472 472 if MercurialFactory:
473 473 hgpatches.patch_largefiles_capabilities()
474 474 hgpatches.patch_subrepo_type_mapping()
475 475 app = HTTPApplication(settings=settings, global_config=global_config)
476 476 return app.wsgi_app()
@@ -1,386 +1,386 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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,229 +1,229 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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.hgweb.common
25 25 import mercurial.hgweb.hgweb_mod
26 26 import mercurial.hgweb.protocol
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 req = mercurial.hgweb.request.wsgirequest(environ, start_response)
77 77 gen = self.run_wsgi(req)
78 78
79 79 first_chunk = None
80 80
81 81 try:
82 82 data = gen.next()
83 83 def first_chunk(): yield data
84 84 except StopIteration:
85 85 pass
86 86
87 87 if first_chunk:
88 88 return itertools.chain(first_chunk(), gen)
89 89 return gen
90 90
91 91 def _runwsgi(self, req, repo):
92 92 cmd = req.form.get('cmd', [''])[0]
93 93 if not mercurial.hgweb.protocol.iscmd(cmd):
94 94 req.respond(
95 95 mercurial.hgweb.common.ErrorResponse(
96 96 mercurial.hgweb.common.HTTP_BAD_REQUEST),
97 97 mercurial.hgweb.protocol.HGTYPE
98 98 )
99 99 return ['']
100 100
101 101 return super(HgWeb, self)._runwsgi(req, repo)
102 102
103 103
104 104 def make_hg_ui_from_config(repo_config):
105 105 baseui = mercurial.ui.ui()
106 106
107 107 # clean the baseui object
108 108 baseui._ocfg = mercurial.config.config()
109 109 baseui._ucfg = mercurial.config.config()
110 110 baseui._tcfg = mercurial.config.config()
111 111
112 112 for section, option, value in repo_config:
113 113 baseui.setconfig(section, option, value)
114 114
115 115 # make our hgweb quiet so it doesn't print output
116 116 baseui.setconfig('ui', 'quiet', 'true')
117 117
118 118 return baseui
119 119
120 120
121 121 def update_hg_ui_from_hgrc(baseui, repo_path):
122 122 path = os.path.join(repo_path, '.hg', 'hgrc')
123 123
124 124 if not os.path.isfile(path):
125 125 log.debug('hgrc file is not present at %s, skipping...', path)
126 126 return
127 127 log.debug('reading hgrc from %s', path)
128 128 cfg = mercurial.config.config()
129 129 cfg.read(path)
130 130 for section in HG_UI_SECTIONS:
131 131 for k, v in cfg.items(section):
132 132 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
133 133 baseui.setconfig(section, k, v)
134 134
135 135
136 136 def create_hg_wsgi_app(repo_path, repo_name, config):
137 137 """
138 138 Prepares a WSGI application to handle Mercurial requests.
139 139
140 140 :param config: is a list of 3-item tuples representing a ConfigObject
141 141 (it is the serialized version of the config object).
142 142 """
143 143 log.debug("Creating Mercurial WSGI application")
144 144
145 145 baseui = make_hg_ui_from_config(config)
146 146 update_hg_ui_from_hgrc(baseui, repo_path)
147 147
148 148 try:
149 149 return HgWeb(repo_path, name=repo_name, baseui=baseui)
150 150 except mercurial.error.RequirementError as exc:
151 151 raise exceptions.RequirementException(exc)
152 152
153 153
154 154 class GitHandler(object):
155 155 """
156 156 Handler for Git operations like push/pull etc
157 157 """
158 158 def __init__(self, repo_location, repo_name, git_path, update_server_info,
159 159 extras):
160 160 if not os.path.isdir(repo_location):
161 161 raise OSError(repo_location)
162 162 self.content_path = repo_location
163 163 self.repo_name = repo_name
164 164 self.repo_location = repo_location
165 165 self.extras = extras
166 166 self.git_path = git_path
167 167 self.update_server_info = update_server_info
168 168
169 169 def __call__(self, environ, start_response):
170 170 app = webob.exc.HTTPNotFound()
171 171 candidate_paths = (
172 172 self.content_path, os.path.join(self.content_path, '.git'))
173 173
174 174 for content_path in candidate_paths:
175 175 try:
176 176 app = pygrack.GitRepository(
177 177 self.repo_name, content_path, self.git_path,
178 178 self.update_server_info, self.extras)
179 179 break
180 180 except OSError:
181 181 continue
182 182
183 183 return app(environ, start_response)
184 184
185 185
186 186 def create_git_wsgi_app(repo_path, repo_name, config):
187 187 """
188 188 Creates a WSGI application to handle Git requests.
189 189
190 190 :param config: is a dictionary holding the extras.
191 191 """
192 192 git_path = settings.GIT_EXECUTABLE
193 193 update_server_info = config.pop('git_update_server_info')
194 194 app = GitHandler(
195 195 repo_path, repo_name, git_path, update_server_info, config)
196 196
197 197 return app
198 198
199 199
200 200 class GitLFSHandler(object):
201 201 """
202 202 Handler for Git LFS operations
203 203 """
204 204
205 205 def __init__(self, repo_location, repo_name, git_path, update_server_info,
206 206 extras):
207 207 if not os.path.isdir(repo_location):
208 208 raise OSError(repo_location)
209 209 self.content_path = repo_location
210 210 self.repo_name = repo_name
211 211 self.repo_location = repo_location
212 212 self.extras = extras
213 213 self.git_path = git_path
214 214 self.update_server_info = update_server_info
215 215
216 216 def get_app(self, git_lfs_enabled, git_lfs_store_path):
217 217 app = git_lfs.create_app(git_lfs_enabled, git_lfs_store_path)
218 218 return app
219 219
220 220
221 221 def create_git_lfs_wsgi_app(repo_path, repo_name, config):
222 222 git_path = settings.GIT_EXECUTABLE
223 223 update_server_info = config.pop('git_update_server_info')
224 224 git_lfs_enabled = config.pop('git_lfs_enabled')
225 225 git_lfs_store_path = config.pop('git_lfs_store_path')
226 226 app = GitLFSHandler(
227 227 repo_path, repo_name, git_path, update_server_info, config)
228 228
229 229 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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,19 +1,19 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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'
@@ -1,679 +1,679 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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 from urllib2 import URLError
22 22 import logging
23 23 import posixpath as vcspath
24 24 import StringIO
25 25 import subprocess
26 26 import urllib
27 27 import traceback
28 28
29 29 import svn.client
30 30 import svn.core
31 31 import svn.delta
32 32 import svn.diff
33 33 import svn.fs
34 34 import svn.repos
35 35
36 36 from vcsserver import svn_diff
37 37 from vcsserver import exceptions
38 38 from vcsserver.base import RepoFactory, raise_from_original
39 39
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 # Set of svn compatible version flags.
45 45 # Compare with subversion/svnadmin/svnadmin.c
46 46 svn_compatible_versions = set([
47 47 'pre-1.4-compatible',
48 48 'pre-1.5-compatible',
49 49 'pre-1.6-compatible',
50 50 'pre-1.8-compatible',
51 51 'pre-1.9-compatible',
52 52 ])
53 53
54 54 svn_compatible_versions_map = {
55 55 'pre-1.4-compatible': '1.3',
56 56 'pre-1.5-compatible': '1.4',
57 57 'pre-1.6-compatible': '1.5',
58 58 'pre-1.8-compatible': '1.7',
59 59 'pre-1.9-compatible': '1.8',
60 60 }
61 61
62 62
63 63 def reraise_safe_exceptions(func):
64 64 """Decorator for converting svn exceptions to something neutral."""
65 65 def wrapper(*args, **kwargs):
66 66 try:
67 67 return func(*args, **kwargs)
68 68 except Exception as e:
69 69 if not hasattr(e, '_vcs_kind'):
70 70 log.exception("Unhandled exception in hg remote call")
71 71 raise_from_original(exceptions.UnhandledException)
72 72 raise
73 73 return wrapper
74 74
75 75
76 76 class SubversionFactory(RepoFactory):
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 def create_new_repo():
99 99 return self._create_repo(wire, create, compatible_version)
100 100
101 101 return self._repo(wire, create_new_repo)
102 102
103 103
104 104 NODE_TYPE_MAPPING = {
105 105 svn.core.svn_node_file: 'file',
106 106 svn.core.svn_node_dir: 'dir',
107 107 }
108 108
109 109
110 110 class SvnRemote(object):
111 111
112 112 def __init__(self, factory, hg_factory=None):
113 113 self._factory = factory
114 114 # TODO: Remove once we do not use internal Mercurial objects anymore
115 115 # for subversion
116 116 self._hg_factory = hg_factory
117 117
118 118 @reraise_safe_exceptions
119 119 def discover_svn_version(self):
120 120 try:
121 121 import svn.core
122 122 svn_ver = svn.core.SVN_VERSION
123 123 except ImportError:
124 124 svn_ver = None
125 125 return svn_ver
126 126
127 127 def check_url(self, url, config_items):
128 128 # this can throw exception if not installed, but we detect this
129 129 from hgsubversion import svnrepo
130 130
131 131 baseui = self._hg_factory._create_config(config_items)
132 132 # uuid function get's only valid UUID from proper repo, else
133 133 # throws exception
134 134 try:
135 135 svnrepo.svnremoterepo(baseui, url).svn.uuid
136 136 except Exception:
137 137 tb = traceback.format_exc()
138 138 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
139 139 raise URLError(
140 140 '"%s" is not a valid Subversion source url.' % (url, ))
141 141 return True
142 142
143 143 def is_path_valid_repository(self, wire, path):
144 144
145 145 # NOTE(marcink): short circuit the check for SVN repo
146 146 # the repos.open might be expensive to check, but we have one cheap
147 147 # pre condition that we can use, to check for 'format' file
148 148
149 149 if not os.path.isfile(os.path.join(path, 'format')):
150 150 return False
151 151
152 152 try:
153 153 svn.repos.open(path)
154 154 except svn.core.SubversionException:
155 155 tb = traceback.format_exc()
156 156 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
157 157 return False
158 158 return True
159 159
160 160 @reraise_safe_exceptions
161 161 def verify(self, wire,):
162 162 repo_path = wire['path']
163 163 if not self.is_path_valid_repository(wire, repo_path):
164 164 raise Exception(
165 165 "Path %s is not a valid Subversion repository." % repo_path)
166 166
167 167 load = subprocess.Popen(
168 168 ['svnadmin', 'info', repo_path],
169 169 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
170 170 return ''.join(load.stdout)
171 171
172 172 def lookup(self, wire, revision):
173 173 if revision not in [-1, None, 'HEAD']:
174 174 raise NotImplementedError
175 175 repo = self._factory.repo(wire)
176 176 fs_ptr = svn.repos.fs(repo)
177 177 head = svn.fs.youngest_rev(fs_ptr)
178 178 return head
179 179
180 180 def lookup_interval(self, wire, start_ts, end_ts):
181 181 repo = self._factory.repo(wire)
182 182 fsobj = svn.repos.fs(repo)
183 183 start_rev = None
184 184 end_rev = None
185 185 if start_ts:
186 186 start_ts_svn = apr_time_t(start_ts)
187 187 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
188 188 else:
189 189 start_rev = 1
190 190 if end_ts:
191 191 end_ts_svn = apr_time_t(end_ts)
192 192 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
193 193 else:
194 194 end_rev = svn.fs.youngest_rev(fsobj)
195 195 return start_rev, end_rev
196 196
197 197 def revision_properties(self, wire, revision):
198 198 repo = self._factory.repo(wire)
199 199 fs_ptr = svn.repos.fs(repo)
200 200 return svn.fs.revision_proplist(fs_ptr, revision)
201 201
202 202 def revision_changes(self, wire, revision):
203 203
204 204 repo = self._factory.repo(wire)
205 205 fsobj = svn.repos.fs(repo)
206 206 rev_root = svn.fs.revision_root(fsobj, revision)
207 207
208 208 editor = svn.repos.ChangeCollector(fsobj, rev_root)
209 209 editor_ptr, editor_baton = svn.delta.make_editor(editor)
210 210 base_dir = ""
211 211 send_deltas = False
212 212 svn.repos.replay2(
213 213 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
214 214 editor_ptr, editor_baton, None)
215 215
216 216 added = []
217 217 changed = []
218 218 removed = []
219 219
220 220 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
221 221 for path, change in editor.changes.iteritems():
222 222 # TODO: Decide what to do with directory nodes. Subversion can add
223 223 # empty directories.
224 224
225 225 if change.item_kind == svn.core.svn_node_dir:
226 226 continue
227 227 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
228 228 added.append(path)
229 229 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
230 230 svn.repos.CHANGE_ACTION_REPLACE]:
231 231 changed.append(path)
232 232 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
233 233 removed.append(path)
234 234 else:
235 235 raise NotImplementedError(
236 236 "Action %s not supported on path %s" % (
237 237 change.action, path))
238 238
239 239 changes = {
240 240 'added': added,
241 241 'changed': changed,
242 242 'removed': removed,
243 243 }
244 244 return changes
245 245
246 246 def node_history(self, wire, path, revision, limit):
247 247 cross_copies = False
248 248 repo = self._factory.repo(wire)
249 249 fsobj = svn.repos.fs(repo)
250 250 rev_root = svn.fs.revision_root(fsobj, revision)
251 251
252 252 history_revisions = []
253 253 history = svn.fs.node_history(rev_root, path)
254 254 history = svn.fs.history_prev(history, cross_copies)
255 255 while history:
256 256 __, node_revision = svn.fs.history_location(history)
257 257 history_revisions.append(node_revision)
258 258 if limit and len(history_revisions) >= limit:
259 259 break
260 260 history = svn.fs.history_prev(history, cross_copies)
261 261 return history_revisions
262 262
263 263 def node_properties(self, wire, path, revision):
264 264 repo = self._factory.repo(wire)
265 265 fsobj = svn.repos.fs(repo)
266 266 rev_root = svn.fs.revision_root(fsobj, revision)
267 267 return svn.fs.node_proplist(rev_root, path)
268 268
269 269 def file_annotate(self, wire, path, revision):
270 270 abs_path = 'file://' + urllib.pathname2url(
271 271 vcspath.join(wire['path'], path))
272 272 file_uri = svn.core.svn_path_canonicalize(abs_path)
273 273
274 274 start_rev = svn_opt_revision_value_t(0)
275 275 peg_rev = svn_opt_revision_value_t(revision)
276 276 end_rev = peg_rev
277 277
278 278 annotations = []
279 279
280 280 def receiver(line_no, revision, author, date, line, pool):
281 281 annotations.append((line_no, revision, line))
282 282
283 283 # TODO: Cannot use blame5, missing typemap function in the swig code
284 284 try:
285 285 svn.client.blame2(
286 286 file_uri, peg_rev, start_rev, end_rev,
287 287 receiver, svn.client.create_context())
288 288 except svn.core.SubversionException as exc:
289 289 log.exception("Error during blame operation.")
290 290 raise Exception(
291 291 "Blame not supported or file does not exist at path %s. "
292 292 "Error %s." % (path, exc))
293 293
294 294 return annotations
295 295
296 296 def get_node_type(self, wire, path, rev=None):
297 297 repo = self._factory.repo(wire)
298 298 fs_ptr = svn.repos.fs(repo)
299 299 if rev is None:
300 300 rev = svn.fs.youngest_rev(fs_ptr)
301 301 root = svn.fs.revision_root(fs_ptr, rev)
302 302 node = svn.fs.check_path(root, path)
303 303 return NODE_TYPE_MAPPING.get(node, None)
304 304
305 305 def get_nodes(self, wire, path, revision=None):
306 306 repo = self._factory.repo(wire)
307 307 fsobj = svn.repos.fs(repo)
308 308 if revision is None:
309 309 revision = svn.fs.youngest_rev(fsobj)
310 310 root = svn.fs.revision_root(fsobj, revision)
311 311 entries = svn.fs.dir_entries(root, path)
312 312 result = []
313 313 for entry_path, entry_info in entries.iteritems():
314 314 result.append(
315 315 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
316 316 return result
317 317
318 318 def get_file_content(self, wire, path, rev=None):
319 319 repo = self._factory.repo(wire)
320 320 fsobj = svn.repos.fs(repo)
321 321 if rev is None:
322 322 rev = svn.fs.youngest_revision(fsobj)
323 323 root = svn.fs.revision_root(fsobj, rev)
324 324 content = svn.core.Stream(svn.fs.file_contents(root, path))
325 325 return content.read()
326 326
327 327 def get_file_size(self, wire, path, revision=None):
328 328 repo = self._factory.repo(wire)
329 329 fsobj = svn.repos.fs(repo)
330 330 if revision is None:
331 331 revision = svn.fs.youngest_revision(fsobj)
332 332 root = svn.fs.revision_root(fsobj, revision)
333 333 size = svn.fs.file_length(root, path)
334 334 return size
335 335
336 336 def create_repository(self, wire, compatible_version=None):
337 337 log.info('Creating Subversion repository in path "%s"', wire['path'])
338 338 self._factory.repo(wire, create=True,
339 339 compatible_version=compatible_version)
340 340
341 341 def import_remote_repository(self, wire, src_url):
342 342 repo_path = wire['path']
343 343 if not self.is_path_valid_repository(wire, repo_path):
344 344 raise Exception(
345 345 "Path %s is not a valid Subversion repository." % repo_path)
346 346 # TODO: johbo: URL checks ?
347 347 rdump = subprocess.Popen(
348 348 ['svnrdump', 'dump', '--non-interactive', src_url],
349 349 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
350 350 load = subprocess.Popen(
351 351 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
352 352
353 353 # TODO: johbo: This can be a very long operation, might be better
354 354 # to track some kind of status and provide an api to check if the
355 355 # import is done.
356 356 rdump.wait()
357 357 load.wait()
358 358
359 359 if rdump.returncode != 0:
360 360 errors = rdump.stderr.read()
361 361 log.error('svnrdump dump failed: statuscode %s: message: %s',
362 362 rdump.returncode, errors)
363 363 reason = 'UNKNOWN'
364 364 if 'svnrdump: E230001:' in errors:
365 365 reason = 'INVALID_CERTIFICATE'
366 366 raise Exception(
367 367 'Failed to dump the remote repository from %s.' % src_url,
368 368 reason)
369 369 if load.returncode != 0:
370 370 raise Exception(
371 371 'Failed to load the dump of remote repository from %s.' %
372 372 (src_url, ))
373 373
374 374 def commit(self, wire, message, author, timestamp, updated, removed):
375 375 assert isinstance(message, str)
376 376 assert isinstance(author, str)
377 377
378 378 repo = self._factory.repo(wire)
379 379 fsobj = svn.repos.fs(repo)
380 380
381 381 rev = svn.fs.youngest_rev(fsobj)
382 382 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
383 383 txn_root = svn.fs.txn_root(txn)
384 384
385 385 for node in updated:
386 386 TxnNodeProcessor(node, txn_root).update()
387 387 for node in removed:
388 388 TxnNodeProcessor(node, txn_root).remove()
389 389
390 390 commit_id = svn.repos.fs_commit_txn(repo, txn)
391 391
392 392 if timestamp:
393 393 apr_time = apr_time_t(timestamp)
394 394 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
395 395 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
396 396
397 397 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
398 398 return commit_id
399 399
400 400 def diff(self, wire, rev1, rev2, path1=None, path2=None,
401 401 ignore_whitespace=False, context=3):
402 402
403 403 wire.update(cache=False)
404 404 repo = self._factory.repo(wire)
405 405 diff_creator = SvnDiffer(
406 406 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
407 407 try:
408 408 return diff_creator.generate_diff()
409 409 except svn.core.SubversionException as e:
410 410 log.exception(
411 411 "Error during diff operation operation. "
412 412 "Path might not exist %s, %s" % (path1, path2))
413 413 return ""
414 414
415 415 @reraise_safe_exceptions
416 416 def is_large_file(self, wire, path):
417 417 return False
418 418
419 419
420 420 class SvnDiffer(object):
421 421 """
422 422 Utility to create diffs based on difflib and the Subversion api
423 423 """
424 424
425 425 binary_content = False
426 426
427 427 def __init__(
428 428 self, repo, src_rev, src_path, tgt_rev, tgt_path,
429 429 ignore_whitespace, context):
430 430 self.repo = repo
431 431 self.ignore_whitespace = ignore_whitespace
432 432 self.context = context
433 433
434 434 fsobj = svn.repos.fs(repo)
435 435
436 436 self.tgt_rev = tgt_rev
437 437 self.tgt_path = tgt_path or ''
438 438 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
439 439 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
440 440
441 441 self.src_rev = src_rev
442 442 self.src_path = src_path or self.tgt_path
443 443 self.src_root = svn.fs.revision_root(fsobj, src_rev)
444 444 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
445 445
446 446 self._validate()
447 447
448 448 def _validate(self):
449 449 if (self.tgt_kind != svn.core.svn_node_none and
450 450 self.src_kind != svn.core.svn_node_none and
451 451 self.src_kind != self.tgt_kind):
452 452 # TODO: johbo: proper error handling
453 453 raise Exception(
454 454 "Source and target are not compatible for diff generation. "
455 455 "Source type: %s, target type: %s" %
456 456 (self.src_kind, self.tgt_kind))
457 457
458 458 def generate_diff(self):
459 459 buf = StringIO.StringIO()
460 460 if self.tgt_kind == svn.core.svn_node_dir:
461 461 self._generate_dir_diff(buf)
462 462 else:
463 463 self._generate_file_diff(buf)
464 464 return buf.getvalue()
465 465
466 466 def _generate_dir_diff(self, buf):
467 467 editor = DiffChangeEditor()
468 468 editor_ptr, editor_baton = svn.delta.make_editor(editor)
469 469 svn.repos.dir_delta2(
470 470 self.src_root,
471 471 self.src_path,
472 472 '', # src_entry
473 473 self.tgt_root,
474 474 self.tgt_path,
475 475 editor_ptr, editor_baton,
476 476 authorization_callback_allow_all,
477 477 False, # text_deltas
478 478 svn.core.svn_depth_infinity, # depth
479 479 False, # entry_props
480 480 False, # ignore_ancestry
481 481 )
482 482
483 483 for path, __, change in sorted(editor.changes):
484 484 self._generate_node_diff(
485 485 buf, change, path, self.tgt_path, path, self.src_path)
486 486
487 487 def _generate_file_diff(self, buf):
488 488 change = None
489 489 if self.src_kind == svn.core.svn_node_none:
490 490 change = "add"
491 491 elif self.tgt_kind == svn.core.svn_node_none:
492 492 change = "delete"
493 493 tgt_base, tgt_path = vcspath.split(self.tgt_path)
494 494 src_base, src_path = vcspath.split(self.src_path)
495 495 self._generate_node_diff(
496 496 buf, change, tgt_path, tgt_base, src_path, src_base)
497 497
498 498 def _generate_node_diff(
499 499 self, buf, change, tgt_path, tgt_base, src_path, src_base):
500 500
501 501 if self.src_rev == self.tgt_rev and tgt_base == src_base:
502 502 # makes consistent behaviour with git/hg to return empty diff if
503 503 # we compare same revisions
504 504 return
505 505
506 506 tgt_full_path = vcspath.join(tgt_base, tgt_path)
507 507 src_full_path = vcspath.join(src_base, src_path)
508 508
509 509 self.binary_content = False
510 510 mime_type = self._get_mime_type(tgt_full_path)
511 511
512 512 if mime_type and not mime_type.startswith('text'):
513 513 self.binary_content = True
514 514 buf.write("=" * 67 + '\n')
515 515 buf.write("Cannot display: file marked as a binary type.\n")
516 516 buf.write("svn:mime-type = %s\n" % mime_type)
517 517 buf.write("Index: %s\n" % (tgt_path, ))
518 518 buf.write("=" * 67 + '\n')
519 519 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
520 520 'tgt_path': tgt_path})
521 521
522 522 if change == 'add':
523 523 # TODO: johbo: SVN is missing a zero here compared to git
524 524 buf.write("new file mode 10644\n")
525 525
526 526 #TODO(marcink): intro to binary detection of svn patches
527 527 # if self.binary_content:
528 528 # buf.write('GIT binary patch\n')
529 529
530 530 buf.write("--- /dev/null\t(revision 0)\n")
531 531 src_lines = []
532 532 else:
533 533 if change == 'delete':
534 534 buf.write("deleted file mode 10644\n")
535 535
536 536 #TODO(marcink): intro to binary detection of svn patches
537 537 # if self.binary_content:
538 538 # buf.write('GIT binary patch\n')
539 539
540 540 buf.write("--- a/%s\t(revision %s)\n" % (
541 541 src_path, self.src_rev))
542 542 src_lines = self._svn_readlines(self.src_root, src_full_path)
543 543
544 544 if change == 'delete':
545 545 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
546 546 tgt_lines = []
547 547 else:
548 548 buf.write("+++ b/%s\t(revision %s)\n" % (
549 549 tgt_path, self.tgt_rev))
550 550 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
551 551
552 552 if not self.binary_content:
553 553 udiff = svn_diff.unified_diff(
554 554 src_lines, tgt_lines, context=self.context,
555 555 ignore_blank_lines=self.ignore_whitespace,
556 556 ignore_case=False,
557 557 ignore_space_changes=self.ignore_whitespace)
558 558 buf.writelines(udiff)
559 559
560 560 def _get_mime_type(self, path):
561 561 try:
562 562 mime_type = svn.fs.node_prop(
563 563 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
564 564 except svn.core.SubversionException:
565 565 mime_type = svn.fs.node_prop(
566 566 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
567 567 return mime_type
568 568
569 569 def _svn_readlines(self, fs_root, node_path):
570 570 if self.binary_content:
571 571 return []
572 572 node_kind = svn.fs.check_path(fs_root, node_path)
573 573 if node_kind not in (
574 574 svn.core.svn_node_file, svn.core.svn_node_symlink):
575 575 return []
576 576 content = svn.core.Stream(
577 577 svn.fs.file_contents(fs_root, node_path)).read()
578 578 return content.splitlines(True)
579 579
580 580
581 581 class DiffChangeEditor(svn.delta.Editor):
582 582 """
583 583 Records changes between two given revisions
584 584 """
585 585
586 586 def __init__(self):
587 587 self.changes = []
588 588
589 589 def delete_entry(self, path, revision, parent_baton, pool=None):
590 590 self.changes.append((path, None, 'delete'))
591 591
592 592 def add_file(
593 593 self, path, parent_baton, copyfrom_path, copyfrom_revision,
594 594 file_pool=None):
595 595 self.changes.append((path, 'file', 'add'))
596 596
597 597 def open_file(self, path, parent_baton, base_revision, file_pool=None):
598 598 self.changes.append((path, 'file', 'change'))
599 599
600 600
601 601 def authorization_callback_allow_all(root, path, pool):
602 602 return True
603 603
604 604
605 605 class TxnNodeProcessor(object):
606 606 """
607 607 Utility to process the change of one node within a transaction root.
608 608
609 609 It encapsulates the knowledge of how to add, update or remove
610 610 a node for a given transaction root. The purpose is to support the method
611 611 `SvnRemote.commit`.
612 612 """
613 613
614 614 def __init__(self, node, txn_root):
615 615 assert isinstance(node['path'], str)
616 616
617 617 self.node = node
618 618 self.txn_root = txn_root
619 619
620 620 def update(self):
621 621 self._ensure_parent_dirs()
622 622 self._add_file_if_node_does_not_exist()
623 623 self._update_file_content()
624 624 self._update_file_properties()
625 625
626 626 def remove(self):
627 627 svn.fs.delete(self.txn_root, self.node['path'])
628 628 # TODO: Clean up directory if empty
629 629
630 630 def _ensure_parent_dirs(self):
631 631 curdir = vcspath.dirname(self.node['path'])
632 632 dirs_to_create = []
633 633 while not self._svn_path_exists(curdir):
634 634 dirs_to_create.append(curdir)
635 635 curdir = vcspath.dirname(curdir)
636 636
637 637 for curdir in reversed(dirs_to_create):
638 638 log.debug('Creating missing directory "%s"', curdir)
639 639 svn.fs.make_dir(self.txn_root, curdir)
640 640
641 641 def _svn_path_exists(self, path):
642 642 path_status = svn.fs.check_path(self.txn_root, path)
643 643 return path_status != svn.core.svn_node_none
644 644
645 645 def _add_file_if_node_does_not_exist(self):
646 646 kind = svn.fs.check_path(self.txn_root, self.node['path'])
647 647 if kind == svn.core.svn_node_none:
648 648 svn.fs.make_file(self.txn_root, self.node['path'])
649 649
650 650 def _update_file_content(self):
651 651 assert isinstance(self.node['content'], str)
652 652 handler, baton = svn.fs.apply_textdelta(
653 653 self.txn_root, self.node['path'], None, None)
654 654 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
655 655
656 656 def _update_file_properties(self):
657 657 properties = self.node.get('properties', {})
658 658 for key, value in properties.iteritems():
659 659 svn.fs.change_node_prop(
660 660 self.txn_root, self.node['path'], key, value)
661 661
662 662
663 663 def apr_time_t(timestamp):
664 664 """
665 665 Convert a Python timestamp into APR timestamp type apr_time_t
666 666 """
667 667 return timestamp * 1E6
668 668
669 669
670 670 def svn_opt_revision_value_t(num):
671 671 """
672 672 Put `num` into a `svn_opt_revision_value_t` structure.
673 673 """
674 674 value = svn.core.svn_opt_revision_value_t()
675 675 value.number = num
676 676 revision = svn.core.svn_opt_revision_t()
677 677 revision.kind = svn.core.svn_opt_revision_number
678 678 revision.value = value
679 679 return revision
@@ -1,57 +1,57 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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,71 +1,71 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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)
@@ -1,162 +1,162 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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.fetch(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.fetch(
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 factory = git.GitFactory(repo_cache=Mock())
156 156 wire = {
157 157 'path': '/tmp/abcde'
158 158 }
159 159 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
160 160 with isdir_patcher:
161 161 result = factory._create_repo(wire, True)
162 162 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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,130 +1,130 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
37 37 hgcompat.largefiles.proto, stub_extensions)
38 38
39 39 caps = dynamic_capabilities(stub_repo, stub_proto)
40 40
41 41 stub_extensions.assert_called_once_with(stub_ui)
42 42 assert LARGEFILES_CAPABILITY not in caps
43 43
44 44
45 45 def test_dynamic_capabilities_uses_updated_capabilitiesorig(
46 46 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
47 47 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
48 48 hgcompat.largefiles.proto, stub_extensions)
49 49
50 50 # This happens when the extension is loaded for the first time, important
51 51 # to ensure that an updated function is correctly picked up.
52 52 hgcompat.largefiles.proto.capabilitiesorig = mock.Mock(
53 53 return_value='REPLACED')
54 54
55 55 caps = dynamic_capabilities(stub_repo, stub_proto)
56 56 assert 'REPLACED' == caps
57 57
58 58
59 59 def test_dynamic_capabilities_ignores_updated_capabilities(
60 60 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
61 61 stub_extensions.return_value = [('largefiles', mock.Mock())]
62 62 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
63 63 hgcompat.largefiles.proto, stub_extensions)
64 64
65 65 # This happens when the extension is loaded for the first time, important
66 66 # to ensure that an updated function is correctly picked up.
67 67 hgcompat.largefiles.proto.capabilities = mock.Mock(
68 68 side_effect=Exception('Must not be called'))
69 69
70 70 dynamic_capabilities(stub_repo, stub_proto)
71 71
72 72
73 73 def test_dynamic_capabilities_uses_largefiles_if_enabled(
74 74 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
75 75 stub_extensions.return_value = [('largefiles', mock.Mock())]
76 76
77 77 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
78 78 hgcompat.largefiles.proto, stub_extensions)
79 79
80 80 caps = dynamic_capabilities(stub_repo, stub_proto)
81 81
82 82 stub_extensions.assert_called_once_with(stub_ui)
83 83 assert LARGEFILES_CAPABILITY in caps
84 84
85 85
86 86 def test_hgsubversion_import():
87 87 from hgsubversion import svnrepo
88 88 assert svnrepo
89 89
90 90
91 91 @pytest.fixture
92 92 def patched_capabilities(request):
93 93 """
94 94 Patch in `capabilitiesorig` and restore both capability functions.
95 95 """
96 96 lfproto = hgcompat.largefiles.proto
97 97 orig_capabilities = lfproto.capabilities
98 98 orig_capabilitiesorig = lfproto.capabilitiesorig
99 99
100 100 lfproto.capabilitiesorig = mock.Mock(return_value='ORIG')
101 101
102 102 @request.addfinalizer
103 103 def restore():
104 104 lfproto.capabilities = orig_capabilities
105 105 lfproto.capabilitiesorig = orig_capabilitiesorig
106 106
107 107
108 108 @pytest.fixture
109 109 def stub_repo(stub_ui):
110 110 repo = mock.Mock()
111 111 repo.ui = stub_ui
112 112 return repo
113 113
114 114
115 115 @pytest.fixture
116 116 def stub_proto(stub_ui):
117 117 proto = mock.Mock()
118 118 proto.ui = stub_ui
119 119 return proto
120 120
121 121
122 122 @pytest.fixture
123 123 def stub_ui():
124 124 return hgcompat.ui.ui()
125 125
126 126
127 127 @pytest.fixture
128 128 def stub_extensions():
129 129 extensions = mock.Mock(return_value=tuple())
130 130 return extensions
@@ -1,241 +1,241 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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': [],
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': [],
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,57 +1,57 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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,122 +1,122 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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',
40 40 'import sys; import time; import shutil; ' + script]
41 41
42 42
43 43 def test_raise_exception_on_non_zero_return_code(environ):
44 44 args = _get_python_args('sys.exit(1)')
45 45 with pytest.raises(EnvironmentError):
46 46 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
47 47
48 48
49 49 def test_does_not_fail_on_non_zero_return_code(environ):
50 50 args = _get_python_args('sys.exit(1)')
51 51 output = ''.join(subprocessio.SubprocessIOChunker(
52 52 args, shell=False, fail_on_return_code=False, env=environ))
53 53
54 54 assert output == ''
55 55
56 56
57 57 def test_raise_exception_on_stderr(environ):
58 58 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
59 59 with pytest.raises(EnvironmentError) as excinfo:
60 60 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
61 61
62 62 assert 'exited due to an error:\nX' in str(excinfo.value)
63 63
64 64
65 65 def test_does_not_fail_on_stderr(environ):
66 66 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
67 67 output = ''.join(subprocessio.SubprocessIOChunker(
68 68 args, shell=False, fail_on_stderr=False, env=environ))
69 69
70 70 assert output == ''
71 71
72 72
73 73 @pytest.mark.parametrize('size', [1, 10**5])
74 74 def test_output_with_no_input(size, environ):
75 75 print type(environ)
76 76 data = 'X'
77 77 args = _get_python_args('sys.stdout.write("%s" * %d)' % (data, size))
78 78 output = ''.join(subprocessio.SubprocessIOChunker(
79 79 args, shell=False, env=environ))
80 80
81 81 assert output == data * size
82 82
83 83
84 84 @pytest.mark.parametrize('size', [1, 10**5])
85 85 def test_output_with_no_input_does_not_fail(size, environ):
86 86 data = 'X'
87 87 args = _get_python_args(
88 88 'sys.stdout.write("%s" * %d); sys.exit(1)' % (data, size))
89 89 output = ''.join(subprocessio.SubprocessIOChunker(
90 90 args, shell=False, fail_on_return_code=False, env=environ))
91 91
92 92 print len(data * size), len(output)
93 93 assert output == data * size
94 94
95 95
96 96 @pytest.mark.parametrize('size', [1, 10**5])
97 97 def test_output_with_input(size, environ):
98 98 data = 'X' * size
99 99 inputstream = io.BytesIO(data)
100 100 # This acts like the cat command.
101 101 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
102 102 output = ''.join(subprocessio.SubprocessIOChunker(
103 103 args, shell=False, inputstream=inputstream, env=environ))
104 104
105 105 print len(data), len(output)
106 106 assert output == data
107 107
108 108
109 109 @pytest.mark.parametrize('size', [1, 10**5])
110 110 def test_output_with_input_skipping_iterator(size, environ):
111 111 data = 'X' * size
112 112 inputstream = io.BytesIO(data)
113 113 # This acts like the cat command.
114 114 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
115 115
116 116 # Note: assigning the chunker makes sure that it is not deleted too early
117 117 chunker = subprocessio.SubprocessIOChunker(
118 118 args, shell=False, inputstream=inputstream, env=environ)
119 119 output = ''.join(chunker.output)
120 120
121 121 print len(data), len(output)
122 122 assert output == data
@@ -1,67 +1,67 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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'),
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.',
59 59 expected_reason)
60 60
61 61 assert excinfo.value.args == expected_error_args
62 62
63 63
64 64 def test_svn_libraries_can_be_imported():
65 65 import svn
66 66 import svn.client
67 67 assert svn.client is not None
@@ -1,96 +1,96 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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,60 +1,60 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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',
51 51 safe_str(get_access_path(request)), end - start)
52 52 )
53 53
54 54 return response
55 55
56 56
57 57 def includeme(config):
58 58 config.add_tween(
59 59 'vcsserver.tweens.RequestWrapperTween',
60 60 )
@@ -1,72 +1,72 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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 def safe_int(val, default=None):
20 20 """
21 21 Returns int() of val if val is not convertable to int use default
22 22 instead
23 23
24 24 :param val:
25 25 :param default:
26 26 """
27 27
28 28 try:
29 29 val = int(val)
30 30 except (ValueError, TypeError):
31 31 val = default
32 32
33 33 return val
34 34
35 35
36 36 def safe_str(unicode_, to_encoding=['utf8']):
37 37 """
38 38 safe str function. Does few trick to turn unicode_ into string
39 39
40 40 In case of UnicodeEncodeError, we try to return it with encoding detected
41 41 by chardet library if it fails fallback to string with errors replaced
42 42
43 43 :param unicode_: unicode to encode
44 44 :rtype: str
45 45 :returns: str object
46 46 """
47 47
48 48 # if it's not basestr cast to str
49 49 if not isinstance(unicode_, basestring):
50 50 return str(unicode_)
51 51
52 52 if isinstance(unicode_, str):
53 53 return unicode_
54 54
55 55 if not isinstance(to_encoding, (list, tuple)):
56 56 to_encoding = [to_encoding]
57 57
58 58 for enc in to_encoding:
59 59 try:
60 60 return unicode_.encode(enc)
61 61 except UnicodeEncodeError:
62 62 pass
63 63
64 64 try:
65 65 import chardet
66 66 encoding = chardet.detect(unicode_)['encoding']
67 67 if encoding is None:
68 68 raise UnicodeEncodeError()
69 69
70 70 return unicode_.encode(encoding)
71 71 except (ImportError, UnicodeEncodeError):
72 72 return unicode_.encode(to_encoding[0], 'replace')
@@ -1,116 +1,116 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2018 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