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