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)