##// END OF EJS Templates
exceptions: don't report lookup errors as exceptions stored in the exception store....
marcink -
r843:d6e71ccf default
parent child Browse files
Show More
@@ -1,117 +1,121 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 """
19 19 Special exception handling over the wire.
20 20
21 21 Since we cannot assume that our client is able to import our exception classes,
22 22 this module provides a "wrapping" mechanism to raise plain exceptions
23 23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
24 24 different error conditions.
25 25 """
26 26
27 27 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
28 28
29 29
30 30 def _make_exception(kind, org_exc, *args):
31 31 """
32 32 Prepares a base `Exception` instance to be sent over the wire.
33 33
34 34 To give our caller a hint what this is about, it will attach an attribute
35 35 `_vcs_kind` to the exception.
36 36 """
37 37 exc = Exception(*args)
38 38 exc._vcs_kind = kind
39 39 exc._org_exc = org_exc
40 40 exc._org_exc_tb = getattr(org_exc, '_org_exc_tb', '')
41 41 return exc
42 42
43 43
44 44 def AbortException(org_exc=None):
45 45 def _make_exception_wrapper(*args):
46 46 return _make_exception('abort', org_exc, *args)
47 47 return _make_exception_wrapper
48 48
49 49
50 50 def ArchiveException(org_exc=None):
51 51 def _make_exception_wrapper(*args):
52 52 return _make_exception('archive', org_exc, *args)
53 53 return _make_exception_wrapper
54 54
55 55
56 56 def LookupException(org_exc=None):
57 57 def _make_exception_wrapper(*args):
58 58 return _make_exception('lookup', org_exc, *args)
59 59 return _make_exception_wrapper
60 60
61 61
62 62 def VcsException(org_exc=None):
63 63 def _make_exception_wrapper(*args):
64 64 return _make_exception('error', org_exc, *args)
65 65 return _make_exception_wrapper
66 66
67 67
68 68 def RepositoryLockedException(org_exc=None):
69 69 def _make_exception_wrapper(*args):
70 70 return _make_exception('repo_locked', org_exc, *args)
71 71 return _make_exception_wrapper
72 72
73 73
74 74 def RepositoryBranchProtectedException(org_exc=None):
75 75 def _make_exception_wrapper(*args):
76 76 return _make_exception('repo_branch_protected', org_exc, *args)
77 77 return _make_exception_wrapper
78 78
79 79
80 80 def RequirementException(org_exc=None):
81 81 def _make_exception_wrapper(*args):
82 82 return _make_exception('requirement', org_exc, *args)
83 83 return _make_exception_wrapper
84 84
85 85
86 86 def UnhandledException(org_exc=None):
87 87 def _make_exception_wrapper(*args):
88 88 return _make_exception('unhandled', org_exc, *args)
89 89 return _make_exception_wrapper
90 90
91 91
92 92 def URLError(org_exc=None):
93 93 def _make_exception_wrapper(*args):
94 94 return _make_exception('url_error', org_exc, *args)
95 95 return _make_exception_wrapper
96 96
97 97
98 98 def SubrepoMergeException(org_exc=None):
99 99 def _make_exception_wrapper(*args):
100 100 return _make_exception('subrepo_merge_error', org_exc, *args)
101 101 return _make_exception_wrapper
102 102
103 103
104 104 class HTTPRepoLocked(HTTPLocked):
105 105 """
106 106 Subclass of HTTPLocked response that allows to set the title and status
107 107 code via constructor arguments.
108 108 """
109 109 def __init__(self, title, status_code=None, **kwargs):
110 110 self.code = status_code or HTTPLocked.code
111 111 self.title = title
112 112 super(HTTPRepoLocked, self).__init__(**kwargs)
113 113
114 114
115 115 class HTTPRepoBranchProtected(HTTPForbidden):
116 116 def __init__(self, *args, **kwargs):
117 117 super(HTTPForbidden, self).__init__(*args, **kwargs)
118
119
120 class RefNotFoundException(KeyError):
121 pass
@@ -1,1181 +1,1189 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import collections
19 19 import logging
20 20 import os
21 21 import posixpath as vcspath
22 22 import re
23 23 import stat
24 24 import traceback
25 25 import urllib
26 26 import urllib2
27 27 from functools import wraps
28 28
29 29 import more_itertools
30 30 import pygit2
31 31 from pygit2 import Repository as LibGit2Repo
32 32 from dulwich import index, objects
33 33 from dulwich.client import HttpGitClient, LocalGitClient
34 34 from dulwich.errors import (
35 35 NotGitRepository, ChecksumMismatch, WrongObjectException,
36 36 MissingCommitError, ObjectMissing, HangupException,
37 37 UnexpectedCommandError)
38 38 from dulwich.repo import Repo as DulwichRepo
39 39 from dulwich.server import update_server_info
40 40
41 41 from vcsserver import exceptions, settings, subprocessio
42 42 from vcsserver.utils import safe_str, safe_int, safe_unicode
43 43 from vcsserver.base import RepoFactory, obfuscate_qs
44 44 from vcsserver.hgcompat import (
45 45 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
46 46 from vcsserver.git_lfs.lib import LFSOidStore
47 47 from vcsserver.vcs_base import RemoteBase
48 48
49 49 DIR_STAT = stat.S_IFDIR
50 50 FILE_MODE = stat.S_IFMT
51 51 GIT_LINK = objects.S_IFGITLINK
52 52 PEELED_REF_MARKER = '^{}'
53 53
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 def str_to_dulwich(value):
59 59 """
60 60 Dulwich 0.10.1a requires `unicode` objects to be passed in.
61 61 """
62 62 return value.decode(settings.WIRE_ENCODING)
63 63
64 64
65 65 def reraise_safe_exceptions(func):
66 66 """Converts Dulwich exceptions to something neutral."""
67 67
68 68 @wraps(func)
69 69 def wrapper(*args, **kwargs):
70 70 try:
71 71 return func(*args, **kwargs)
72 72 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
73 73 exc = exceptions.LookupException(org_exc=e)
74 74 raise exc(safe_str(e))
75 75 except (HangupException, UnexpectedCommandError) as e:
76 76 exc = exceptions.VcsException(org_exc=e)
77 77 raise exc(safe_str(e))
78 78 except Exception as e:
79 79 # NOTE(marcink): becuase of how dulwich handles some exceptions
80 80 # (KeyError on empty repos), we cannot track this and catch all
81 81 # exceptions, it's an exceptions from other handlers
82 82 #if not hasattr(e, '_vcs_kind'):
83 83 #log.exception("Unhandled exception in git remote call")
84 84 #raise_from_original(exceptions.UnhandledException)
85 85 raise
86 86 return wrapper
87 87
88 88
89 89 class Repo(DulwichRepo):
90 90 """
91 91 A wrapper for dulwich Repo class.
92 92
93 93 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
94 94 "Too many open files" error. We need to close all opened file descriptors
95 95 once the repo object is destroyed.
96 96 """
97 97 def __del__(self):
98 98 if hasattr(self, 'object_store'):
99 99 self.close()
100 100
101 101
102 102 class Repository(LibGit2Repo):
103 103
104 104 def __enter__(self):
105 105 return self
106 106
107 107 def __exit__(self, exc_type, exc_val, exc_tb):
108 108 self.free()
109 109
110 110
111 111 class GitFactory(RepoFactory):
112 112 repo_type = 'git'
113 113
114 114 def _create_repo(self, wire, create, use_libgit2=False):
115 115 if use_libgit2:
116 116 return Repository(wire['path'])
117 117 else:
118 118 repo_path = str_to_dulwich(wire['path'])
119 119 return Repo(repo_path)
120 120
121 121 def repo(self, wire, create=False, use_libgit2=False):
122 122 """
123 123 Get a repository instance for the given path.
124 124 """
125 125 return self._create_repo(wire, create, use_libgit2)
126 126
127 127 def repo_libgit2(self, wire):
128 128 return self.repo(wire, use_libgit2=True)
129 129
130 130
131 131 class GitRemote(RemoteBase):
132 132
133 133 def __init__(self, factory):
134 134 self._factory = factory
135 135 self._bulk_methods = {
136 136 "date": self.date,
137 137 "author": self.author,
138 138 "branch": self.branch,
139 139 "message": self.message,
140 140 "parents": self.parents,
141 141 "_commit": self.revision,
142 142 }
143 143
144 144 def _wire_to_config(self, wire):
145 145 if 'config' in wire:
146 146 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
147 147 return {}
148 148
149 149 def _remote_conf(self, config):
150 150 params = [
151 151 '-c', 'core.askpass=""',
152 152 ]
153 153 ssl_cert_dir = config.get('vcs_ssl_dir')
154 154 if ssl_cert_dir:
155 155 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
156 156 return params
157 157
158 158 @reraise_safe_exceptions
159 159 def discover_git_version(self):
160 160 stdout, _ = self.run_git_command(
161 161 {}, ['--version'], _bare=True, _safe=True)
162 162 prefix = 'git version'
163 163 if stdout.startswith(prefix):
164 164 stdout = stdout[len(prefix):]
165 165 return stdout.strip()
166 166
167 167 @reraise_safe_exceptions
168 168 def is_empty(self, wire):
169 169 repo_init = self._factory.repo_libgit2(wire)
170 170 with repo_init as repo:
171 171
172 172 try:
173 173 has_head = repo.head.name
174 174 if has_head:
175 175 return False
176 176
177 177 # NOTE(marcink): check again using more expensive method
178 178 return repo.is_empty
179 179 except Exception:
180 180 pass
181 181
182 182 return True
183 183
184 184 @reraise_safe_exceptions
185 185 def assert_correct_path(self, wire):
186 186 cache_on, context_uid, repo_id = self._cache_on(wire)
187 187 @self.region.conditional_cache_on_arguments(condition=cache_on)
188 188 def _assert_correct_path(_context_uid, _repo_id):
189 189 try:
190 190 repo_init = self._factory.repo_libgit2(wire)
191 191 with repo_init as repo:
192 192 pass
193 193 except pygit2.GitError:
194 194 path = wire.get('path')
195 195 tb = traceback.format_exc()
196 196 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
197 197 return False
198 198
199 199 return True
200 200 return _assert_correct_path(context_uid, repo_id)
201 201
202 202 @reraise_safe_exceptions
203 203 def bare(self, wire):
204 204 repo_init = self._factory.repo_libgit2(wire)
205 205 with repo_init as repo:
206 206 return repo.is_bare
207 207
208 208 @reraise_safe_exceptions
209 209 def blob_as_pretty_string(self, wire, sha):
210 210 repo_init = self._factory.repo_libgit2(wire)
211 211 with repo_init as repo:
212 212 blob_obj = repo[sha]
213 213 blob = blob_obj.data
214 214 return blob
215 215
216 216 @reraise_safe_exceptions
217 217 def blob_raw_length(self, wire, sha):
218 218 cache_on, context_uid, repo_id = self._cache_on(wire)
219 219 @self.region.conditional_cache_on_arguments(condition=cache_on)
220 220 def _blob_raw_length(_repo_id, _sha):
221 221
222 222 repo_init = self._factory.repo_libgit2(wire)
223 223 with repo_init as repo:
224 224 blob = repo[sha]
225 225 return blob.size
226 226
227 227 return _blob_raw_length(repo_id, sha)
228 228
229 229 def _parse_lfs_pointer(self, raw_content):
230 230
231 231 spec_string = 'version https://git-lfs.github.com/spec'
232 232 if raw_content and raw_content.startswith(spec_string):
233 233 pattern = re.compile(r"""
234 234 (?:\n)?
235 235 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
236 236 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
237 237 ^size[ ](?P<oid_size>[0-9]+)\n
238 238 (?:\n)?
239 239 """, re.VERBOSE | re.MULTILINE)
240 240 match = pattern.match(raw_content)
241 241 if match:
242 242 return match.groupdict()
243 243
244 244 return {}
245 245
246 246 @reraise_safe_exceptions
247 247 def is_large_file(self, wire, commit_id):
248 248 cache_on, context_uid, repo_id = self._cache_on(wire)
249 249
250 250 @self.region.conditional_cache_on_arguments(condition=cache_on)
251 251 def _is_large_file(_repo_id, _sha):
252 252 repo_init = self._factory.repo_libgit2(wire)
253 253 with repo_init as repo:
254 254 blob = repo[commit_id]
255 255 if blob.is_binary:
256 256 return {}
257 257
258 258 return self._parse_lfs_pointer(blob.data)
259 259
260 260 return _is_large_file(repo_id, commit_id)
261 261
262 262 @reraise_safe_exceptions
263 263 def is_binary(self, wire, tree_id):
264 264 cache_on, context_uid, repo_id = self._cache_on(wire)
265 265
266 266 @self.region.conditional_cache_on_arguments(condition=cache_on)
267 267 def _is_binary(_repo_id, _tree_id):
268 268 repo_init = self._factory.repo_libgit2(wire)
269 269 with repo_init as repo:
270 270 blob_obj = repo[tree_id]
271 271 return blob_obj.is_binary
272 272
273 273 return _is_binary(repo_id, tree_id)
274 274
275 275 @reraise_safe_exceptions
276 276 def in_largefiles_store(self, wire, oid):
277 277 conf = self._wire_to_config(wire)
278 278 repo_init = self._factory.repo_libgit2(wire)
279 279 with repo_init as repo:
280 280 repo_name = repo.path
281 281
282 282 store_location = conf.get('vcs_git_lfs_store_location')
283 283 if store_location:
284 284
285 285 store = LFSOidStore(
286 286 oid=oid, repo=repo_name, store_location=store_location)
287 287 return store.has_oid()
288 288
289 289 return False
290 290
291 291 @reraise_safe_exceptions
292 292 def store_path(self, wire, oid):
293 293 conf = self._wire_to_config(wire)
294 294 repo_init = self._factory.repo_libgit2(wire)
295 295 with repo_init as repo:
296 296 repo_name = repo.path
297 297
298 298 store_location = conf.get('vcs_git_lfs_store_location')
299 299 if store_location:
300 300 store = LFSOidStore(
301 301 oid=oid, repo=repo_name, store_location=store_location)
302 302 return store.oid_path
303 303 raise ValueError('Unable to fetch oid with path {}'.format(oid))
304 304
305 305 @reraise_safe_exceptions
306 306 def bulk_request(self, wire, rev, pre_load):
307 307 cache_on, context_uid, repo_id = self._cache_on(wire)
308 308 @self.region.conditional_cache_on_arguments(condition=cache_on)
309 309 def _bulk_request(_repo_id, _rev, _pre_load):
310 310 result = {}
311 311 for attr in pre_load:
312 312 try:
313 313 method = self._bulk_methods[attr]
314 314 args = [wire, rev]
315 315 result[attr] = method(*args)
316 316 except KeyError as e:
317 317 raise exceptions.VcsException(e)(
318 318 "Unknown bulk attribute: %s" % attr)
319 319 return result
320 320
321 321 return _bulk_request(repo_id, rev, sorted(pre_load))
322 322
323 323 def _build_opener(self, url):
324 324 handlers = []
325 325 url_obj = url_parser(url)
326 326 _, authinfo = url_obj.authinfo()
327 327
328 328 if authinfo:
329 329 # create a password manager
330 330 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
331 331 passmgr.add_password(*authinfo)
332 332
333 333 handlers.extend((httpbasicauthhandler(passmgr),
334 334 httpdigestauthhandler(passmgr)))
335 335
336 336 return urllib2.build_opener(*handlers)
337 337
338 338 def _type_id_to_name(self, type_id):
339 339 return {
340 340 1: b'commit',
341 341 2: b'tree',
342 342 3: b'blob',
343 343 4: b'tag'
344 344 }[type_id]
345 345
346 346 @reraise_safe_exceptions
347 347 def check_url(self, url, config):
348 348 url_obj = url_parser(url)
349 349 test_uri, _ = url_obj.authinfo()
350 350 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
351 351 url_obj.query = obfuscate_qs(url_obj.query)
352 352 cleaned_uri = str(url_obj)
353 353 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
354 354
355 355 if not test_uri.endswith('info/refs'):
356 356 test_uri = test_uri.rstrip('/') + '/info/refs'
357 357
358 358 o = self._build_opener(url)
359 359 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
360 360
361 361 q = {"service": 'git-upload-pack'}
362 362 qs = '?%s' % urllib.urlencode(q)
363 363 cu = "%s%s" % (test_uri, qs)
364 364 req = urllib2.Request(cu, None, {})
365 365
366 366 try:
367 367 log.debug("Trying to open URL %s", cleaned_uri)
368 368 resp = o.open(req)
369 369 if resp.code != 200:
370 370 raise exceptions.URLError()('Return Code is not 200')
371 371 except Exception as e:
372 372 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
373 373 # means it cannot be cloned
374 374 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
375 375
376 376 # now detect if it's proper git repo
377 377 gitdata = resp.read()
378 378 if 'service=git-upload-pack' in gitdata:
379 379 pass
380 380 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
381 381 # old style git can return some other format !
382 382 pass
383 383 else:
384 384 raise exceptions.URLError()(
385 385 "url [%s] does not look like an git" % (cleaned_uri,))
386 386
387 387 return True
388 388
389 389 @reraise_safe_exceptions
390 390 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
391 391 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
392 392 remote_refs = self.pull(wire, url, apply_refs=False)
393 393 repo = self._factory.repo(wire)
394 394 if isinstance(valid_refs, list):
395 395 valid_refs = tuple(valid_refs)
396 396
397 397 for k in remote_refs:
398 398 # only parse heads/tags and skip so called deferred tags
399 399 if k.startswith(valid_refs) and not k.endswith(deferred):
400 400 repo[k] = remote_refs[k]
401 401
402 402 if update_after_clone:
403 403 # we want to checkout HEAD
404 404 repo["HEAD"] = remote_refs["HEAD"]
405 405 index.build_index_from_tree(repo.path, repo.index_path(),
406 406 repo.object_store, repo["HEAD"].tree)
407 407
408 408 @reraise_safe_exceptions
409 409 def branch(self, wire, commit_id):
410 410 cache_on, context_uid, repo_id = self._cache_on(wire)
411 411 @self.region.conditional_cache_on_arguments(condition=cache_on)
412 412 def _branch(_context_uid, _repo_id, _commit_id):
413 413 regex = re.compile('^refs/heads')
414 414
415 415 def filter_with(ref):
416 416 return regex.match(ref[0]) and ref[1] == _commit_id
417 417
418 418 branches = filter(filter_with, self.get_refs(wire).items())
419 419 return [x[0].split('refs/heads/')[-1] for x in branches]
420 420
421 421 return _branch(context_uid, repo_id, commit_id)
422 422
423 423 @reraise_safe_exceptions
424 424 def commit_branches(self, wire, commit_id):
425 425 cache_on, context_uid, repo_id = self._cache_on(wire)
426 426 @self.region.conditional_cache_on_arguments(condition=cache_on)
427 427 def _commit_branches(_context_uid, _repo_id, _commit_id):
428 428 repo_init = self._factory.repo_libgit2(wire)
429 429 with repo_init as repo:
430 430 branches = [x for x in repo.branches.with_commit(_commit_id)]
431 431 return branches
432 432
433 433 return _commit_branches(context_uid, repo_id, commit_id)
434 434
435 435 @reraise_safe_exceptions
436 436 def add_object(self, wire, content):
437 437 repo_init = self._factory.repo_libgit2(wire)
438 438 with repo_init as repo:
439 439 blob = objects.Blob()
440 440 blob.set_raw_string(content)
441 441 repo.object_store.add_object(blob)
442 442 return blob.id
443 443
444 444 # TODO: this is quite complex, check if that can be simplified
445 445 @reraise_safe_exceptions
446 446 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
447 447 repo = self._factory.repo(wire)
448 448 object_store = repo.object_store
449 449
450 450 # Create tree and populates it with blobs
451 451 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
452 452
453 453 for node in updated:
454 454 # Compute subdirs if needed
455 455 dirpath, nodename = vcspath.split(node['path'])
456 456 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
457 457 parent = commit_tree
458 458 ancestors = [('', parent)]
459 459
460 460 # Tries to dig for the deepest existing tree
461 461 while dirnames:
462 462 curdir = dirnames.pop(0)
463 463 try:
464 464 dir_id = parent[curdir][1]
465 465 except KeyError:
466 466 # put curdir back into dirnames and stops
467 467 dirnames.insert(0, curdir)
468 468 break
469 469 else:
470 470 # If found, updates parent
471 471 parent = repo[dir_id]
472 472 ancestors.append((curdir, parent))
473 473 # Now parent is deepest existing tree and we need to create
474 474 # subtrees for dirnames (in reverse order)
475 475 # [this only applies for nodes from added]
476 476 new_trees = []
477 477
478 478 blob = objects.Blob.from_string(node['content'])
479 479
480 480 if dirnames:
481 481 # If there are trees which should be created we need to build
482 482 # them now (in reverse order)
483 483 reversed_dirnames = list(reversed(dirnames))
484 484 curtree = objects.Tree()
485 485 curtree[node['node_path']] = node['mode'], blob.id
486 486 new_trees.append(curtree)
487 487 for dirname in reversed_dirnames[:-1]:
488 488 newtree = objects.Tree()
489 489 newtree[dirname] = (DIR_STAT, curtree.id)
490 490 new_trees.append(newtree)
491 491 curtree = newtree
492 492 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
493 493 else:
494 494 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
495 495
496 496 new_trees.append(parent)
497 497 # Update ancestors
498 498 reversed_ancestors = reversed(
499 499 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
500 500 for parent, tree, path in reversed_ancestors:
501 501 parent[path] = (DIR_STAT, tree.id)
502 502 object_store.add_object(tree)
503 503
504 504 object_store.add_object(blob)
505 505 for tree in new_trees:
506 506 object_store.add_object(tree)
507 507
508 508 for node_path in removed:
509 509 paths = node_path.split('/')
510 510 tree = commit_tree
511 511 trees = [tree]
512 512 # Traverse deep into the forest...
513 513 for path in paths:
514 514 try:
515 515 obj = repo[tree[path][1]]
516 516 if isinstance(obj, objects.Tree):
517 517 trees.append(obj)
518 518 tree = obj
519 519 except KeyError:
520 520 break
521 521 # Cut down the blob and all rotten trees on the way back...
522 522 for path, tree in reversed(zip(paths, trees)):
523 523 del tree[path]
524 524 if tree:
525 525 # This tree still has elements - don't remove it or any
526 526 # of it's parents
527 527 break
528 528
529 529 object_store.add_object(commit_tree)
530 530
531 531 # Create commit
532 532 commit = objects.Commit()
533 533 commit.tree = commit_tree.id
534 534 for k, v in commit_data.iteritems():
535 535 setattr(commit, k, v)
536 536 object_store.add_object(commit)
537 537
538 538 self.create_branch(wire, branch, commit.id)
539 539
540 540 # dulwich set-ref
541 541 ref = 'refs/heads/%s' % branch
542 542 repo.refs[ref] = commit.id
543 543
544 544 return commit.id
545 545
546 546 @reraise_safe_exceptions
547 547 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
548 548 if url != 'default' and '://' not in url:
549 549 client = LocalGitClient(url)
550 550 else:
551 551 url_obj = url_parser(url)
552 552 o = self._build_opener(url)
553 553 url, _ = url_obj.authinfo()
554 554 client = HttpGitClient(base_url=url, opener=o)
555 555 repo = self._factory.repo(wire)
556 556
557 557 determine_wants = repo.object_store.determine_wants_all
558 558 if refs:
559 559 def determine_wants_requested(references):
560 560 return [references[r] for r in references if r in refs]
561 561 determine_wants = determine_wants_requested
562 562
563 563 try:
564 564 remote_refs = client.fetch(
565 565 path=url, target=repo, determine_wants=determine_wants)
566 566 except NotGitRepository as e:
567 567 log.warning(
568 568 'Trying to fetch from "%s" failed, not a Git repository.', url)
569 569 # Exception can contain unicode which we convert
570 570 raise exceptions.AbortException(e)(repr(e))
571 571
572 572 # mikhail: client.fetch() returns all the remote refs, but fetches only
573 573 # refs filtered by `determine_wants` function. We need to filter result
574 574 # as well
575 575 if refs:
576 576 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
577 577
578 578 if apply_refs:
579 579 # TODO: johbo: Needs proper test coverage with a git repository
580 580 # that contains a tag object, so that we would end up with
581 581 # a peeled ref at this point.
582 582 for k in remote_refs:
583 583 if k.endswith(PEELED_REF_MARKER):
584 584 log.debug("Skipping peeled reference %s", k)
585 585 continue
586 586 repo[k] = remote_refs[k]
587 587
588 588 if refs and not update_after:
589 589 # mikhail: explicitly set the head to the last ref.
590 590 repo['HEAD'] = remote_refs[refs[-1]]
591 591
592 592 if update_after:
593 593 # we want to checkout HEAD
594 594 repo["HEAD"] = remote_refs["HEAD"]
595 595 index.build_index_from_tree(repo.path, repo.index_path(),
596 596 repo.object_store, repo["HEAD"].tree)
597 597 return remote_refs
598 598
599 599 @reraise_safe_exceptions
600 600 def sync_fetch(self, wire, url, refs=None, all_refs=False):
601 601 repo = self._factory.repo(wire)
602 602 if refs and not isinstance(refs, (list, tuple)):
603 603 refs = [refs]
604 604
605 605 config = self._wire_to_config(wire)
606 606 # get all remote refs we'll use to fetch later
607 607 cmd = ['ls-remote']
608 608 if not all_refs:
609 609 cmd += ['--heads', '--tags']
610 610 cmd += [url]
611 611 output, __ = self.run_git_command(
612 612 wire, cmd, fail_on_stderr=False,
613 613 _copts=self._remote_conf(config),
614 614 extra_env={'GIT_TERMINAL_PROMPT': '0'})
615 615
616 616 remote_refs = collections.OrderedDict()
617 617 fetch_refs = []
618 618
619 619 for ref_line in output.splitlines():
620 620 sha, ref = ref_line.split('\t')
621 621 sha = sha.strip()
622 622 if ref in remote_refs:
623 623 # duplicate, skip
624 624 continue
625 625 if ref.endswith(PEELED_REF_MARKER):
626 626 log.debug("Skipping peeled reference %s", ref)
627 627 continue
628 628 # don't sync HEAD
629 629 if ref in ['HEAD']:
630 630 continue
631 631
632 632 remote_refs[ref] = sha
633 633
634 634 if refs and sha in refs:
635 635 # we filter fetch using our specified refs
636 636 fetch_refs.append('{}:{}'.format(ref, ref))
637 637 elif not refs:
638 638 fetch_refs.append('{}:{}'.format(ref, ref))
639 639 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
640 640
641 641 if fetch_refs:
642 642 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
643 643 fetch_refs_chunks = list(chunk)
644 644 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
645 645 _out, _err = self.run_git_command(
646 646 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
647 647 fail_on_stderr=False,
648 648 _copts=self._remote_conf(config),
649 649 extra_env={'GIT_TERMINAL_PROMPT': '0'})
650 650
651 651 return remote_refs
652 652
653 653 @reraise_safe_exceptions
654 654 def sync_push(self, wire, url, refs=None):
655 655 if not self.check_url(url, wire):
656 656 return
657 657 config = self._wire_to_config(wire)
658 658 self._factory.repo(wire)
659 659 self.run_git_command(
660 660 wire, ['push', url, '--mirror'], fail_on_stderr=False,
661 661 _copts=self._remote_conf(config),
662 662 extra_env={'GIT_TERMINAL_PROMPT': '0'})
663 663
664 664 @reraise_safe_exceptions
665 665 def get_remote_refs(self, wire, url):
666 666 repo = Repo(url)
667 667 return repo.get_refs()
668 668
669 669 @reraise_safe_exceptions
670 670 def get_description(self, wire):
671 671 repo = self._factory.repo(wire)
672 672 return repo.get_description()
673 673
674 674 @reraise_safe_exceptions
675 675 def get_missing_revs(self, wire, rev1, rev2, path2):
676 676 repo = self._factory.repo(wire)
677 677 LocalGitClient(thin_packs=False).fetch(path2, repo)
678 678
679 679 wire_remote = wire.copy()
680 680 wire_remote['path'] = path2
681 681 repo_remote = self._factory.repo(wire_remote)
682 682 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
683 683
684 684 revs = [
685 685 x.commit.id
686 686 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
687 687 return revs
688 688
689 689 @reraise_safe_exceptions
690 690 def get_object(self, wire, sha):
691 691 cache_on, context_uid, repo_id = self._cache_on(wire)
692 692 @self.region.conditional_cache_on_arguments(condition=cache_on)
693 693 def _get_object(_context_uid, _repo_id, _sha):
694 694 repo_init = self._factory.repo_libgit2(wire)
695 695 with repo_init as repo:
696 696
697 697 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
698 698 try:
699 699 commit = repo.revparse_single(sha)
700 except (KeyError, ValueError) as e:
700 except KeyError:
701 # NOTE(marcink): KeyError doesn't give us any meaningful information
702 # here, we instead give something more explicit
703 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
704 raise exceptions.LookupException(e)(missing_commit_err)
705 except ValueError as e:
701 706 raise exceptions.LookupException(e)(missing_commit_err)
702 707
703 708 is_tag = False
704 709 if isinstance(commit, pygit2.Tag):
705 710 commit = repo.get(commit.target)
706 711 is_tag = True
707 712
708 713 check_dangling = True
709 714 if is_tag:
710 715 check_dangling = False
711 716
712 717 # we used a reference and it parsed means we're not having a dangling commit
713 718 if sha != commit.hex:
714 719 check_dangling = False
715 720
716 721 if check_dangling:
717 722 # check for dangling commit
718 723 for branch in repo.branches.with_commit(commit.hex):
719 724 if branch:
720 725 break
721 726 else:
722 raise exceptions.LookupException(None)(missing_commit_err)
727 # NOTE(marcink): Empty error doesn't give us any meaningful information
728 # here, we instead give something more explicit
729 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
730 raise exceptions.LookupException(e)(missing_commit_err)
723 731
724 732 commit_id = commit.hex
725 733 type_id = commit.type
726 734
727 735 return {
728 736 'id': commit_id,
729 737 'type': self._type_id_to_name(type_id),
730 738 'commit_id': commit_id,
731 739 'idx': 0
732 740 }
733 741
734 742 return _get_object(context_uid, repo_id, sha)
735 743
736 744 @reraise_safe_exceptions
737 745 def get_refs(self, wire):
738 746 cache_on, context_uid, repo_id = self._cache_on(wire)
739 747 @self.region.conditional_cache_on_arguments(condition=cache_on)
740 748 def _get_refs(_context_uid, _repo_id):
741 749
742 750 repo_init = self._factory.repo_libgit2(wire)
743 751 with repo_init as repo:
744 752 regex = re.compile('^refs/(heads|tags)/')
745 753 return {x.name: x.target.hex for x in
746 754 filter(lambda ref: regex.match(ref.name) ,repo.listall_reference_objects())}
747 755
748 756 return _get_refs(context_uid, repo_id)
749 757
750 758 @reraise_safe_exceptions
751 759 def get_branch_pointers(self, wire):
752 760 cache_on, context_uid, repo_id = self._cache_on(wire)
753 761 @self.region.conditional_cache_on_arguments(condition=cache_on)
754 762 def _get_branch_pointers(_context_uid, _repo_id):
755 763
756 764 repo_init = self._factory.repo_libgit2(wire)
757 765 regex = re.compile('^refs/heads')
758 766 with repo_init as repo:
759 767 branches = filter(lambda ref: regex.match(ref.name), repo.listall_reference_objects())
760 768 return {x.target.hex: x.shorthand for x in branches}
761 769
762 770 return _get_branch_pointers(context_uid, repo_id)
763 771
764 772 @reraise_safe_exceptions
765 773 def head(self, wire, show_exc=True):
766 774 cache_on, context_uid, repo_id = self._cache_on(wire)
767 775 @self.region.conditional_cache_on_arguments(condition=cache_on)
768 776 def _head(_context_uid, _repo_id, _show_exc):
769 777 repo_init = self._factory.repo_libgit2(wire)
770 778 with repo_init as repo:
771 779 try:
772 780 return repo.head.peel().hex
773 781 except Exception:
774 782 if show_exc:
775 783 raise
776 784 return _head(context_uid, repo_id, show_exc)
777 785
778 786 @reraise_safe_exceptions
779 787 def init(self, wire):
780 788 repo_path = str_to_dulwich(wire['path'])
781 789 self.repo = Repo.init(repo_path)
782 790
783 791 @reraise_safe_exceptions
784 792 def init_bare(self, wire):
785 793 repo_path = str_to_dulwich(wire['path'])
786 794 self.repo = Repo.init_bare(repo_path)
787 795
788 796 @reraise_safe_exceptions
789 797 def revision(self, wire, rev):
790 798
791 799 cache_on, context_uid, repo_id = self._cache_on(wire)
792 800 @self.region.conditional_cache_on_arguments(condition=cache_on)
793 801 def _revision(_context_uid, _repo_id, _rev):
794 802 repo_init = self._factory.repo_libgit2(wire)
795 803 with repo_init as repo:
796 804 commit = repo[rev]
797 805 obj_data = {
798 806 'id': commit.id.hex,
799 807 }
800 808 # tree objects itself don't have tree_id attribute
801 809 if hasattr(commit, 'tree_id'):
802 810 obj_data['tree'] = commit.tree_id.hex
803 811
804 812 return obj_data
805 813 return _revision(context_uid, repo_id, rev)
806 814
807 815 @reraise_safe_exceptions
808 816 def date(self, wire, commit_id):
809 817 cache_on, context_uid, repo_id = self._cache_on(wire)
810 818 @self.region.conditional_cache_on_arguments(condition=cache_on)
811 819 def _date(_repo_id, _commit_id):
812 820 repo_init = self._factory.repo_libgit2(wire)
813 821 with repo_init as repo:
814 822 commit = repo[commit_id]
815 823
816 824 if hasattr(commit, 'commit_time'):
817 825 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
818 826 else:
819 827 commit = commit.get_object()
820 828 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
821 829
822 830 # TODO(marcink): check dulwich difference of offset vs timezone
823 831 return [commit_time, commit_time_offset]
824 832 return _date(repo_id, commit_id)
825 833
826 834 @reraise_safe_exceptions
827 835 def author(self, wire, commit_id):
828 836 cache_on, context_uid, repo_id = self._cache_on(wire)
829 837 @self.region.conditional_cache_on_arguments(condition=cache_on)
830 838 def _author(_repo_id, _commit_id):
831 839 repo_init = self._factory.repo_libgit2(wire)
832 840 with repo_init as repo:
833 841 commit = repo[commit_id]
834 842
835 843 if hasattr(commit, 'author'):
836 844 author = commit.author
837 845 else:
838 846 author = commit.get_object().author
839 847
840 848 if author.email:
841 849 return u"{} <{}>".format(author.name, author.email)
842 850
843 851 try:
844 852 return u"{}".format(author.name)
845 853 except Exception:
846 854 return u"{}".format(safe_unicode(author.raw_name))
847 855
848 856 return _author(repo_id, commit_id)
849 857
850 858 @reraise_safe_exceptions
851 859 def message(self, wire, commit_id):
852 860 cache_on, context_uid, repo_id = self._cache_on(wire)
853 861 @self.region.conditional_cache_on_arguments(condition=cache_on)
854 862 def _message(_repo_id, _commit_id):
855 863 repo_init = self._factory.repo_libgit2(wire)
856 864 with repo_init as repo:
857 865 commit = repo[commit_id]
858 866 return commit.message
859 867 return _message(repo_id, commit_id)
860 868
861 869 @reraise_safe_exceptions
862 870 def parents(self, wire, commit_id):
863 871 cache_on, context_uid, repo_id = self._cache_on(wire)
864 872 @self.region.conditional_cache_on_arguments(condition=cache_on)
865 873 def _parents(_repo_id, _commit_id):
866 874 repo_init = self._factory.repo_libgit2(wire)
867 875 with repo_init as repo:
868 876 commit = repo[commit_id]
869 877 if hasattr(commit, 'parent_ids'):
870 878 parent_ids = commit.parent_ids
871 879 else:
872 880 parent_ids = commit.get_object().parent_ids
873 881
874 882 return [x.hex for x in parent_ids]
875 883 return _parents(repo_id, commit_id)
876 884
877 885 @reraise_safe_exceptions
878 886 def children(self, wire, commit_id):
879 887 cache_on, context_uid, repo_id = self._cache_on(wire)
880 888 @self.region.conditional_cache_on_arguments(condition=cache_on)
881 889 def _children(_repo_id, _commit_id):
882 890 output, __ = self.run_git_command(
883 891 wire, ['rev-list', '--all', '--children'])
884 892
885 893 child_ids = []
886 894 pat = re.compile(r'^%s' % commit_id)
887 895 for l in output.splitlines():
888 896 if pat.match(l):
889 897 found_ids = l.split(' ')[1:]
890 898 child_ids.extend(found_ids)
891 899
892 900 return child_ids
893 901 return _children(repo_id, commit_id)
894 902
895 903 @reraise_safe_exceptions
896 904 def set_refs(self, wire, key, value):
897 905 repo_init = self._factory.repo_libgit2(wire)
898 906 with repo_init as repo:
899 907 repo.references.create(key, value, force=True)
900 908
901 909 @reraise_safe_exceptions
902 910 def create_branch(self, wire, branch_name, commit_id, force=False):
903 911 repo_init = self._factory.repo_libgit2(wire)
904 912 with repo_init as repo:
905 913 commit = repo[commit_id]
906 914
907 915 if force:
908 916 repo.branches.local.create(branch_name, commit, force=force)
909 917 elif not repo.branches.get(branch_name):
910 918 # create only if that branch isn't existing
911 919 repo.branches.local.create(branch_name, commit, force=force)
912 920
913 921 @reraise_safe_exceptions
914 922 def remove_ref(self, wire, key):
915 923 repo_init = self._factory.repo_libgit2(wire)
916 924 with repo_init as repo:
917 925 repo.references.delete(key)
918 926
919 927 @reraise_safe_exceptions
920 928 def tag_remove(self, wire, tag_name):
921 929 repo_init = self._factory.repo_libgit2(wire)
922 930 with repo_init as repo:
923 931 key = 'refs/tags/{}'.format(tag_name)
924 932 repo.references.delete(key)
925 933
926 934 @reraise_safe_exceptions
927 935 def tree_changes(self, wire, source_id, target_id):
928 936 # TODO(marcink): remove this seems it's only used by tests
929 937 repo = self._factory.repo(wire)
930 938 source = repo[source_id].tree if source_id else None
931 939 target = repo[target_id].tree
932 940 result = repo.object_store.tree_changes(source, target)
933 941 return list(result)
934 942
935 943 @reraise_safe_exceptions
936 944 def tree_and_type_for_path(self, wire, commit_id, path):
937 945
938 946 cache_on, context_uid, repo_id = self._cache_on(wire)
939 947 @self.region.conditional_cache_on_arguments(condition=cache_on)
940 948 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
941 949 repo_init = self._factory.repo_libgit2(wire)
942 950
943 951 with repo_init as repo:
944 952 commit = repo[commit_id]
945 953 try:
946 954 tree = commit.tree[path]
947 955 except KeyError:
948 956 return None, None, None
949 957
950 958 return tree.id.hex, tree.type, tree.filemode
951 959 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
952 960
953 961 @reraise_safe_exceptions
954 962 def tree_items(self, wire, tree_id):
955 963 cache_on, context_uid, repo_id = self._cache_on(wire)
956 964 @self.region.conditional_cache_on_arguments(condition=cache_on)
957 965 def _tree_items(_repo_id, _tree_id):
958 966
959 967 repo_init = self._factory.repo_libgit2(wire)
960 968 with repo_init as repo:
961 969 try:
962 970 tree = repo[tree_id]
963 971 except KeyError:
964 972 raise ObjectMissing('No tree with id: {}'.format(tree_id))
965 973
966 974 result = []
967 975 for item in tree:
968 976 item_sha = item.hex
969 977 item_mode = item.filemode
970 978 item_type = item.type
971 979
972 980 if item_type == 'commit':
973 981 # NOTE(marcink): submodules we translate to 'link' for backward compat
974 982 item_type = 'link'
975 983
976 984 result.append((item.name, item_mode, item_sha, item_type))
977 985 return result
978 986 return _tree_items(repo_id, tree_id)
979 987
980 988 @reraise_safe_exceptions
981 989 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
982 990 """
983 991 Old version that uses subprocess to call diff
984 992 """
985 993
986 994 flags = [
987 995 '-U%s' % context, '--patch',
988 996 '--binary',
989 997 '--find-renames',
990 998 '--no-indent-heuristic',
991 999 # '--indent-heuristic',
992 1000 #'--full-index',
993 1001 #'--abbrev=40'
994 1002 ]
995 1003
996 1004 if opt_ignorews:
997 1005 flags.append('--ignore-all-space')
998 1006
999 1007 if commit_id_1 == self.EMPTY_COMMIT:
1000 1008 cmd = ['show'] + flags + [commit_id_2]
1001 1009 else:
1002 1010 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1003 1011
1004 1012 if file_filter:
1005 1013 cmd.extend(['--', file_filter])
1006 1014
1007 1015 diff, __ = self.run_git_command(wire, cmd)
1008 1016 # If we used 'show' command, strip first few lines (until actual diff
1009 1017 # starts)
1010 1018 if commit_id_1 == self.EMPTY_COMMIT:
1011 1019 lines = diff.splitlines()
1012 1020 x = 0
1013 1021 for line in lines:
1014 1022 if line.startswith('diff'):
1015 1023 break
1016 1024 x += 1
1017 1025 # Append new line just like 'diff' command do
1018 1026 diff = '\n'.join(lines[x:]) + '\n'
1019 1027 return diff
1020 1028
1021 1029 @reraise_safe_exceptions
1022 1030 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1023 1031 repo_init = self._factory.repo_libgit2(wire)
1024 1032 with repo_init as repo:
1025 1033 swap = True
1026 1034 flags = 0
1027 1035 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1028 1036
1029 1037 if opt_ignorews:
1030 1038 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1031 1039
1032 1040 if commit_id_1 == self.EMPTY_COMMIT:
1033 1041 comm1 = repo[commit_id_2]
1034 1042 diff_obj = comm1.tree.diff_to_tree(
1035 1043 flags=flags, context_lines=context, swap=swap)
1036 1044
1037 1045 else:
1038 1046 comm1 = repo[commit_id_2]
1039 1047 comm2 = repo[commit_id_1]
1040 1048 diff_obj = comm1.tree.diff_to_tree(
1041 1049 comm2.tree, flags=flags, context_lines=context, swap=swap)
1042 1050 similar_flags = 0
1043 1051 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1044 1052 diff_obj.find_similar(flags=similar_flags)
1045 1053
1046 1054 if file_filter:
1047 1055 for p in diff_obj:
1048 1056 if p.delta.old_file.path == file_filter:
1049 1057 return p.patch or ''
1050 1058 # fo matching path == no diff
1051 1059 return ''
1052 1060 return diff_obj.patch or ''
1053 1061
1054 1062 @reraise_safe_exceptions
1055 1063 def node_history(self, wire, commit_id, path, limit):
1056 1064 cache_on, context_uid, repo_id = self._cache_on(wire)
1057 1065 @self.region.conditional_cache_on_arguments(condition=cache_on)
1058 1066 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1059 1067 # optimize for n==1, rev-list is much faster for that use-case
1060 1068 if limit == 1:
1061 1069 cmd = ['rev-list', '-1', commit_id, '--', path]
1062 1070 else:
1063 1071 cmd = ['log']
1064 1072 if limit:
1065 1073 cmd.extend(['-n', str(safe_int(limit, 0))])
1066 1074 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1067 1075
1068 1076 output, __ = self.run_git_command(wire, cmd)
1069 1077 commit_ids = re.findall(r'[0-9a-fA-F]{40}', output)
1070 1078
1071 1079 return [x for x in commit_ids]
1072 1080 return _node_history(context_uid, repo_id, commit_id, path, limit)
1073 1081
1074 1082 @reraise_safe_exceptions
1075 1083 def node_annotate(self, wire, commit_id, path):
1076 1084
1077 1085 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1078 1086 # -l ==> outputs long shas (and we need all 40 characters)
1079 1087 # --root ==> doesn't put '^' character for boundaries
1080 1088 # -r commit_id ==> blames for the given commit
1081 1089 output, __ = self.run_git_command(wire, cmd)
1082 1090
1083 1091 result = []
1084 1092 for i, blame_line in enumerate(output.split('\n')[:-1]):
1085 1093 line_no = i + 1
1086 1094 commit_id, line = re.split(r' ', blame_line, 1)
1087 1095 result.append((line_no, commit_id, line))
1088 1096 return result
1089 1097
1090 1098 @reraise_safe_exceptions
1091 1099 def update_server_info(self, wire):
1092 1100 repo = self._factory.repo(wire)
1093 1101 update_server_info(repo)
1094 1102
1095 1103 @reraise_safe_exceptions
1096 1104 def get_all_commit_ids(self, wire):
1097 1105
1098 1106 cache_on, context_uid, repo_id = self._cache_on(wire)
1099 1107 @self.region.conditional_cache_on_arguments(condition=cache_on)
1100 1108 def _get_all_commit_ids(_context_uid, _repo_id):
1101 1109
1102 1110 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1103 1111 try:
1104 1112 output, __ = self.run_git_command(wire, cmd)
1105 1113 return output.splitlines()
1106 1114 except Exception:
1107 1115 # Can be raised for empty repositories
1108 1116 return []
1109 1117 return _get_all_commit_ids(context_uid, repo_id)
1110 1118
1111 1119 @reraise_safe_exceptions
1112 1120 def run_git_command(self, wire, cmd, **opts):
1113 1121 path = wire.get('path', None)
1114 1122
1115 1123 if path and os.path.isdir(path):
1116 1124 opts['cwd'] = path
1117 1125
1118 1126 if '_bare' in opts:
1119 1127 _copts = []
1120 1128 del opts['_bare']
1121 1129 else:
1122 1130 _copts = ['-c', 'core.quotepath=false', ]
1123 1131 safe_call = False
1124 1132 if '_safe' in opts:
1125 1133 # no exc on failure
1126 1134 del opts['_safe']
1127 1135 safe_call = True
1128 1136
1129 1137 if '_copts' in opts:
1130 1138 _copts.extend(opts['_copts'] or [])
1131 1139 del opts['_copts']
1132 1140
1133 1141 gitenv = os.environ.copy()
1134 1142 gitenv.update(opts.pop('extra_env', {}))
1135 1143 # need to clean fix GIT_DIR !
1136 1144 if 'GIT_DIR' in gitenv:
1137 1145 del gitenv['GIT_DIR']
1138 1146 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1139 1147 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1140 1148
1141 1149 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1142 1150 _opts = {'env': gitenv, 'shell': False}
1143 1151
1144 1152 proc = None
1145 1153 try:
1146 1154 _opts.update(opts)
1147 1155 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1148 1156
1149 1157 return ''.join(proc), ''.join(proc.error)
1150 1158 except (EnvironmentError, OSError) as err:
1151 1159 cmd = ' '.join(cmd) # human friendly CMD
1152 1160 tb_err = ("Couldn't run git command (%s).\n"
1153 1161 "Original error was:%s\n"
1154 1162 "Call options:%s\n"
1155 1163 % (cmd, err, _opts))
1156 1164 log.exception(tb_err)
1157 1165 if safe_call:
1158 1166 return '', err
1159 1167 else:
1160 1168 raise exceptions.VcsException()(tb_err)
1161 1169 finally:
1162 1170 if proc:
1163 1171 proc.close()
1164 1172
1165 1173 @reraise_safe_exceptions
1166 1174 def install_hooks(self, wire, force=False):
1167 1175 from vcsserver.hook_utils import install_git_hooks
1168 1176 bare = self.bare(wire)
1169 1177 path = wire['path']
1170 1178 return install_git_hooks(path, bare, force_create=force)
1171 1179
1172 1180 @reraise_safe_exceptions
1173 1181 def get_hooks_info(self, wire):
1174 1182 from vcsserver.hook_utils import (
1175 1183 get_git_pre_hook_version, get_git_post_hook_version)
1176 1184 bare = self.bare(wire)
1177 1185 path = wire['path']
1178 1186 return {
1179 1187 'pre_version': get_git_pre_hook_version(path, bare),
1180 1188 'post_version': get_git_post_hook_version(path, bare),
1181 1189 }
@@ -1,675 +1,688 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import sys
20 20 import base64
21 21 import locale
22 22 import logging
23 23 import uuid
24 24 import wsgiref.util
25 25 import traceback
26 26 import tempfile
27 27 from itertools import chain
28 28 from cStringIO import StringIO
29 29
30 30 import simplejson as json
31 31 import msgpack
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.wsgi import wsgiapp
35 35 from pyramid.compat import configparser
36 36 from pyramid.response import Response
37 37
38 38 from vcsserver.utils import safe_int
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
43 43 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
44 44
45 45 try:
46 46 locale.setlocale(locale.LC_ALL, '')
47 47 except locale.Error as e:
48 48 log.error(
49 49 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
50 50 os.environ['LC_ALL'] = 'C'
51 51
52 52 import vcsserver
53 53 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
54 54 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
55 55 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
56 56 from vcsserver.echo_stub.echo_app import EchoApp
57 57 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
58 58 from vcsserver.lib.exc_tracking import store_exception
59 59 from vcsserver.server import VcsServer
60 60
61 61 try:
62 62 from vcsserver.git import GitFactory, GitRemote
63 63 except ImportError:
64 64 GitFactory = None
65 65 GitRemote = None
66 66
67 67 try:
68 68 from vcsserver.hg import MercurialFactory, HgRemote
69 69 except ImportError:
70 70 MercurialFactory = None
71 71 HgRemote = None
72 72
73 73 try:
74 74 from vcsserver.svn import SubversionFactory, SvnRemote
75 75 except ImportError:
76 76 SubversionFactory = None
77 77 SvnRemote = None
78 78
79 79
80 80 def _is_request_chunked(environ):
81 81 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
82 82 return stream
83 83
84 84
85 85 def _int_setting(settings, name, default):
86 86 settings[name] = int(settings.get(name, default))
87 87 return settings[name]
88 88
89 89
90 90 def _bool_setting(settings, name, default):
91 91 input_val = settings.get(name, default)
92 92 if isinstance(input_val, unicode):
93 93 input_val = input_val.encode('utf8')
94 94 settings[name] = asbool(input_val)
95 95 return settings[name]
96 96
97 97
98 98 def _list_setting(settings, name, default):
99 99 raw_value = settings.get(name, default)
100 100
101 101 # Otherwise we assume it uses pyramids space/newline separation.
102 102 settings[name] = aslist(raw_value)
103 103 return settings[name]
104 104
105 105
106 106 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
107 107 value = settings.get(name, default)
108 108
109 109 if default_when_empty and not value:
110 110 # use default value when value is empty
111 111 value = default
112 112
113 113 if lower:
114 114 value = value.lower()
115 115 settings[name] = value
116 116 return settings[name]
117 117
118 118
119 119 class VCS(object):
120 120 def __init__(self, locale_conf=None, cache_config=None):
121 121 self.locale = locale_conf
122 122 self.cache_config = cache_config
123 123 self._configure_locale()
124 124
125 125 if GitFactory and GitRemote:
126 126 git_factory = GitFactory()
127 127 self._git_remote = GitRemote(git_factory)
128 128 else:
129 129 log.info("Git client import failed")
130 130
131 131 if MercurialFactory and HgRemote:
132 132 hg_factory = MercurialFactory()
133 133 self._hg_remote = HgRemote(hg_factory)
134 134 else:
135 135 log.info("Mercurial client import failed")
136 136
137 137 if SubversionFactory and SvnRemote:
138 138 svn_factory = SubversionFactory()
139 139
140 140 # hg factory is used for svn url validation
141 141 hg_factory = MercurialFactory()
142 142 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
143 143 else:
144 144 log.info("Subversion client import failed")
145 145
146 146 self._vcsserver = VcsServer()
147 147
148 148 def _configure_locale(self):
149 149 if self.locale:
150 150 log.info('Settings locale: `LC_ALL` to %s', self.locale)
151 151 else:
152 152 log.info(
153 153 'Configuring locale subsystem based on environment variables')
154 154 try:
155 155 # If self.locale is the empty string, then the locale
156 156 # module will use the environment variables. See the
157 157 # documentation of the package `locale`.
158 158 locale.setlocale(locale.LC_ALL, self.locale)
159 159
160 160 language_code, encoding = locale.getlocale()
161 161 log.info(
162 162 'Locale set to language code "%s" with encoding "%s".',
163 163 language_code, encoding)
164 164 except locale.Error:
165 165 log.exception(
166 166 'Cannot set locale, not configuring the locale system')
167 167
168 168
169 169 class WsgiProxy(object):
170 170 def __init__(self, wsgi):
171 171 self.wsgi = wsgi
172 172
173 173 def __call__(self, environ, start_response):
174 174 input_data = environ['wsgi.input'].read()
175 175 input_data = msgpack.unpackb(input_data)
176 176
177 177 error = None
178 178 try:
179 179 data, status, headers = self.wsgi.handle(
180 180 input_data['environment'], input_data['input_data'],
181 181 *input_data['args'], **input_data['kwargs'])
182 182 except Exception as e:
183 183 data, status, headers = [], None, None
184 184 error = {
185 185 'message': str(e),
186 186 '_vcs_kind': getattr(e, '_vcs_kind', None)
187 187 }
188 188
189 189 start_response(200, {})
190 190 return self._iterator(error, status, headers, data)
191 191
192 192 def _iterator(self, error, status, headers, data):
193 193 initial_data = [
194 194 error,
195 195 status,
196 196 headers,
197 197 ]
198 198
199 199 for d in chain(initial_data, data):
200 200 yield msgpack.packb(d)
201 201
202 202
203 203 def not_found(request):
204 204 return {'status': '404 NOT FOUND'}
205 205
206 206
207 207 class VCSViewPredicate(object):
208 208 def __init__(self, val, config):
209 209 self.remotes = val
210 210
211 211 def text(self):
212 212 return 'vcs view method = %s' % (self.remotes.keys(),)
213 213
214 214 phash = text
215 215
216 216 def __call__(self, context, request):
217 217 """
218 218 View predicate that returns true if given backend is supported by
219 219 defined remotes.
220 220 """
221 221 backend = request.matchdict.get('backend')
222 222 return backend in self.remotes
223 223
224 224
225 225 class HTTPApplication(object):
226 226 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
227 227
228 228 remote_wsgi = remote_wsgi
229 229 _use_echo_app = False
230 230
231 231 def __init__(self, settings=None, global_config=None):
232 232 self._sanitize_settings_and_apply_defaults(settings)
233 233
234 234 self.config = Configurator(settings=settings)
235 235 self.global_config = global_config
236 236 self.config.include('vcsserver.lib.rc_cache')
237 237
238 238 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
239 239 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
240 240 self._remotes = {
241 241 'hg': vcs._hg_remote,
242 242 'git': vcs._git_remote,
243 243 'svn': vcs._svn_remote,
244 244 'server': vcs._vcsserver,
245 245 }
246 246 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
247 247 self._use_echo_app = True
248 248 log.warning("Using EchoApp for VCS operations.")
249 249 self.remote_wsgi = remote_wsgi_stub
250 250
251 251 self._configure_settings(global_config, settings)
252 252 self._configure()
253 253
254 254 def _configure_settings(self, global_config, app_settings):
255 255 """
256 256 Configure the settings module.
257 257 """
258 258 settings_merged = global_config.copy()
259 259 settings_merged.update(app_settings)
260 260
261 261 git_path = app_settings.get('git_path', None)
262 262 if git_path:
263 263 settings.GIT_EXECUTABLE = git_path
264 264 binary_dir = app_settings.get('core.binary_dir', None)
265 265 if binary_dir:
266 266 settings.BINARY_DIR = binary_dir
267 267
268 268 # Store the settings to make them available to other modules.
269 269 vcsserver.PYRAMID_SETTINGS = settings_merged
270 270 vcsserver.CONFIG = settings_merged
271 271
272 272 def _sanitize_settings_and_apply_defaults(self, settings):
273 273 temp_store = tempfile.gettempdir()
274 274 default_cache_dir = os.path.join(temp_store, 'rc_cache')
275 275
276 276 # save default, cache dir, and use it for all backends later.
277 277 default_cache_dir = _string_setting(
278 278 settings,
279 279 'cache_dir',
280 280 default_cache_dir, lower=False, default_when_empty=True)
281 281
282 282 # ensure we have our dir created
283 283 if not os.path.isdir(default_cache_dir):
284 284 os.makedirs(default_cache_dir, mode=0o755)
285 285
286 286 # exception store cache
287 287 _string_setting(
288 288 settings,
289 289 'exception_tracker.store_path',
290 290 temp_store, lower=False, default_when_empty=True)
291 291
292 292 # repo_object cache
293 293 _string_setting(
294 294 settings,
295 295 'rc_cache.repo_object.backend',
296 296 'dogpile.cache.rc.file_namespace', lower=False)
297 297 _int_setting(
298 298 settings,
299 299 'rc_cache.repo_object.expiration_time',
300 300 30 * 24 * 60 * 60)
301 301 _string_setting(
302 302 settings,
303 303 'rc_cache.repo_object.arguments.filename',
304 304 os.path.join(default_cache_dir, 'vcsserver_cache_1'), lower=False)
305 305
306 306 def _configure(self):
307 307 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
308 308
309 309 self.config.add_route('service', '/_service')
310 310 self.config.add_route('status', '/status')
311 311 self.config.add_route('hg_proxy', '/proxy/hg')
312 312 self.config.add_route('git_proxy', '/proxy/git')
313 313
314 314 # rpc methods
315 315 self.config.add_route('vcs', '/{backend}')
316 316
317 317 # streaming rpc remote methods
318 318 self.config.add_route('vcs_stream', '/{backend}/stream')
319 319
320 320 # vcs operations clone/push as streaming
321 321 self.config.add_route('stream_git', '/stream/git/*repo_name')
322 322 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
323 323
324 324 self.config.add_view(self.status_view, route_name='status', renderer='json')
325 325 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
326 326
327 327 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
328 328 self.config.add_view(self.git_proxy(), route_name='git_proxy')
329 329 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
330 330 vcs_view=self._remotes)
331 331 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
332 332 vcs_view=self._remotes)
333 333
334 334 self.config.add_view(self.hg_stream(), route_name='stream_hg')
335 335 self.config.add_view(self.git_stream(), route_name='stream_git')
336 336
337 337 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
338 338
339 339 self.config.add_notfound_view(not_found, renderer='json')
340 340
341 341 self.config.add_view(self.handle_vcs_exception, context=Exception)
342 342
343 343 self.config.add_tween(
344 344 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
345 345 )
346 346 self.config.add_request_method(
347 347 'vcsserver.lib.request_counter.get_request_counter',
348 348 'request_count')
349 349
350 350 def wsgi_app(self):
351 351 return self.config.make_wsgi_app()
352 352
353 353 def _vcs_view_params(self, request):
354 354 remote = self._remotes[request.matchdict['backend']]
355 355 payload = msgpack.unpackb(request.body, use_list=True)
356 356 method = payload.get('method')
357 357 params = payload['params']
358 358 wire = params.get('wire')
359 359 args = params.get('args')
360 360 kwargs = params.get('kwargs')
361 361 context_uid = None
362 362
363 363 if wire:
364 364 try:
365 365 wire['context'] = context_uid = uuid.UUID(wire['context'])
366 366 except KeyError:
367 367 pass
368 368 args.insert(0, wire)
369 369 repo_state_uid = wire.get('repo_state_uid') if wire else None
370 370
371 371 # NOTE(marcink): trading complexity for slight performance
372 372 if log.isEnabledFor(logging.DEBUG):
373 373 no_args_methods = [
374 374 'archive_repo'
375 375 ]
376 376 if method in no_args_methods:
377 377 call_args = ''
378 378 else:
379 379 call_args = args[1:]
380 380
381 381 log.debug('method requested:%s with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
382 382 method, call_args, kwargs, context_uid, repo_state_uid)
383 383
384 384 return payload, remote, method, args, kwargs
385 385
386 386 def vcs_view(self, request):
387 387
388 388 payload, remote, method, args, kwargs = self._vcs_view_params(request)
389 389 payload_id = payload.get('id')
390 390
391 391 try:
392 392 resp = getattr(remote, method)(*args, **kwargs)
393 393 except Exception as e:
394 394 exc_info = list(sys.exc_info())
395 395 exc_type, exc_value, exc_traceback = exc_info
396 396
397 397 org_exc = getattr(e, '_org_exc', None)
398 398 org_exc_name = None
399 399 org_exc_tb = ''
400 400 if org_exc:
401 401 org_exc_name = org_exc.__class__.__name__
402 402 org_exc_tb = getattr(e, '_org_exc_tb', '')
403 403 # replace our "faked" exception with our org
404 404 exc_info[0] = org_exc.__class__
405 405 exc_info[1] = org_exc
406 406
407 store_exception(id(exc_info), exc_info)
407 should_store_exc = True
408 if org_exc:
409 def get_exc_fqn(_exc_obj):
410 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
411 return module_name + '.' + org_exc_name
412
413 exc_fqn = get_exc_fqn(org_exc)
414
415 if exc_fqn in ['mercurial.error.RepoLookupError',
416 'vcsserver.exceptions.RefNotFoundException']:
417 should_store_exc = False
418
419 if should_store_exc:
420 store_exception(id(exc_info), exc_info)
408 421
409 422 tb_info = ''.join(
410 423 traceback.format_exception(exc_type, exc_value, exc_traceback))
411 424
412 425 type_ = e.__class__.__name__
413 426 if type_ not in self.ALLOWED_EXCEPTIONS:
414 427 type_ = None
415 428
416 429 resp = {
417 430 'id': payload_id,
418 431 'error': {
419 432 'message': e.message,
420 433 'traceback': tb_info,
421 434 'org_exc': org_exc_name,
422 435 'org_exc_tb': org_exc_tb,
423 436 'type': type_
424 437 }
425 438 }
426 439 try:
427 440 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
428 441 except AttributeError:
429 442 pass
430 443 else:
431 444 resp = {
432 445 'id': payload_id,
433 446 'result': resp
434 447 }
435 448
436 449 return resp
437 450
438 451 def vcs_stream_view(self, request):
439 452 payload, remote, method, args, kwargs = self._vcs_view_params(request)
440 453 # this method has a stream: marker we remove it here
441 454 method = method.split('stream:')[-1]
442 455 chunk_size = safe_int(payload.get('chunk_size')) or 4096
443 456
444 457 try:
445 458 resp = getattr(remote, method)(*args, **kwargs)
446 459 except Exception as e:
447 460 raise
448 461
449 462 def get_chunked_data(method_resp):
450 463 stream = StringIO(method_resp)
451 464 while 1:
452 465 chunk = stream.read(chunk_size)
453 466 if not chunk:
454 467 break
455 468 yield chunk
456 469
457 470 response = Response(app_iter=get_chunked_data(resp))
458 471 response.content_type = 'application/octet-stream'
459 472
460 473 return response
461 474
462 475 def status_view(self, request):
463 476 import vcsserver
464 477 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
465 478 'pid': os.getpid()}
466 479
467 480 def service_view(self, request):
468 481 import vcsserver
469 482
470 483 payload = msgpack.unpackb(request.body, use_list=True)
471 484 server_config, app_config = {}, {}
472 485
473 486 try:
474 487 path = self.global_config['__file__']
475 488 config = configparser.RawConfigParser()
476 489
477 490 config.read(path)
478 491
479 492 if config.has_section('server:main'):
480 493 server_config = dict(config.items('server:main'))
481 494 if config.has_section('app:main'):
482 495 app_config = dict(config.items('app:main'))
483 496
484 497 except Exception:
485 498 log.exception('Failed to read .ini file for display')
486 499
487 500 environ = os.environ.items()
488 501
489 502 resp = {
490 503 'id': payload.get('id'),
491 504 'result': dict(
492 505 version=vcsserver.__version__,
493 506 config=server_config,
494 507 app_config=app_config,
495 508 environ=environ,
496 509 payload=payload,
497 510 )
498 511 }
499 512 return resp
500 513
501 514 def _msgpack_renderer_factory(self, info):
502 515 def _render(value, system):
503 516 request = system.get('request')
504 517 if request is not None:
505 518 response = request.response
506 519 ct = response.content_type
507 520 if ct == response.default_content_type:
508 521 response.content_type = 'application/x-msgpack'
509 522 return msgpack.packb(value)
510 523 return _render
511 524
512 525 def set_env_from_config(self, environ, config):
513 526 dict_conf = {}
514 527 try:
515 528 for elem in config:
516 529 if elem[0] == 'rhodecode':
517 530 dict_conf = json.loads(elem[2])
518 531 break
519 532 except Exception:
520 533 log.exception('Failed to fetch SCM CONFIG')
521 534 return
522 535
523 536 username = dict_conf.get('username')
524 537 if username:
525 538 environ['REMOTE_USER'] = username
526 539 # mercurial specific, some extension api rely on this
527 540 environ['HGUSER'] = username
528 541
529 542 ip = dict_conf.get('ip')
530 543 if ip:
531 544 environ['REMOTE_HOST'] = ip
532 545
533 546 if _is_request_chunked(environ):
534 547 # set the compatibility flag for webob
535 548 environ['wsgi.input_terminated'] = True
536 549
537 550 def hg_proxy(self):
538 551 @wsgiapp
539 552 def _hg_proxy(environ, start_response):
540 553 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
541 554 return app(environ, start_response)
542 555 return _hg_proxy
543 556
544 557 def git_proxy(self):
545 558 @wsgiapp
546 559 def _git_proxy(environ, start_response):
547 560 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
548 561 return app(environ, start_response)
549 562 return _git_proxy
550 563
551 564 def hg_stream(self):
552 565 if self._use_echo_app:
553 566 @wsgiapp
554 567 def _hg_stream(environ, start_response):
555 568 app = EchoApp('fake_path', 'fake_name', None)
556 569 return app(environ, start_response)
557 570 return _hg_stream
558 571 else:
559 572 @wsgiapp
560 573 def _hg_stream(environ, start_response):
561 574 log.debug('http-app: handling hg stream')
562 575 repo_path = environ['HTTP_X_RC_REPO_PATH']
563 576 repo_name = environ['HTTP_X_RC_REPO_NAME']
564 577 packed_config = base64.b64decode(
565 578 environ['HTTP_X_RC_REPO_CONFIG'])
566 579 config = msgpack.unpackb(packed_config)
567 580 app = scm_app.create_hg_wsgi_app(
568 581 repo_path, repo_name, config)
569 582
570 583 # Consistent path information for hgweb
571 584 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
572 585 environ['REPO_NAME'] = repo_name
573 586 self.set_env_from_config(environ, config)
574 587
575 588 log.debug('http-app: starting app handler '
576 589 'with %s and process request', app)
577 590 return app(environ, ResponseFilter(start_response))
578 591 return _hg_stream
579 592
580 593 def git_stream(self):
581 594 if self._use_echo_app:
582 595 @wsgiapp
583 596 def _git_stream(environ, start_response):
584 597 app = EchoApp('fake_path', 'fake_name', None)
585 598 return app(environ, start_response)
586 599 return _git_stream
587 600 else:
588 601 @wsgiapp
589 602 def _git_stream(environ, start_response):
590 603 log.debug('http-app: handling git stream')
591 604 repo_path = environ['HTTP_X_RC_REPO_PATH']
592 605 repo_name = environ['HTTP_X_RC_REPO_NAME']
593 606 packed_config = base64.b64decode(
594 607 environ['HTTP_X_RC_REPO_CONFIG'])
595 608 config = msgpack.unpackb(packed_config)
596 609
597 610 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
598 611 self.set_env_from_config(environ, config)
599 612
600 613 content_type = environ.get('CONTENT_TYPE', '')
601 614
602 615 path = environ['PATH_INFO']
603 616 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
604 617 log.debug(
605 618 'LFS: Detecting if request `%s` is LFS server path based '
606 619 'on content type:`%s`, is_lfs:%s',
607 620 path, content_type, is_lfs_request)
608 621
609 622 if not is_lfs_request:
610 623 # fallback detection by path
611 624 if GIT_LFS_PROTO_PAT.match(path):
612 625 is_lfs_request = True
613 626 log.debug(
614 627 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
615 628 path, is_lfs_request)
616 629
617 630 if is_lfs_request:
618 631 app = scm_app.create_git_lfs_wsgi_app(
619 632 repo_path, repo_name, config)
620 633 else:
621 634 app = scm_app.create_git_wsgi_app(
622 635 repo_path, repo_name, config)
623 636
624 637 log.debug('http-app: starting app handler '
625 638 'with %s and process request', app)
626 639
627 640 return app(environ, start_response)
628 641
629 642 return _git_stream
630 643
631 644 def handle_vcs_exception(self, exception, request):
632 645 _vcs_kind = getattr(exception, '_vcs_kind', '')
633 646 if _vcs_kind == 'repo_locked':
634 647 # Get custom repo-locked status code if present.
635 648 status_code = request.headers.get('X-RC-Locked-Status-Code')
636 649 return HTTPRepoLocked(
637 650 title=exception.message, status_code=status_code)
638 651
639 652 elif _vcs_kind == 'repo_branch_protected':
640 653 # Get custom repo-branch-protected status code if present.
641 654 return HTTPRepoBranchProtected(title=exception.message)
642 655
643 656 exc_info = request.exc_info
644 657 store_exception(id(exc_info), exc_info)
645 658
646 659 traceback_info = 'unavailable'
647 660 if request.exc_info:
648 661 exc_type, exc_value, exc_tb = request.exc_info
649 662 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
650 663
651 664 log.error(
652 665 'error occurred handling this request for path: %s, \n tb: %s',
653 666 request.path, traceback_info)
654 667 raise exception
655 668
656 669
657 670 class ResponseFilter(object):
658 671
659 672 def __init__(self, start_response):
660 673 self._start_response = start_response
661 674
662 675 def __call__(self, status, response_headers, exc_info=None):
663 676 headers = tuple(
664 677 (h, v) for h, v in response_headers
665 678 if not wsgiref.util.is_hop_by_hop(h))
666 679 return self._start_response(status, headers, exc_info)
667 680
668 681
669 682 def main(global_config, **settings):
670 683 if MercurialFactory:
671 684 hgpatches.patch_largefiles_capabilities()
672 685 hgpatches.patch_subrepo_type_mapping()
673 686
674 687 app = HTTPApplication(settings=settings, global_config=global_config)
675 688 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now