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