Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Repository.describe(...). #585

Merged
merged 1 commit into from
Dec 7, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/repository.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
41 changes: 41 additions & 0 deletions pygit2/decl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ...
Expand Down
94 changes: 94 additions & 0 deletions pygit2/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down
5 changes: 5 additions & 0 deletions src/pygit2.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
106 changes: 106 additions & 0 deletions test/test_describe.py
Original file line number Diff line number Diff line change
@@ -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'))