From 99dfce9ab8202dacf45127cf1257318d584f18a0 Mon Sep 17 00:00:00 2001 From: Noah Fontes Date: Sat, 5 Dec 2015 23:22:37 -0800 Subject: [PATCH] Add support for Repository.describe(...). --- docs/repository.rst | 1 + pygit2/decl.h | 41 ++++++++++++++++ pygit2/repository.py | 94 +++++++++++++++++++++++++++++++++++++ src/pygit2.c | 5 ++ test/test_describe.py | 106 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 247 insertions(+) create mode 100644 test/test_describe.py diff --git a/docs/repository.rst b/docs/repository.rst index e434661b9..cad8908a3 100644 --- a/docs/repository.rst +++ b/docs/repository.rst @@ -71,3 +71,4 @@ Below there are some general attributes and methods: .. automethod:: pygit2.Repository.state_cleanup .. automethod:: pygit2.Repository.write_archive .. automethod:: pygit2.Repository.ahead_behind +.. automethod:: pygit2.Repository.describe diff --git a/pygit2/decl.h b/pygit2/decl.h index 61afe33aa..ed595423e 100644 --- a/pygit2/decl.h +++ b/pygit2/decl.h @@ -756,6 +756,47 @@ int git_merge_trees(git_index **out, git_repository *repo, const git_tree *ances int git_merge_file_from_index(git_merge_file_result *out, git_repository *repo, const git_index_entry *ancestor, const git_index_entry *ours, const git_index_entry *theirs, const git_merge_file_options *opts); void git_merge_file_result_free(git_merge_file_result *result); +/* + * Describe + */ + +typedef enum { + GIT_DESCRIBE_DEFAULT, + GIT_DESCRIBE_TAGS, + GIT_DESCRIBE_ALL, +} git_describe_strategy_t; + +typedef struct git_describe_options { + unsigned int version; + unsigned int max_candidates_tags; + unsigned int describe_strategy; + const char *pattern; + int only_follow_first_parent; + int show_commit_oid_as_fallback; +} git_describe_options; + +#define GIT_DESCRIBE_OPTIONS_VERSION ... + +int git_describe_init_options(git_describe_options *opts, unsigned int version); + +typedef struct { + unsigned int version; + unsigned int abbreviated_size; + int always_use_long_format; + const char *dirty_suffix; +} git_describe_format_options; + +#define GIT_DESCRIBE_FORMAT_OPTIONS_VERSION ... + +int git_describe_init_format_options(git_describe_format_options *opts, unsigned int version); + +typedef ... git_describe_result; + +int git_describe_commit(git_describe_result **result, git_object *committish, git_describe_options *opts); +int git_describe_workdir(git_describe_result **out, git_repository *repo, git_describe_options *opts); +int git_describe_format(git_buf *out, const git_describe_result *result, const git_describe_format_options *opts); +void git_describe_result_free(git_describe_result *result); + #define GIT_ATTR_CHECK_FILE_THEN_INDEX ... #define GIT_ATTR_CHECK_INDEX_THEN_FILE ... #define GIT_ATTR_CHECK_INDEX_ONLY ... diff --git a/pygit2/repository.py b/pygit2/repository.py index c714a633b..f8a6cada9 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -637,6 +637,100 @@ def merge_trees(self, ancestor, ours, theirs, favor='normal'): return Index.from_c(self, cindex) + # + # Describe + # + def describe(self, committish=None, max_candidates_tags=None, + describe_strategy=None, pattern=None, + only_follow_first_parent=None, + show_commit_oid_as_fallback=None, abbreviated_size=None, + always_use_long_format=None, dirty_suffix=None): + """Describe a commit-ish or the current working tree. + + :param committish: Commit-ish object or object name to describe, or + `None` to describe the current working tree. + :type committish: `str`, :class:`~.Reference`, or :class:`~.Commit` + + :param int max_candidates_tags: The number of candidate tags to + consider. Increasing above 10 will take slightly longer but may + produce a more accurate result. A value of 0 will cause only exact + matches to be output. + :param int describe_strategy: A GIT_DESCRIBE_* constant. + :param str pattern: Only consider tags matching the given `glob(7)` + pattern, excluding the "refs/tags/" prefix. + :param bool only_follow_first_parent: Follow only the first parent + commit upon seeing a merge commit. + :param bool show_commit_oid_as_fallback: Show uniquely abbreviated + commit object as fallback. + :param int abbreviated_size: The minimum number of hexadecimal digits + to show for abbreviated object names. A value of 0 will suppress + long format, only showing the closest tag. + :param bool always_use_long_format: Always output the long format (the + nearest tag, the number of commits, and the abbrevated commit name) + even when the committish matches a tag. + :param str dirty_suffix: A string to append if the working tree is + dirty. + + :returns: The description. + :rtype: `str` + + Example:: + + repo.describe(pattern='public/*', dirty_suffix='-dirty') + """ + + options = ffi.new('git_describe_options *') + C.git_describe_init_options(options, C.GIT_DESCRIBE_OPTIONS_VERSION) + + if max_candidates_tags is not None: + options.max_candidates_tags = max_candidates_tags + if describe_strategy is not None: + options.describe_strategy = describe_strategy + if pattern: + options.pattern = ffi.new('char[]', to_bytes(pattern)) + if only_follow_first_parent is not None: + options.only_follow_first_parent = only_follow_first_parent + if show_commit_oid_as_fallback is not None: + options.show_commit_oid_as_fallback = show_commit_oid_as_fallback + + result = ffi.new('git_describe_result **') + if committish: + if is_string(committish): + committish = self.revparse_single(committish) + + commit = committish.peel(Commit) + + cptr = ffi.new('git_object **') + ffi.buffer(cptr)[:] = commit._pointer[:] + + err = C.git_describe_commit(result, cptr[0], options) + else: + err = C.git_describe_workdir(result, self._repo, options) + check_error(err) + + try: + format_options = ffi.new('git_describe_format_options *') + C.git_describe_init_format_options(format_options, C.GIT_DESCRIBE_FORMAT_OPTIONS_VERSION) + + if abbreviated_size is not None: + format_options.abbreviated_size = abbreviated_size + if always_use_long_format is not None: + format_options.always_use_long_format = always_use_long_format + if dirty_suffix: + format_options.dirty_suffix = ffi.new('char[]', to_bytes(dirty_suffix)) + + buf = ffi.new('git_buf *', (ffi.NULL, 0)) + + err = C.git_describe_format(buf, result[0], format_options) + check_error(err) + + try: + return ffi.string(buf.ptr).decode('utf-8') + finally: + C.git_buf_free(buf) + finally: + C.git_describe_result_free(result[0]) + # # Utility for writing a tree into an archive # diff --git a/src/pygit2.c b/src/pygit2.c index 02ddcb9f5..47f9edbdc 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -381,6 +381,11 @@ moduleinit(PyObject* m) ADD_CONSTANT_INT(m, GIT_MERGE_ANALYSIS_FASTFORWARD) ADD_CONSTANT_INT(m, GIT_MERGE_ANALYSIS_UNBORN) + /* Describe */ + ADD_CONSTANT_INT(m, GIT_DESCRIBE_DEFAULT); + ADD_CONSTANT_INT(m, GIT_DESCRIBE_TAGS); + ADD_CONSTANT_INT(m, GIT_DESCRIBE_ALL); + /* Global initialization of libgit2 */ git_libgit2_init(); diff --git a/test/test_describe.py b/test/test_describe.py new file mode 100644 index 000000000..f4e2cd939 --- /dev/null +++ b/test/test_describe.py @@ -0,0 +1,106 @@ +# -*- coding: UTF-8 -*- +# +# Copyright 2010-2015 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +"""Tests for describing commits.""" + +from __future__ import absolute_import +from __future__ import unicode_literals +import unittest + +from pygit2 import GIT_DESCRIBE_DEFAULT, GIT_DESCRIBE_TAGS, GIT_DESCRIBE_ALL +import pygit2 +from . import utils + + +def add_tag(repo, name, target): + message = 'Example tag.\n' + tagger = pygit2.Signature('John Doe', 'jdoe@example.com', 12347, 0) + + sha = repo.create_tag(name, target, pygit2.GIT_OBJ_COMMIT, tagger, message) + return sha + + +class DescribeTest(utils.RepoTestCase): + + def test_describe(self): + add_tag(self.repo, 'thetag', '4ec4389a8068641da2d6578db0419484972284c8') + self.assertEqual('thetag-2-g2be5719', self.repo.describe()) + + def test_describe_without_ref(self): + self.assertRaises(pygit2.GitError, self.repo.describe) + + def test_describe_default_oid(self): + self.assertEqual('2be5719', self.repo.describe(show_commit_oid_as_fallback=True)) + + def test_describe_strategies(self): + self.assertEqual('heads/master', self.repo.describe(describe_strategy=GIT_DESCRIBE_ALL)) + + self.repo.create_reference('refs/tags/thetag', '4ec4389a8068641da2d6578db0419484972284c8') + self.assertRaises(KeyError, self.repo.describe) + self.assertEqual('thetag-2-g2be5719', self.repo.describe(describe_strategy=GIT_DESCRIBE_TAGS)) + + def test_describe_pattern(self): + add_tag(self.repo, 'private/tag1', '5ebeeebb320790caf276b9fc8b24546d63316533') + add_tag(self.repo, 'public/tag2', '4ec4389a8068641da2d6578db0419484972284c8') + + self.assertEqual('public/tag2-2-g2be5719', self.repo.describe(pattern='public/*')) + + def test_describe_committish(self): + add_tag(self.repo, 'thetag', 'acecd5ea2924a4b900e7e149496e1f4b57976e51') + self.assertEqual('thetag-4-g2be5719', self.repo.describe(committish='HEAD')) + self.assertEqual('thetag-1-g5ebeeeb', self.repo.describe(committish='HEAD^')) + + self.assertEqual('thetag-4-g2be5719', self.repo.describe(committish=self.repo.head)) + + self.assertEqual('thetag-1-g6aaa262', self.repo.describe(committish='6aaa262e655dd54252e5813c8e5acd7780ed097d')) + self.assertEqual('thetag-1-g6aaa262', self.repo.describe(committish='6aaa262')) + + def test_describe_follows_first_branch_only(self): + add_tag(self.repo, 'thetag', '4ec4389a8068641da2d6578db0419484972284c8') + self.assertRaises(KeyError, self.repo.describe, only_follow_first_parent=True) + + def test_describe_abbreviated_size(self): + add_tag(self.repo, 'thetag', '4ec4389a8068641da2d6578db0419484972284c8') + self.assertEqual('thetag-2-g2be5719152d4f82c', self.repo.describe(abbreviated_size=16)) + self.assertEqual('thetag', self.repo.describe(abbreviated_size=0)) + + def test_describe_long_format(self): + add_tag(self.repo, 'thetag', '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98') + self.assertEqual('thetag-0-g2be5719', self.repo.describe(always_use_long_format=True)) + + +class DescribeDirtyWorkdirTest(utils.DirtyRepoTestCase): + + def setUp(self): + super(utils.DirtyRepoTestCase, self).setUp() + add_tag(self.repo, 'thetag', 'a763aa560953e7cfb87ccbc2f536d665aa4dff22') + + def test_describe(self): + self.assertEqual('thetag', self.repo.describe()) + + def test_describe_with_dirty_suffix(self): + self.assertEqual('thetag-dirty', self.repo.describe(dirty_suffix='-dirty'))