github.com/whamcloud/lemur@v0.0.0-20190827193804-4655df8a52af/packaging/ci/lambda/GitPullS3/pygit2/repository.py (about) 1 # -*- coding: utf-8 -*- 2 # 3 # Copyright 2010-2015 The pygit2 contributors 4 # 5 # This file is free software; you can redistribute it and/or modify 6 # it under the terms of the GNU General Public License, version 2, 7 # as published by the Free Software Foundation. 8 # 9 # In addition to the permissions in the GNU General Public License, 10 # the authors give you unlimited permission to link the compiled 11 # version of this file into combinations with other programs, 12 # and to distribute those combinations without any restriction 13 # coming from the use of this file. (The General Public License 14 # restrictions do apply in other respects; for example, they cover 15 # modification of the file, and distribution when not linked into 16 # a combined executable.) 17 # 18 # This file is distributed in the hope that it will be useful, but 19 # WITHOUT ANY WARRANTY; without even the implied warranty of 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 21 # General Public License for more details. 22 # 23 # You should have received a copy of the GNU General Public License 24 # along with this program; see the file COPYING. If not, write to 25 # the Free Software Foundation, 51 Franklin Street, Fifth Floor, 26 # Boston, MA 02110-1301, USA. 27 28 # Import from the future 29 from __future__ import absolute_import 30 31 # Import from the Standard Library 32 from string import hexdigits 33 import sys, tarfile 34 from time import time 35 if sys.version_info[0] < 3: 36 from cStringIO import StringIO 37 else: 38 from io import BytesIO as StringIO 39 40 import six 41 42 # Import from pygit2 43 from _pygit2 import Repository as _Repository 44 from _pygit2 import Oid, GIT_OID_HEXSZ, GIT_OID_MINPREFIXLEN 45 from _pygit2 import GIT_CHECKOUT_SAFE, GIT_CHECKOUT_RECREATE_MISSING, GIT_DIFF_NORMAL 46 from _pygit2 import GIT_FILEMODE_LINK 47 from _pygit2 import Reference, Tree, Commit, Blob 48 49 from .config import Config 50 from .errors import check_error 51 from .ffi import ffi, C 52 from .index import Index 53 from .remote import RemoteCollection 54 from .blame import Blame 55 from .utils import to_bytes, is_string 56 from .submodule import Submodule 57 58 59 class Repository(_Repository): 60 61 def __init__(self, path, *args, **kwargs): 62 if not isinstance(path, six.string_types): 63 path = path.decode('utf-8') 64 super(Repository, self).__init__(path, *args, **kwargs) 65 self._common_init() 66 67 @classmethod 68 def _from_c(cls, ptr, owned): 69 cptr = ffi.new('git_repository **') 70 cptr[0] = ptr 71 repo = cls.__new__(cls) 72 super(cls, repo)._from_c(bytes(ffi.buffer(cptr)[:]), owned) 73 repo._common_init() 74 return repo 75 76 def _common_init(self): 77 self.remotes = RemoteCollection(self) 78 79 # Get the pointer as the contents of a buffer and store it for 80 # later access 81 repo_cptr = ffi.new('git_repository **') 82 ffi.buffer(repo_cptr)[:] = self._pointer[:] 83 self._repo = repo_cptr[0] 84 85 def lookup_submodule(self, path): 86 csub = ffi.new('git_submodule **') 87 cpath = ffi.new('char[]', to_bytes(path)) 88 89 err = C.git_submodule_lookup(csub, self._repo, cpath) 90 check_error(err) 91 return Submodule._from_c(self, csub[0]) 92 93 # 94 # Mapping interface 95 # 96 def get(self, key, default=None): 97 value = self.git_object_lookup_prefix(key) 98 return value if (value is not None) else default 99 100 def __getitem__(self, key): 101 value = self.git_object_lookup_prefix(key) 102 if value is None: 103 raise KeyError(key) 104 return value 105 106 def __contains__(self, key): 107 return self.git_object_lookup_prefix(key) is not None 108 109 def __repr__(self): 110 return "pygit2.Repository(%r)" % self.path 111 112 # 113 # Remotes 114 # 115 def create_remote(self, name, url): 116 """Create a new remote. Return a <Remote> object. 117 118 This method is deprecated, please use Remote.remotes.create() 119 """ 120 return self.remotes.create(name, url) 121 122 # 123 # Configuration 124 # 125 @property 126 def config(self): 127 """The configuration file for this repository. 128 129 If a the configuration hasn't been set yet, the default config for 130 repository will be returned, including global and system configurations 131 (if they are available). 132 """ 133 cconfig = ffi.new('git_config **') 134 err = C.git_repository_config(cconfig, self._repo) 135 check_error(err) 136 137 return Config.from_c(self, cconfig[0]) 138 139 @property 140 def config_snapshot(self): 141 """A snapshot for this repositiory's configuration 142 143 This allows reads over multiple values to use the same version 144 of the configuration files. 145 """ 146 cconfig = ffi.new('git_config **') 147 err = C.git_repository_config_snapshot(cconfig, self._repo) 148 check_error(err) 149 150 return Config.from_c(self, cconfig[0]) 151 152 # 153 # References 154 # 155 def create_reference(self, name, target, force=False): 156 """Create a new reference "name" which points to an object or to 157 another reference. 158 159 Based on the type and value of the target parameter, this method tries 160 to guess whether it is a direct or a symbolic reference. 161 162 Keyword arguments: 163 164 force 165 If True references will be overridden, otherwise (the default) an 166 exception is raised. 167 168 Examples:: 169 170 repo.create_reference('refs/heads/foo', repo.head.target) 171 repo.create_reference('refs/tags/foo', 'refs/heads/master') 172 repo.create_reference('refs/tags/foo', 'bbb78a9cec580') 173 """ 174 direct = ( 175 type(target) is Oid 176 or ( 177 all(c in hexdigits for c in target) 178 and GIT_OID_MINPREFIXLEN <= len(target) <= GIT_OID_HEXSZ)) 179 180 if direct: 181 return self.create_reference_direct(name, target, force) 182 183 return self.create_reference_symbolic(name, target, force) 184 185 # 186 # Checkout 187 # 188 @staticmethod 189 def _checkout_args_to_options(strategy=None, directory=None): 190 # Create the options struct to pass 191 copts = ffi.new('git_checkout_options *') 192 check_error(C.git_checkout_init_options(copts, 1)) 193 194 # References we need to keep to strings and so forth 195 refs = [] 196 197 # pygit2's default is SAFE | RECREATE_MISSING 198 copts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_RECREATE_MISSING 199 # and go through the arguments to see what the user wanted 200 if strategy: 201 copts.checkout_strategy = strategy 202 203 if directory: 204 target_dir = ffi.new('char[]', to_bytes(directory)) 205 refs.append(target_dir) 206 copts.target_directory = target_dir 207 208 return copts, refs 209 210 def checkout_head(self, **kwargs): 211 """Checkout HEAD 212 213 For arguments, see Repository.checkout(). 214 """ 215 copts, refs = Repository._checkout_args_to_options(**kwargs) 216 check_error(C.git_checkout_head(self._repo, copts)) 217 218 def checkout_index(self, **kwargs): 219 """Checkout the repository's index 220 221 For arguments, see Repository.checkout(). 222 """ 223 copts, refs = Repository._checkout_args_to_options(**kwargs) 224 check_error(C.git_checkout_index(self._repo, ffi.NULL, copts)) 225 226 def checkout_tree(self, treeish, **kwargs): 227 """Checkout the given treeish 228 229 For arguments, see Repository.checkout(). 230 """ 231 copts, refs = Repository._checkout_args_to_options(**kwargs) 232 cptr = ffi.new('git_object **') 233 ffi.buffer(cptr)[:] = treeish._pointer[:] 234 235 check_error(C.git_checkout_tree(self._repo, cptr[0], copts)) 236 237 def checkout(self, refname=None, **kwargs): 238 """ 239 Checkout the given reference using the given strategy, and update 240 the HEAD. 241 The reference may be a reference name or a Reference object. 242 The default strategy is GIT_CHECKOUT_SAFE | GIT_CHECKOUT_RECREATE_MISSING. 243 244 To checkout from the HEAD, just pass 'HEAD':: 245 246 >>> checkout('HEAD') 247 248 This is identical to calling checkout_head(). 249 250 If no reference is given, checkout from the index. 251 252 Arguments: 253 254 :param str|Reference refname: The reference to checkout. After checkout, 255 the current branch will be switched to this one. 256 257 :param int strategy: A ``GIT_CHECKOUT_`` value. The default is 258 ``GIT_CHECKOUT_SAFE``. 259 260 :param str directory: Alternative checkout path to workdir. 261 262 """ 263 264 # Case 1: Checkout index 265 if refname is None: 266 return self.checkout_index(**kwargs) 267 268 # Case 2: Checkout head 269 if refname == 'HEAD': 270 return self.checkout_head(**kwargs) 271 272 # Case 3: Reference 273 if isinstance(refname, Reference): 274 reference = refname 275 refname = refname.name 276 else: 277 reference = self.lookup_reference(refname) 278 279 oid = reference.resolve().target 280 treeish = self[oid] 281 self.checkout_tree(treeish, **kwargs) 282 head = self.lookup_reference('HEAD') 283 if head.type == C.GIT_REF_SYMBOLIC: 284 from_ = self.head.shorthand 285 else: 286 from_ = head.target.hex 287 288 self.set_head(refname) 289 290 # 291 # Setting HEAD 292 # 293 def set_head(self, target): 294 """Set HEAD to point to the given target 295 296 Arguments: 297 298 target 299 The new target for HEAD. Can be a string or Oid (to detach) 300 """ 301 302 if isinstance(target, Oid): 303 oid = ffi.new('git_oid *') 304 ffi.buffer(oid)[:] = target.raw[:] 305 err = C.git_repository_set_head_detached(self._repo, oid) 306 check_error(err) 307 return 308 309 # if it's a string, then it's a reference name 310 err = C.git_repository_set_head(self._repo, to_bytes(target)) 311 check_error(err) 312 313 # 314 # Diff 315 # 316 def diff(self, a=None, b=None, cached=False, flags=GIT_DIFF_NORMAL, 317 context_lines=3, interhunk_lines=0): 318 """ 319 Show changes between the working tree and the index or a tree, 320 changes between the index and a tree, changes between two trees, or 321 changes between two blobs. 322 323 Keyword arguments: 324 325 cached 326 use staged changes instead of workdir 327 328 flag 329 a GIT_DIFF_* constant 330 331 context_lines 332 the number of unchanged lines that define the boundary 333 of a hunk (and to display before and after) 334 335 interhunk_lines 336 the maximum number of unchanged lines between hunk 337 boundaries before the hunks will be merged into a one 338 339 Examples:: 340 341 # Changes in the working tree not yet staged for the next commit 342 >>> diff() 343 344 # Changes between the index and your last commit 345 >>> diff(cached=True) 346 347 # Changes in the working tree since your last commit 348 >>> diff('HEAD') 349 350 # Changes between commits 351 >>> t0 = revparse_single('HEAD') 352 >>> t1 = revparse_single('HEAD^') 353 >>> diff(t0, t1) 354 >>> diff('HEAD', 'HEAD^') # equivalent 355 356 If you want to diff a tree against an empty tree, use the low level 357 API (Tree.diff_to_tree()) directly. 358 """ 359 360 def whatever_to_tree_or_blob(obj): 361 if obj is None: 362 return None 363 364 # If it's a string, then it has to be valid revspec 365 if is_string(obj): 366 obj = self.revparse_single(obj) 367 368 # First we try to get to a blob 369 try: 370 obj = obj.peel(Blob) 371 except Exception: 372 # And if that failed, try to get a tree, raising a type 373 # error if that still doesn't work 374 try: 375 obj = obj.peel(Tree) 376 except Exception: 377 raise TypeError('unexpected "%s"' % type(obj)) 378 379 return obj 380 381 a = whatever_to_tree_or_blob(a) 382 b = whatever_to_tree_or_blob(b) 383 384 opt_keys = ['flags', 'context_lines', 'interhunk_lines'] 385 opt_values = [flags, context_lines, interhunk_lines] 386 387 # Case 1: Diff tree to tree 388 if isinstance(a, Tree) and isinstance(b, Tree): 389 return a.diff_to_tree(b, **dict(zip(opt_keys, opt_values))) 390 391 # Case 2: Index to workdir 392 elif a is None and b is None: 393 return self.index.diff_to_workdir(*opt_values) 394 395 # Case 3: Diff tree to index or workdir 396 elif isinstance(a, Tree) and b is None: 397 if cached: 398 return a.diff_to_index(self.index, *opt_values) 399 else: 400 return a.diff_to_workdir(*opt_values) 401 402 # Case 4: Diff blob to blob 403 if isinstance(a, Blob) and isinstance(b, Blob): 404 return a.diff(b) 405 406 raise ValueError("Only blobs and treeish can be diffed") 407 408 def state_cleanup(self): 409 """Remove all the metadata associated with an ongoing command like 410 merge, revert, cherry-pick, etc. For example: MERGE_HEAD, MERGE_MSG, 411 etc. 412 """ 413 C.git_repository_state_cleanup(self._repo) 414 415 # 416 # blame 417 # 418 def blame(self, path, flags=None, min_match_characters=None, 419 newest_commit=None, oldest_commit=None, min_line=None, 420 max_line=None): 421 """Return a Blame object for a single file. 422 423 Arguments: 424 425 path 426 Path to the file to blame. 427 flags 428 A GIT_BLAME_* constant. 429 min_match_characters 430 The number of alphanum chars that must be detected as moving/copying 431 within a file for it to associate those lines with the parent commit. 432 newest_commit 433 The id of the newest commit to consider. 434 oldest_commit 435 The id of the oldest commit to consider. 436 min_line 437 The first line in the file to blame. 438 max_line 439 The last line in the file to blame. 440 441 Examples:: 442 443 repo.blame('foo.c', flags=GIT_BLAME_TRACK_COPIES_SAME_FILE)"); 444 """ 445 446 options = ffi.new('git_blame_options *') 447 C.git_blame_init_options(options, C.GIT_BLAME_OPTIONS_VERSION) 448 if min_match_characters: 449 options.min_match_characters = min_match_characters 450 if newest_commit: 451 if not isinstance(newest_commit, Oid): 452 newest_commit = Oid(hex=newest_commit) 453 ffi.buffer(ffi.addressof(options, 'newest_commit'))[:] = newest_commit.raw 454 if oldest_commit: 455 if not isinstance(oldest_commit, Oid): 456 oldest_commit = Oid(hex=oldest_commit) 457 ffi.buffer(ffi.addressof(options, 'oldest_commit'))[:] = oldest_commit.raw 458 if min_line: 459 options.min_line = min_line 460 if max_line: 461 options.max_line = max_line 462 463 cblame = ffi.new('git_blame **') 464 err = C.git_blame_file(cblame, self._repo, to_bytes(path), options) 465 check_error(err) 466 467 return Blame._from_c(self, cblame[0]) 468 469 # 470 # Index 471 # 472 @property 473 def index(self): 474 """Index representing the repository's index file.""" 475 cindex = ffi.new('git_index **') 476 err = C.git_repository_index(cindex, self._repo) 477 check_error(err, True) 478 479 return Index.from_c(self, cindex) 480 481 # 482 # Merging 483 # 484 485 @staticmethod 486 def _merge_options(favor): 487 """Return a 'git_merge_opts *'""" 488 def favor_to_enum(favor): 489 if favor == 'normal': 490 return C.GIT_MERGE_FILE_FAVOR_NORMAL 491 elif favor == 'ours': 492 return C.GIT_MERGE_FILE_FAVOR_OURS 493 elif favor == 'theirs': 494 return C.GIT_MERGE_FILE_FAVOR_THEIRS 495 elif favor == 'union': 496 return C.GIT_MERGE_FILE_FAVOR_UNION 497 else: 498 return None 499 500 favor_val = favor_to_enum(favor) 501 if favor_val is None: 502 raise ValueError("unkown favor value %s" % favor) 503 504 opts = ffi.new('git_merge_options *') 505 err = C.git_merge_init_options(opts, C.GIT_MERGE_OPTIONS_VERSION) 506 check_error(err) 507 508 opts.file_favor = favor_val 509 510 return opts 511 512 def merge_file_from_index(self, ancestor, ours, theirs): 513 """Merge files from index. Return a string with the merge result 514 containing possible conflicts. 515 516 ancestor 517 The index entry which will be used as a common 518 ancestor. 519 ours 520 The index entry to take as "ours" or base. 521 theirs 522 The index entry which will be merged into "ours" 523 """ 524 cmergeresult = ffi.new('git_merge_file_result *') 525 526 cancestor, ancestor_str_ref = ( 527 ancestor._to_c() if ancestor is not None else (ffi.NULL, ffi.NULL)) 528 cours, ours_str_ref = ( 529 ours._to_c() if ours is not None else (ffi.NULL, ffi.NULL)) 530 ctheirs, theirs_str_ref = ( 531 theirs._to_c() if theirs is not None else (ffi.NULL, ffi.NULL)) 532 533 err = C.git_merge_file_from_index( 534 cmergeresult, self._repo, 535 cancestor, cours, ctheirs, 536 ffi.NULL); 537 check_error(err) 538 539 ret = ffi.string(cmergeresult.ptr, 540 cmergeresult.len).decode('utf-8') 541 C.git_merge_file_result_free(cmergeresult) 542 543 return ret 544 545 def merge_commits(self, ours, theirs, favor='normal'): 546 """Merge two arbitrary commits 547 548 Arguments: 549 550 ours 551 The commit to take as "ours" or base. 552 theirs 553 The commit which will be merged into "ours" 554 favor 555 How to deal with file-level conflicts. Can be one of 556 557 * normal (default). Conflicts will be preserved. 558 * ours. The "ours" side of the conflict region is used. 559 * theirs. The "theirs" side of the conflict region is used. 560 * union. Unique lines from each side will be used. 561 562 for all but NORMAL, the index will not record a conflict. 563 564 Both "ours" and "theirs" can be any object which peels to a commit or the id 565 (string or Oid) of an object which peels to a commit. 566 567 Returns an index with the result of the merge 568 569 """ 570 571 ours_ptr = ffi.new('git_commit **') 572 theirs_ptr = ffi.new('git_commit **') 573 cindex = ffi.new('git_index **') 574 575 if is_string(ours) or isinstance(ours, Oid): 576 ours = self[ours] 577 if is_string(theirs) or isinstance(theirs, Oid): 578 theirs = self[theirs] 579 580 ours = ours.peel(Commit) 581 theirs = theirs.peel(Commit) 582 583 opts = self._merge_options(favor) 584 585 ffi.buffer(ours_ptr)[:] = ours._pointer[:] 586 ffi.buffer(theirs_ptr)[:] = theirs._pointer[:] 587 588 err = C.git_merge_commits(cindex, self._repo, ours_ptr[0], theirs_ptr[0], opts) 589 check_error(err) 590 591 return Index.from_c(self, cindex) 592 593 def merge_trees(self, ancestor, ours, theirs, favor='normal'): 594 """Merge two trees 595 596 Arguments: 597 598 ancestor 599 The tree which is the common ancestor between 'ours' and 'theirs' 600 ours 601 The commit to take as "ours" or base. 602 theirs 603 The commit which will be merged into "ours" 604 favor 605 How to deal with file-level conflicts. Can be one of 606 607 * normal (default). Conflicts will be preserved. 608 * ours. The "ours" side of the conflict region is used. 609 * theirs. The "theirs" side of the conflict region is used. 610 * union. Unique lines from each side will be used. 611 612 for all but NORMAL, the index will not record a conflict. 613 614 Returns an Index that reflects the result of the merge. 615 """ 616 617 ancestor_ptr = ffi.new('git_tree **') 618 ours_ptr = ffi.new('git_tree **') 619 theirs_ptr = ffi.new('git_tree **') 620 cindex = ffi.new('git_index **') 621 622 if is_string(ancestor) or isinstance(ancestor, Oid): 623 ancestor = self[ancestor] 624 if is_string(ours) or isinstance(ours, Oid): 625 ours = self[ours] 626 if is_string(theirs) or isinstance(theirs, Oid): 627 theirs = self[theirs] 628 629 ancestor = ancestor.peel(Tree) 630 ours = ours.peel(Tree) 631 theirs = theirs.peel(Tree) 632 633 opts = self._merge_options(favor) 634 635 ffi.buffer(ancestor_ptr)[:] = ancestor._pointer[:] 636 ffi.buffer(ours_ptr)[:] = ours._pointer[:] 637 ffi.buffer(theirs_ptr)[:] = theirs._pointer[:] 638 639 err = C.git_merge_trees(cindex, self._repo, ancestor_ptr[0], ours_ptr[0], theirs_ptr[0], opts) 640 check_error(err) 641 642 return Index.from_c(self, cindex) 643 644 # 645 # Describe 646 # 647 def describe(self, committish=None, max_candidates_tags=None, 648 describe_strategy=None, pattern=None, 649 only_follow_first_parent=None, 650 show_commit_oid_as_fallback=None, abbreviated_size=None, 651 always_use_long_format=None, dirty_suffix=None): 652 """Describe a commit-ish or the current working tree. 653 654 :param committish: Commit-ish object or object name to describe, or 655 `None` to describe the current working tree. 656 :type committish: `str`, :class:`~.Reference`, or :class:`~.Commit` 657 658 :param int max_candidates_tags: The number of candidate tags to 659 consider. Increasing above 10 will take slightly longer but may 660 produce a more accurate result. A value of 0 will cause only exact 661 matches to be output. 662 :param int describe_strategy: A GIT_DESCRIBE_* constant. 663 :param str pattern: Only consider tags matching the given `glob(7)` 664 pattern, excluding the "refs/tags/" prefix. 665 :param bool only_follow_first_parent: Follow only the first parent 666 commit upon seeing a merge commit. 667 :param bool show_commit_oid_as_fallback: Show uniquely abbreviated 668 commit object as fallback. 669 :param int abbreviated_size: The minimum number of hexadecimal digits 670 to show for abbreviated object names. A value of 0 will suppress 671 long format, only showing the closest tag. 672 :param bool always_use_long_format: Always output the long format (the 673 nearest tag, the number of commits, and the abbrevated commit name) 674 even when the committish matches a tag. 675 :param str dirty_suffix: A string to append if the working tree is 676 dirty. 677 678 :returns: The description. 679 :rtype: `str` 680 681 Example:: 682 683 repo.describe(pattern='public/*', dirty_suffix='-dirty') 684 """ 685 686 options = ffi.new('git_describe_options *') 687 C.git_describe_init_options(options, C.GIT_DESCRIBE_OPTIONS_VERSION) 688 689 if max_candidates_tags is not None: 690 options.max_candidates_tags = max_candidates_tags 691 if describe_strategy is not None: 692 options.describe_strategy = describe_strategy 693 if pattern: 694 options.pattern = ffi.new('char[]', to_bytes(pattern)) 695 if only_follow_first_parent is not None: 696 options.only_follow_first_parent = only_follow_first_parent 697 if show_commit_oid_as_fallback is not None: 698 options.show_commit_oid_as_fallback = show_commit_oid_as_fallback 699 700 result = ffi.new('git_describe_result **') 701 if committish: 702 if is_string(committish): 703 committish = self.revparse_single(committish) 704 705 commit = committish.peel(Commit) 706 707 cptr = ffi.new('git_object **') 708 ffi.buffer(cptr)[:] = commit._pointer[:] 709 710 err = C.git_describe_commit(result, cptr[0], options) 711 else: 712 err = C.git_describe_workdir(result, self._repo, options) 713 check_error(err) 714 715 try: 716 format_options = ffi.new('git_describe_format_options *') 717 C.git_describe_init_format_options(format_options, C.GIT_DESCRIBE_FORMAT_OPTIONS_VERSION) 718 719 if abbreviated_size is not None: 720 format_options.abbreviated_size = abbreviated_size 721 if always_use_long_format is not None: 722 format_options.always_use_long_format = always_use_long_format 723 dirty_ptr = None 724 if dirty_suffix: 725 dirty_ptr = ffi.new('char[]', to_bytes(dirty_suffix)) 726 format_options.dirty_suffix = dirty_ptr 727 728 buf = ffi.new('git_buf *', (ffi.NULL, 0)) 729 730 err = C.git_describe_format(buf, result[0], format_options) 731 check_error(err) 732 733 try: 734 return ffi.string(buf.ptr).decode('utf-8') 735 finally: 736 C.git_buf_free(buf) 737 finally: 738 C.git_describe_result_free(result[0]) 739 740 # 741 # Utility for writing a tree into an archive 742 # 743 def write_archive(self, treeish, archive, timestamp=None, prefix=''): 744 """Write treeish into an archive 745 746 If no timestamp is provided and 'treeish' is a commit, its committer 747 timestamp will be used. Otherwise the current time will be used. 748 749 All path names in the archive are added to 'prefix', which defaults to 750 an empty string. 751 752 Arguments: 753 754 treeish 755 The treeish to write. 756 archive 757 An archive from the 'tarfile' module 758 timestamp 759 Timestamp to use for the files in the archive. 760 prefix 761 Extra prefix to add to the path names in the archive. 762 763 Example:: 764 765 >>> import tarfile, pygit2 766 >>>> with tarfile.open('foo.tar', 'w') as archive: 767 >>>> repo = pygit2.Repsitory('.') 768 >>>> repo.write_archive(repo.head.target, archive) 769 """ 770 771 # Try to get a tree form whatever we got 772 if isinstance(treeish, Tree): 773 tree = treeish 774 775 if isinstance(treeish, Oid) or is_string(treeish): 776 treeish = self[treeish] 777 778 # if we don't have a timestamp, try to get it from a commit 779 if not timestamp: 780 try: 781 commit = treeish.peel(Commit) 782 timestamp = commit.committer.time 783 except Exception: 784 pass 785 786 # as a last resort, use the current timestamp 787 if not timestamp: 788 timestamp = int(time()) 789 790 tree = treeish.peel(Tree) 791 792 index = Index() 793 index.read_tree(tree) 794 795 for entry in index: 796 content = self[entry.id].read_raw() 797 info = tarfile.TarInfo(prefix + entry.path) 798 info.size = len(content) 799 info.mtime = timestamp 800 info.uname = info.gname = 'root' # just because git does this 801 if entry.mode == GIT_FILEMODE_LINK: 802 info.type = tarfile.SYMTYPE 803 info.linkname = content.decode("utf-8") 804 info.mode = 0o777 # symlinks get placeholder 805 info.size = 0 806 archive.addfile(info) 807 else: 808 archive.addfile(info, StringIO(content)) 809 810 # 811 # Ahead-behind, which mostly lives on its own namespace 812 # 813 def ahead_behind(self, local, upstream): 814 """Calculate how many different commits are in the non-common parts 815 of the history between the two given ids. 816 817 Ahead is how many commits are in the ancestry of the 'local' 818 commit which are not in the 'upstream' commit. Behind is the 819 opposite. 820 821 Arguments 822 823 local 824 The commit which is considered the local or current state 825 upstream 826 The commit which is considered the upstream 827 828 Returns a tuple of two integers with the number of commits ahead and 829 behind respectively. 830 """ 831 832 if not isinstance(local, Oid): 833 local = self.expand_id(local) 834 835 if not isinstance(upstream, Oid): 836 upstream = self.expand_id(upstream) 837 838 ahead, behind = ffi.new('size_t*'), ffi.new('size_t*') 839 oid1, oid2 = ffi.new('git_oid *'), ffi.new('git_oid *') 840 ffi.buffer(oid1)[:] = local.raw[:] 841 ffi.buffer(oid2)[:] = upstream.raw[:] 842 err = C.git_graph_ahead_behind(ahead, behind, self._repo, oid1, oid2) 843 check_error(err) 844 845 return int(ahead[0]), int(behind[0]) 846 847 # 848 # Git attributes 849 # 850 def get_attr(self, path, name, flags=0): 851 """Retrieve an attribute for a file by path 852 853 Arguments 854 855 path 856 The path of the file to look up attributes for, relative to the 857 workdir root 858 name 859 The name of the attribute to look up 860 flags 861 A combination of GIT_ATTR_CHECK_ flags which determine the 862 lookup order 863 864 Returns either a boolean, None (if the value is unspecified) or string 865 with the value of the attribute. 866 """ 867 868 cvalue = ffi.new('char **') 869 err = C.git_attr_get(cvalue, self._repo, flags, to_bytes(path), to_bytes(name)) 870 check_error(err) 871 872 # Now let's see if we can figure out what the value is 873 attr_kind = C.git_attr_value(cvalue[0]) 874 if attr_kind == C.GIT_ATTR_UNSPECIFIED_T: 875 return None 876 elif attr_kind == C.GIT_ATTR_TRUE_T: 877 return True 878 elif attr_kind == C.GIT_ATTR_FALSE_T: 879 return False 880 elif attr_kind == C.GIT_ATTR_VALUE_T: 881 return ffi.string(cvalue[0]).decode('utf-8') 882 883 assert False, "the attribute value from libgit2 is invalid" 884 885 # 886 # Identity for reference operations 887 # 888 @property 889 def ident(self): 890 cname = ffi.new('char **') 891 cemail = ffi.new('char **') 892 893 err = C.git_repository_ident(cname, cemail, self._repo) 894 check_error(err) 895 896 return (ffi.string(cname).decode('utf-8'), ffi.string(cemail).decode('utf-8')) 897 898 def set_ident(self, name, email): 899 """Set the identity to be used for reference operations 900 901 Updates to some references also append data to their 902 reflog. You can use this method to set what identity will be 903 used. If none is set, it will be read from the configuration. 904 """ 905 906 err = C.git_repository_set_ident(self._repo, to_bytes(name), to_bytes(email)) 907 check_error(err)