##// END OF EJS Templates
libgit2: always cleanup after opening a repository, this way we don't lock the repo in multi-worker env
marcink -
r731:34168bd1 default
parent child Browse files
Show More
@@ -1,873 +1,891 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 import collections
18 18 import logging
19 19 import os
20 20 import posixpath as vcspath
21 21 import re
22 22 import stat
23 23 import traceback
24 24 import urllib
25 25 import urllib2
26 26 from functools import wraps
27 27
28 28 import more_itertools
29 29 import pygit2
30 30 from pygit2 import Repository as LibGit2Repo
31 31 from dulwich import index, objects
32 32 from dulwich.client import HttpGitClient, LocalGitClient
33 33 from dulwich.errors import (
34 34 NotGitRepository, ChecksumMismatch, WrongObjectException,
35 35 MissingCommitError, ObjectMissing, HangupException,
36 36 UnexpectedCommandError)
37 37 from dulwich.repo import Repo as DulwichRepo
38 38 from dulwich.server import update_server_info
39 39
40 40 from vcsserver import exceptions, settings, subprocessio
41 41 from vcsserver.utils import safe_str
42 42 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
43 43 from vcsserver.hgcompat import (
44 44 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
45 45 from vcsserver.git_lfs.lib import LFSOidStore
46 46
47 47 DIR_STAT = stat.S_IFDIR
48 48 FILE_MODE = stat.S_IFMT
49 49 GIT_LINK = objects.S_IFGITLINK
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 def reraise_safe_exceptions(func):
55 55 """Converts Dulwich exceptions to something neutral."""
56 56
57 57 @wraps(func)
58 58 def wrapper(*args, **kwargs):
59 59 try:
60 60 return func(*args, **kwargs)
61 61 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
62 62 exc = exceptions.LookupException(org_exc=e)
63 63 raise exc(safe_str(e))
64 64 except (HangupException, UnexpectedCommandError) as e:
65 65 exc = exceptions.VcsException(org_exc=e)
66 66 raise exc(safe_str(e))
67 67 except Exception as e:
68 68 # NOTE(marcink): becuase of how dulwich handles some exceptions
69 69 # (KeyError on empty repos), we cannot track this and catch all
70 70 # exceptions, it's an exceptions from other handlers
71 71 #if not hasattr(e, '_vcs_kind'):
72 72 #log.exception("Unhandled exception in git remote call")
73 73 #raise_from_original(exceptions.UnhandledException)
74 74 raise
75 75 return wrapper
76 76
77 77
78 78 class Repo(DulwichRepo):
79 79 """
80 80 A wrapper for dulwich Repo class.
81 81
82 82 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
83 83 "Too many open files" error. We need to close all opened file descriptors
84 84 once the repo object is destroyed.
85 85 """
86 86 def __del__(self):
87 87 if hasattr(self, 'object_store'):
88 88 self.close()
89 89
90 90
91 91 class Repository(LibGit2Repo):
92 92
93 93 def __enter__(self):
94 94 return self
95 95
96 96 def __exit__(self, exc_type, exc_val, exc_tb):
97 97 self.free()
98 98
99 99
100 100 class GitFactory(RepoFactory):
101 101 repo_type = 'git'
102 102
103 103 def _create_repo(self, wire, create, use_libgit2=False):
104 104 if use_libgit2:
105 105 return Repository(wire['path'])
106 106 else:
107 107 repo_path = str_to_dulwich(wire['path'])
108 108 return Repo(repo_path)
109 109
110 110 def repo(self, wire, create=False, use_libgit2=False):
111 111 """
112 112 Get a repository instance for the given path.
113 113 """
114 114 region = self._cache_region
115 115 context = wire.get('context', None)
116 116 repo_path = wire.get('path', '')
117 117 context_uid = '{}'.format(context)
118 118 cache = wire.get('cache', True)
119 119 cache_on = context and cache
120 120
121 121 @region.conditional_cache_on_arguments(condition=cache_on)
122 122 def create_new_repo(_repo_type, _repo_path, _context_uid, _use_libgit2):
123 123 return self._create_repo(wire, create, use_libgit2)
124 124
125 125 repo = create_new_repo(self.repo_type, repo_path, context_uid, use_libgit2)
126 126 return repo
127 127
128 128 def repo_libgit2(self, wire):
129 129 return self.repo(wire, use_libgit2=True)
130 130
131 131
132 132 class GitRemote(object):
133 133
134 134 def __init__(self, factory):
135 135 self._factory = factory
136 136 self.peeled_ref_marker = '^{}'
137 137 self._bulk_methods = {
138 138 "date": self.date,
139 139 "author": self.author,
140 140 "message": self.message,
141 141 "parents": self.parents,
142 142 "_commit": self.revision,
143 143 }
144 144
145 145 def _wire_to_config(self, wire):
146 146 if 'config' in wire:
147 147 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
148 148 return {}
149 149
150 150 def _remote_conf(self, config):
151 151 params = [
152 152 '-c', 'core.askpass=""',
153 153 ]
154 154 ssl_cert_dir = config.get('vcs_ssl_dir')
155 155 if ssl_cert_dir:
156 156 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
157 157 return params
158 158
159 159 @reraise_safe_exceptions
160 160 def is_empty(self, wire):
161 repo = self._factory.repo_libgit2(wire)
162
163 try:
164 has_head = repo.head.name
165 if has_head:
166 return False
161 repo_init = self._factory.repo_libgit2(wire)
162 with repo_init as repo:
167 163
168 # NOTE(marcink): check again using more expensive method
169 return repo.is_empty
170 except Exception:
171 pass
164 try:
165 has_head = repo.head.name
166 if has_head:
167 return False
172 168
173 return True
169 # NOTE(marcink): check again using more expensive method
170 return repo.is_empty
171 except Exception:
172 pass
173
174 return True
174 175
175 176 @reraise_safe_exceptions
176 177 def add_object(self, wire, content):
177 repo = self._factory.repo(wire)
178 blob = objects.Blob()
179 blob.set_raw_string(content)
180 repo.object_store.add_object(blob)
181 return blob.id
178 repo_init = self._factory.repo_libgit2(wire)
179 with repo_init as repo:
180 blob = objects.Blob()
181 blob.set_raw_string(content)
182 repo.object_store.add_object(blob)
183 return blob.id
182 184
183 185 @reraise_safe_exceptions
184 186 def assert_correct_path(self, wire):
185 187 try:
186 self._factory.repo_libgit2(wire)
188 repo_init = self._factory.repo_libgit2(wire)
189 with repo_init as repo:
190 pass
187 191 except pygit2.GitError:
188 192 path = wire.get('path')
189 193 tb = traceback.format_exc()
190 194 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
191 195 return False
192 196
193 197 return True
194 198
195 199 @reraise_safe_exceptions
196 200 def bare(self, wire):
197 repo = self._factory.repo_libgit2(wire)
198 return repo.is_bare
201 repo_init = self._factory.repo_libgit2(wire)
202 with repo_init as repo:
203 return repo.is_bare
199 204
200 205 @reraise_safe_exceptions
201 206 def blob_as_pretty_string(self, wire, sha):
202 207 repo_init = self._factory.repo_libgit2(wire)
203 208 with repo_init as repo:
204 209 blob_obj = repo[sha]
205 210 blob = blob_obj.data
206 211 return blob
207 212
208 213 @reraise_safe_exceptions
209 214 def blob_raw_length(self, wire, sha):
210 215 repo_init = self._factory.repo_libgit2(wire)
211 216 with repo_init as repo:
212 217 blob = repo[sha]
213 218 return blob.size
214 219
215 220 def _parse_lfs_pointer(self, raw_content):
216 221
217 222 spec_string = 'version https://git-lfs.github.com/spec'
218 223 if raw_content and raw_content.startswith(spec_string):
219 224 pattern = re.compile(r"""
220 225 (?:\n)?
221 226 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
222 227 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
223 228 ^size[ ](?P<oid_size>[0-9]+)\n
224 229 (?:\n)?
225 230 """, re.VERBOSE | re.MULTILINE)
226 231 match = pattern.match(raw_content)
227 232 if match:
228 233 return match.groupdict()
229 234
230 235 return {}
231 236
232 237 @reraise_safe_exceptions
233 238 def is_large_file(self, wire, sha):
234 239 repo_init = self._factory.repo_libgit2(wire)
235
236 240 with repo_init as repo:
237 241 blob = repo[sha]
238 242 if blob.is_binary:
239 243 return {}
240 244
241 245 return self._parse_lfs_pointer(blob.data)
242 246
243 247 @reraise_safe_exceptions
244 248 def in_largefiles_store(self, wire, oid):
245 repo = self._factory.repo_libgit2(wire)
246 249 conf = self._wire_to_config(wire)
250 repo_init = self._factory.repo_libgit2(wire)
251 with repo_init as repo:
252 repo_name = repo.path
247 253
248 254 store_location = conf.get('vcs_git_lfs_store_location')
249 255 if store_location:
250 repo_name = repo.path
256
251 257 store = LFSOidStore(
252 258 oid=oid, repo=repo_name, store_location=store_location)
253 259 return store.has_oid()
254 260
255 261 return False
256 262
257 263 @reraise_safe_exceptions
258 264 def store_path(self, wire, oid):
259 repo = self._factory.repo_libgit2(wire)
260 265 conf = self._wire_to_config(wire)
266 repo_init = self._factory.repo_libgit2(wire)
267 with repo_init as repo:
268 repo_name = repo.path
261 269
262 270 store_location = conf.get('vcs_git_lfs_store_location')
263 271 if store_location:
264 repo_name = repo.path
265 272 store = LFSOidStore(
266 273 oid=oid, repo=repo_name, store_location=store_location)
267 274 return store.oid_path
268 275 raise ValueError('Unable to fetch oid with path {}'.format(oid))
269 276
270 277 @reraise_safe_exceptions
271 278 def bulk_request(self, wire, rev, pre_load):
272 279 result = {}
273 280 for attr in pre_load:
274 281 try:
275 282 method = self._bulk_methods[attr]
276 283 args = [wire, rev]
277 284 result[attr] = method(*args)
278 285 except KeyError as e:
279 286 raise exceptions.VcsException(e)("Unknown bulk attribute: %s" % attr)
280 287 return result
281 288
282 289 def _build_opener(self, url):
283 290 handlers = []
284 291 url_obj = url_parser(url)
285 292 _, authinfo = url_obj.authinfo()
286 293
287 294 if authinfo:
288 295 # create a password manager
289 296 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
290 297 passmgr.add_password(*authinfo)
291 298
292 299 handlers.extend((httpbasicauthhandler(passmgr),
293 300 httpdigestauthhandler(passmgr)))
294 301
295 302 return urllib2.build_opener(*handlers)
296 303
297 304 def _type_id_to_name(self, type_id):
298 305 return {
299 306 1: b'commit',
300 307 2: b'tree',
301 308 3: b'blob',
302 309 4: b'tag'
303 310 }[type_id]
304 311
305 312 @reraise_safe_exceptions
306 313 def check_url(self, url, config):
307 314 url_obj = url_parser(url)
308 315 test_uri, _ = url_obj.authinfo()
309 316 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
310 317 url_obj.query = obfuscate_qs(url_obj.query)
311 318 cleaned_uri = str(url_obj)
312 319 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
313 320
314 321 if not test_uri.endswith('info/refs'):
315 322 test_uri = test_uri.rstrip('/') + '/info/refs'
316 323
317 324 o = self._build_opener(url)
318 325 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
319 326
320 327 q = {"service": 'git-upload-pack'}
321 328 qs = '?%s' % urllib.urlencode(q)
322 329 cu = "%s%s" % (test_uri, qs)
323 330 req = urllib2.Request(cu, None, {})
324 331
325 332 try:
326 333 log.debug("Trying to open URL %s", cleaned_uri)
327 334 resp = o.open(req)
328 335 if resp.code != 200:
329 336 raise exceptions.URLError()('Return Code is not 200')
330 337 except Exception as e:
331 338 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
332 339 # means it cannot be cloned
333 340 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
334 341
335 342 # now detect if it's proper git repo
336 343 gitdata = resp.read()
337 344 if 'service=git-upload-pack' in gitdata:
338 345 pass
339 346 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
340 347 # old style git can return some other format !
341 348 pass
342 349 else:
343 350 raise exceptions.URLError()(
344 351 "url [%s] does not look like an git" % (cleaned_uri,))
345 352
346 353 return True
347 354
348 355 @reraise_safe_exceptions
349 356 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
350 357 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
351 358 remote_refs = self.pull(wire, url, apply_refs=False)
352 359 repo = self._factory.repo(wire)
353 360 if isinstance(valid_refs, list):
354 361 valid_refs = tuple(valid_refs)
355 362
356 363 for k in remote_refs:
357 364 # only parse heads/tags and skip so called deferred tags
358 365 if k.startswith(valid_refs) and not k.endswith(deferred):
359 366 repo[k] = remote_refs[k]
360 367
361 368 if update_after_clone:
362 369 # we want to checkout HEAD
363 370 repo["HEAD"] = remote_refs["HEAD"]
364 371 index.build_index_from_tree(repo.path, repo.index_path(),
365 372 repo.object_store, repo["HEAD"].tree)
366 373
367 374 # TODO: this is quite complex, check if that can be simplified
368 375 @reraise_safe_exceptions
369 376 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
370 377 repo = self._factory.repo(wire)
371 378 object_store = repo.object_store
372 379
373 380 # Create tree and populates it with blobs
374 381 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
375 382
376 383 for node in updated:
377 384 # Compute subdirs if needed
378 385 dirpath, nodename = vcspath.split(node['path'])
379 386 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
380 387 parent = commit_tree
381 388 ancestors = [('', parent)]
382 389
383 390 # Tries to dig for the deepest existing tree
384 391 while dirnames:
385 392 curdir = dirnames.pop(0)
386 393 try:
387 394 dir_id = parent[curdir][1]
388 395 except KeyError:
389 396 # put curdir back into dirnames and stops
390 397 dirnames.insert(0, curdir)
391 398 break
392 399 else:
393 400 # If found, updates parent
394 401 parent = repo[dir_id]
395 402 ancestors.append((curdir, parent))
396 403 # Now parent is deepest existing tree and we need to create
397 404 # subtrees for dirnames (in reverse order)
398 405 # [this only applies for nodes from added]
399 406 new_trees = []
400 407
401 408 blob = objects.Blob.from_string(node['content'])
402 409
403 410 if dirnames:
404 411 # If there are trees which should be created we need to build
405 412 # them now (in reverse order)
406 413 reversed_dirnames = list(reversed(dirnames))
407 414 curtree = objects.Tree()
408 415 curtree[node['node_path']] = node['mode'], blob.id
409 416 new_trees.append(curtree)
410 417 for dirname in reversed_dirnames[:-1]:
411 418 newtree = objects.Tree()
412 419 newtree[dirname] = (DIR_STAT, curtree.id)
413 420 new_trees.append(newtree)
414 421 curtree = newtree
415 422 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
416 423 else:
417 424 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
418 425
419 426 new_trees.append(parent)
420 427 # Update ancestors
421 428 reversed_ancestors = reversed(
422 429 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
423 430 for parent, tree, path in reversed_ancestors:
424 431 parent[path] = (DIR_STAT, tree.id)
425 432 object_store.add_object(tree)
426 433
427 434 object_store.add_object(blob)
428 435 for tree in new_trees:
429 436 object_store.add_object(tree)
430 437
431 438 for node_path in removed:
432 439 paths = node_path.split('/')
433 440 tree = commit_tree
434 441 trees = [tree]
435 442 # Traverse deep into the forest...
436 443 for path in paths:
437 444 try:
438 445 obj = repo[tree[path][1]]
439 446 if isinstance(obj, objects.Tree):
440 447 trees.append(obj)
441 448 tree = obj
442 449 except KeyError:
443 450 break
444 451 # Cut down the blob and all rotten trees on the way back...
445 452 for path, tree in reversed(zip(paths, trees)):
446 453 del tree[path]
447 454 if tree:
448 455 # This tree still has elements - don't remove it or any
449 456 # of it's parents
450 457 break
451 458
452 459 object_store.add_object(commit_tree)
453 460
454 461 # Create commit
455 462 commit = objects.Commit()
456 463 commit.tree = commit_tree.id
457 464 for k, v in commit_data.iteritems():
458 465 setattr(commit, k, v)
459 466 object_store.add_object(commit)
460 467
461 468 self.create_branch(wire, branch, commit.id)
462 469
463 470 # dulwich set-ref
464 471 ref = 'refs/heads/%s' % branch
465 472 repo.refs[ref] = commit.id
466 473
467 474 return commit.id
468 475
469 476 @reraise_safe_exceptions
470 477 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
471 478 if url != 'default' and '://' not in url:
472 479 client = LocalGitClient(url)
473 480 else:
474 481 url_obj = url_parser(url)
475 482 o = self._build_opener(url)
476 483 url, _ = url_obj.authinfo()
477 484 client = HttpGitClient(base_url=url, opener=o)
478 485 repo = self._factory.repo(wire)
479 486
480 487 determine_wants = repo.object_store.determine_wants_all
481 488 if refs:
482 489 def determine_wants_requested(references):
483 490 return [references[r] for r in references if r in refs]
484 491 determine_wants = determine_wants_requested
485 492
486 493 try:
487 494 remote_refs = client.fetch(
488 495 path=url, target=repo, determine_wants=determine_wants)
489 496 except NotGitRepository as e:
490 497 log.warning(
491 498 'Trying to fetch from "%s" failed, not a Git repository.', url)
492 499 # Exception can contain unicode which we convert
493 500 raise exceptions.AbortException(e)(repr(e))
494 501
495 502 # mikhail: client.fetch() returns all the remote refs, but fetches only
496 503 # refs filtered by `determine_wants` function. We need to filter result
497 504 # as well
498 505 if refs:
499 506 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
500 507
501 508 if apply_refs:
502 509 # TODO: johbo: Needs proper test coverage with a git repository
503 510 # that contains a tag object, so that we would end up with
504 511 # a peeled ref at this point.
505 512 for k in remote_refs:
506 513 if k.endswith(self.peeled_ref_marker):
507 514 log.debug("Skipping peeled reference %s", k)
508 515 continue
509 516 repo[k] = remote_refs[k]
510 517
511 518 if refs and not update_after:
512 519 # mikhail: explicitly set the head to the last ref.
513 520 repo['HEAD'] = remote_refs[refs[-1]]
514 521
515 522 if update_after:
516 523 # we want to checkout HEAD
517 524 repo["HEAD"] = remote_refs["HEAD"]
518 525 index.build_index_from_tree(repo.path, repo.index_path(),
519 526 repo.object_store, repo["HEAD"].tree)
520 527 return remote_refs
521 528
522 529 @reraise_safe_exceptions
523 530 def sync_fetch(self, wire, url, refs=None):
524 531 repo = self._factory.repo(wire)
525 532 if refs and not isinstance(refs, (list, tuple)):
526 533 refs = [refs]
527 534 config = self._wire_to_config(wire)
528 535 # get all remote refs we'll use to fetch later
529 536 output, __ = self.run_git_command(
530 537 wire, ['ls-remote', url], fail_on_stderr=False,
531 538 _copts=self._remote_conf(config),
532 539 extra_env={'GIT_TERMINAL_PROMPT': '0'})
533 540
534 541 remote_refs = collections.OrderedDict()
535 542 fetch_refs = []
536 543
537 544 for ref_line in output.splitlines():
538 545 sha, ref = ref_line.split('\t')
539 546 sha = sha.strip()
540 547 if ref in remote_refs:
541 548 # duplicate, skip
542 549 continue
543 550 if ref.endswith(self.peeled_ref_marker):
544 551 log.debug("Skipping peeled reference %s", ref)
545 552 continue
546 553 # don't sync HEAD
547 554 if ref in ['HEAD']:
548 555 continue
549 556
550 557 remote_refs[ref] = sha
551 558
552 559 if refs and sha in refs:
553 560 # we filter fetch using our specified refs
554 561 fetch_refs.append('{}:{}'.format(ref, ref))
555 562 elif not refs:
556 563 fetch_refs.append('{}:{}'.format(ref, ref))
557 564 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
558 565 if fetch_refs:
559 566 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
560 567 fetch_refs_chunks = list(chunk)
561 568 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
562 569 _out, _err = self.run_git_command(
563 570 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
564 571 fail_on_stderr=False,
565 572 _copts=self._remote_conf(config),
566 573 extra_env={'GIT_TERMINAL_PROMPT': '0'})
567 574
568 575 return remote_refs
569 576
570 577 @reraise_safe_exceptions
571 578 def sync_push(self, wire, url, refs=None):
572 579 if not self.check_url(url, wire):
573 580 return
574 581 config = self._wire_to_config(wire)
575 582 repo = self._factory.repo(wire)
576 583 self.run_git_command(
577 584 wire, ['push', url, '--mirror'], fail_on_stderr=False,
578 585 _copts=self._remote_conf(config),
579 586 extra_env={'GIT_TERMINAL_PROMPT': '0'})
580 587
581 588 @reraise_safe_exceptions
582 589 def get_remote_refs(self, wire, url):
583 590 repo = Repo(url)
584 591 return repo.get_refs()
585 592
586 593 @reraise_safe_exceptions
587 594 def get_description(self, wire):
588 595 repo = self._factory.repo(wire)
589 596 return repo.get_description()
590 597
591 598 @reraise_safe_exceptions
592 599 def get_missing_revs(self, wire, rev1, rev2, path2):
593 600 repo = self._factory.repo(wire)
594 601 LocalGitClient(thin_packs=False).fetch(path2, repo)
595 602
596 603 wire_remote = wire.copy()
597 604 wire_remote['path'] = path2
598 605 repo_remote = self._factory.repo(wire_remote)
599 606 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
600 607
601 608 revs = [
602 609 x.commit.id
603 610 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
604 611 return revs
605 612
606 613 @reraise_safe_exceptions
607 614 def get_object(self, wire, sha):
608 repo = self._factory.repo_libgit2(wire)
615 repo_init = self._factory.repo_libgit2(wire)
616 with repo_init as repo:
609 617
610 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
611 try:
612 commit = repo.revparse_single(sha)
613 except (KeyError, ValueError) as e:
614 raise exceptions.LookupException(e)(missing_commit_err)
618 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
619 try:
620 commit = repo.revparse_single(sha)
621 except (KeyError, ValueError) as e:
622 raise exceptions.LookupException(e)(missing_commit_err)
615 623
616 if isinstance(commit, pygit2.Tag):
617 commit = repo.get(commit.target)
624 if isinstance(commit, pygit2.Tag):
625 commit = repo.get(commit.target)
618 626
619 # check for dangling commit
620 branches = [x for x in repo.branches.with_commit(commit.hex)]
621 if not branches:
622 raise exceptions.LookupException(None)(missing_commit_err)
627 # check for dangling commit
628 branches = [x for x in repo.branches.with_commit(commit.hex)]
629 if not branches:
630 raise exceptions.LookupException(None)(missing_commit_err)
623 631
624 commit_id = commit.hex
625 type_id = commit.type
632 commit_id = commit.hex
633 type_id = commit.type
626 634
627 return {
628 'id': commit_id,
629 'type': self._type_id_to_name(type_id),
630 'commit_id': commit_id,
631 'idx': 0
632 }
635 return {
636 'id': commit_id,
637 'type': self._type_id_to_name(type_id),
638 'commit_id': commit_id,
639 'idx': 0
640 }
633 641
634 642 @reraise_safe_exceptions
635 643 def get_refs(self, wire):
636 repo = self._factory.repo_libgit2(wire)
644 repo_init = self._factory.repo_libgit2(wire)
645 with repo_init as repo:
646 result = {}
647 for ref in repo.references:
648 peeled_sha = repo.lookup_reference(ref).peel()
649 result[ref] = peeled_sha.hex
637 650
638 result = {}
639 for ref in repo.references:
640 peeled_sha = repo.lookup_reference(ref).peel()
641 result[ref] = peeled_sha.hex
642
643 return result
651 return result
644 652
645 653 @reraise_safe_exceptions
646 654 def head(self, wire, show_exc=True):
647 repo = self._factory.repo_libgit2(wire)
648 try:
649 return repo.head.peel().hex
650 except Exception:
651 if show_exc:
652 raise
655 repo_init = self._factory.repo_libgit2(wire)
656 with repo_init as repo:
657 try:
658 return repo.head.peel().hex
659 except Exception:
660 if show_exc:
661 raise
653 662
654 663 @reraise_safe_exceptions
655 664 def init(self, wire):
656 665 repo_path = str_to_dulwich(wire['path'])
657 666 self.repo = Repo.init(repo_path)
658 667
659 668 @reraise_safe_exceptions
660 669 def init_bare(self, wire):
661 670 repo_path = str_to_dulwich(wire['path'])
662 671 self.repo = Repo.init_bare(repo_path)
663 672
664 673 @reraise_safe_exceptions
665 674 def revision(self, wire, rev):
666 repo = self._factory.repo_libgit2(wire)
667 commit = repo[rev]
668 obj_data = {
669 'id': commit.id.hex,
670 }
671 # tree objects itself don't have tree_id attribute
672 if hasattr(commit, 'tree_id'):
673 obj_data['tree'] = commit.tree_id.hex
675 repo_init = self._factory.repo_libgit2(wire)
676 with repo_init as repo:
677 commit = repo[rev]
678 obj_data = {
679 'id': commit.id.hex,
680 }
681 # tree objects itself don't have tree_id attribute
682 if hasattr(commit, 'tree_id'):
683 obj_data['tree'] = commit.tree_id.hex
674 684
675 return obj_data
685 return obj_data
676 686
677 687 @reraise_safe_exceptions
678 688 def date(self, wire, rev):
679 repo = self._factory.repo_libgit2(wire)
680 commit = repo[rev]
681 # TODO(marcink): check dulwich difference of offset vs timezone
682 return [commit.commit_time, commit.commit_time_offset]
689 repo_init = self._factory.repo_libgit2(wire)
690 with repo_init as repo:
691 commit = repo[rev]
692 # TODO(marcink): check dulwich difference of offset vs timezone
693 return [commit.commit_time, commit.commit_time_offset]
683 694
684 695 @reraise_safe_exceptions
685 696 def author(self, wire, rev):
686 repo = self._factory.repo_libgit2(wire)
687 commit = repo[rev]
688 if commit.author.email:
689 return u"{} <{}>".format(commit.author.name, commit.author.email)
697 repo_init = self._factory.repo_libgit2(wire)
698 with repo_init as repo:
699 commit = repo[rev]
700 if commit.author.email:
701 return u"{} <{}>".format(commit.author.name, commit.author.email)
690 702
691 return u"{}".format(commit.author.raw_name)
703 return u"{}".format(commit.author.raw_name)
692 704
693 705 @reraise_safe_exceptions
694 706 def message(self, wire, rev):
695 repo = self._factory.repo_libgit2(wire)
696 commit = repo[rev]
697 return commit.message
707 repo_init = self._factory.repo_libgit2(wire)
708 with repo_init as repo:
709 commit = repo[rev]
710 return commit.message
698 711
699 712 @reraise_safe_exceptions
700 713 def parents(self, wire, rev):
701 repo = self._factory.repo_libgit2(wire)
702 commit = repo[rev]
703 return [x.hex for x in commit.parent_ids]
714 repo_init = self._factory.repo_libgit2(wire)
715 with repo_init as repo:
716 commit = repo[rev]
717 return [x.hex for x in commit.parent_ids]
704 718
705 719 @reraise_safe_exceptions
706 720 def set_refs(self, wire, key, value):
707 repo = self._factory.repo_libgit2(wire)
708 repo.references.create(key, value, force=True)
721 repo_init = self._factory.repo_libgit2(wire)
722 with repo_init as repo:
723 repo.references.create(key, value, force=True)
709 724
710 725 @reraise_safe_exceptions
711 726 def create_branch(self, wire, branch_name, commit_id, force=False):
712 repo = self._factory.repo_libgit2(wire)
713 commit = repo[commit_id]
727 repo_init = self._factory.repo_libgit2(wire)
728 with repo_init as repo:
729 commit = repo[commit_id]
714 730
715 if force:
716 repo.branches.local.create(branch_name, commit, force=force)
717 elif not repo.branches.get(branch_name):
718 # create only if that branch isn't existing
719 repo.branches.local.create(branch_name, commit, force=force)
731 if force:
732 repo.branches.local.create(branch_name, commit, force=force)
733 elif not repo.branches.get(branch_name):
734 # create only if that branch isn't existing
735 repo.branches.local.create(branch_name, commit, force=force)
720 736
721 737 @reraise_safe_exceptions
722 738 def remove_ref(self, wire, key):
723 repo = self._factory.repo_libgit2(wire)
724 repo.references.delete(key)
739 repo_init = self._factory.repo_libgit2(wire)
740 with repo_init as repo:
741 repo.references.delete(key)
725 742
726 743 @reraise_safe_exceptions
727 744 def tag_remove(self, wire, tag_name):
728 repo = self._factory.repo_libgit2(wire)
729 key = 'refs/tags/{}'.format(tag_name)
730 repo.references.delete(key)
745 repo_init = self._factory.repo_libgit2(wire)
746 with repo_init as repo:
747 key = 'refs/tags/{}'.format(tag_name)
748 repo.references.delete(key)
731 749
732 750 @reraise_safe_exceptions
733 751 def tree_changes(self, wire, source_id, target_id):
734 752 # TODO(marcink): remove this seems it's only used by tests
735 753 repo = self._factory.repo(wire)
736 754 source = repo[source_id].tree if source_id else None
737 755 target = repo[target_id].tree
738 756 result = repo.object_store.tree_changes(source, target)
739 757 return list(result)
740 758
741 759 @reraise_safe_exceptions
742 760 def tree_and_type_for_path(self, wire, commit_id, path):
743 761 repo_init = self._factory.repo_libgit2(wire)
744 762
745 763 with repo_init as repo:
746 764 commit = repo[commit_id]
747 765 try:
748 766 tree = commit.tree[path]
749 767 except KeyError:
750 768 return None, None, None
751 769
752 770 return tree.id.hex, tree.type, tree.filemode
753 771
754 772 @reraise_safe_exceptions
755 773 def tree_items(self, wire, tree_id):
756 774 repo_init = self._factory.repo_libgit2(wire)
757 775
758 776 with repo_init as repo:
759 777 try:
760 778 tree = repo[tree_id]
761 779 except KeyError:
762 780 raise ObjectMissing('No tree with id: {}'.format(tree_id))
763 781
764 782 result = []
765 783 for item in tree:
766 784 item_sha = item.hex
767 785 item_mode = item.filemode
768 786 item_type = item.type
769 787
770 788 if item_type == 'commit':
771 789 # NOTE(marcink): submodules we translate to 'link' for backward compat
772 790 item_type = 'link'
773 791
774 792 result.append((item.name, item_mode, item_sha, item_type))
775 793 return result
776 794
777 795 @reraise_safe_exceptions
778 796 def update_server_info(self, wire):
779 797 repo = self._factory.repo(wire)
780 798 update_server_info(repo)
781 799
782 800 @reraise_safe_exceptions
783 801 def discover_git_version(self):
784 802 stdout, _ = self.run_git_command(
785 803 {}, ['--version'], _bare=True, _safe=True)
786 804 prefix = 'git version'
787 805 if stdout.startswith(prefix):
788 806 stdout = stdout[len(prefix):]
789 807 return stdout.strip()
790 808
791 809 @reraise_safe_exceptions
792 810 def get_all_commit_ids(self, wire):
793 811
794 812 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
795 813 try:
796 814 output, __ = self.run_git_command(wire, cmd)
797 815 return output.splitlines()
798 816 except Exception:
799 817 # Can be raised for empty repositories
800 818 return []
801 819
802 820 @reraise_safe_exceptions
803 821 def run_git_command(self, wire, cmd, **opts):
804 822 path = wire.get('path', None)
805 823
806 824 if path and os.path.isdir(path):
807 825 opts['cwd'] = path
808 826
809 827 if '_bare' in opts:
810 828 _copts = []
811 829 del opts['_bare']
812 830 else:
813 831 _copts = ['-c', 'core.quotepath=false', ]
814 832 safe_call = False
815 833 if '_safe' in opts:
816 834 # no exc on failure
817 835 del opts['_safe']
818 836 safe_call = True
819 837
820 838 if '_copts' in opts:
821 839 _copts.extend(opts['_copts'] or [])
822 840 del opts['_copts']
823 841
824 842 gitenv = os.environ.copy()
825 843 gitenv.update(opts.pop('extra_env', {}))
826 844 # need to clean fix GIT_DIR !
827 845 if 'GIT_DIR' in gitenv:
828 846 del gitenv['GIT_DIR']
829 847 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
830 848 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
831 849
832 850 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
833 851 _opts = {'env': gitenv, 'shell': False}
834 852
835 853 try:
836 854 _opts.update(opts)
837 855 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
838 856
839 857 return ''.join(p), ''.join(p.error)
840 858 except (EnvironmentError, OSError) as err:
841 859 cmd = ' '.join(cmd) # human friendly CMD
842 860 tb_err = ("Couldn't run git command (%s).\n"
843 861 "Original error was:%s\n"
844 862 "Call options:%s\n"
845 863 % (cmd, err, _opts))
846 864 log.exception(tb_err)
847 865 if safe_call:
848 866 return '', err
849 867 else:
850 868 raise exceptions.VcsException()(tb_err)
851 869
852 870 @reraise_safe_exceptions
853 871 def install_hooks(self, wire, force=False):
854 872 from vcsserver.hook_utils import install_git_hooks
855 873 repo = self._factory.repo(wire)
856 874 return install_git_hooks(repo.path, repo.bare, force_create=force)
857 875
858 876 @reraise_safe_exceptions
859 877 def get_hooks_info(self, wire):
860 878 from vcsserver.hook_utils import (
861 879 get_git_pre_hook_version, get_git_post_hook_version)
862 880 repo = self._factory.repo(wire)
863 881 return {
864 882 'pre_version': get_git_pre_hook_version(repo.path, repo.bare),
865 883 'post_version': get_git_post_hook_version(repo.path, repo.bare),
866 884 }
867 885
868 886
869 887 def str_to_dulwich(value):
870 888 """
871 889 Dulwich 0.10.1a requires `unicode` objects to be passed in.
872 890 """
873 891 return value.decode(settings.WIRE_ENCODING)
General Comments 0
You need to be logged in to leave comments. Login now