diff options
| -rw-r--r-- | .travis.yml | 2 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | describe.go | 222 | ||||
| -rw-r--r-- | describe_test.go | 106 | ||||
| -rw-r--r-- | merge.go | 31 | ||||
| -rw-r--r-- | merge_test.go | 59 | ||||
| -rw-r--r-- | object.go | 26 | ||||
| -rw-r--r-- | object_test.go | 60 |
8 files changed, 511 insertions, 7 deletions
diff --git a/.travis.yml b/.travis.yml index fb080b9..209d89f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go -sudo: false +sudo: required install: ./script/install-libgit2.sh @@ -8,13 +8,13 @@ Go bindings for [libgit2](http://libgit2.github.com/). The `master` branch follo Installing ---------- -This project wraps the functionality provided by libgit2. If you're using a stable version, install it to your system via your system's package manger and then install git2go as usual. +This project wraps the functionality provided by libgit2. If you're using a stable version, install it to your system via your system's package manaager and then install git2go as usual. Otherwise (`next` which tracks an unstable version), we need to build libgit2 as well. In order to build it, you need `cmake`, `pkg-config` and a C compiler. You will also need the development packages for OpenSSL and LibSSH2 installed if you want libgit2 to support HTTPS and SSH respectively. ### Stable version -git2go has `master` which tracks the latest release of libgit2, and versioned branches which indicate which version of libgit2 they work against. Install the development package it on your system via your favourite package manager or from source and you can use a service like gopkg.in to use the appropriate version. For the libgit2 v0.22 case, you can use +git2go has `master` which tracks the latest release of libgit2, and versioned branches which indicate which version of libgit2 they work against. Install the development package on your system via your favourite package manager or from source and you can use a service like gopkg.in to use the appropriate version. For the libgit2 v0.22 case, you can use import "gopkg.in/libgit2/git2go.v22" @@ -28,15 +28,15 @@ to use the version which works against the latest release. The `next` branch follows libgit2's master branch, which means there is no stable API or ABI to link against. git2go can statically link against a vendored version of libgit2. -Run `go get -d github.com/libgit2/git2go` to download the code and go to your `$GOPATH/src/github.com/libgit2/git2go` dir. From there, we need to build the C code and put it into the resulting go binary. +Run `go get -d github.com/libgit2/git2go` to download the code and go to your `$GOPATH/src/github.com/libgit2/git2go` directory. From there, we need to build the C code and put it into the resulting go binary. git checkout next git submodule update --init # get libgit2 make install -will compile libgit2 and run `go install` such that it's statically linked to the git2go package. +will compile libgit2. Run `go install` so that it's statically linked to the git2go package. -Paralellism and network operations +Parallelism and network operations ---------------------------------- libgit2 uses OpenSSL and LibSSH2 for performing encrypted network connections. For now, git2go asks libgit2 to set locking for OpenSSL. This makes HTTPS connections thread-safe, but it is fragile and will likely stop doing it soon. This may also make SSH connections thread-safe if your copy of libssh2 is linked against OpenSSL. Check libgit2's `THREADSAFE.md` for more information. @@ -48,7 +48,7 @@ For the stable version, `go test` will work as usual. For the `next` branch, sim make test -alternatively, if you want to pass arguments to `go test`, you can use the script that sets it all up +Alternatively, if you want to pass arguments to `go test`, you can use the script that sets it all up ./script/with-static.sh go test -v diff --git a/describe.go b/describe.go new file mode 100644 index 0000000..c6f9a79 --- /dev/null +++ b/describe.go @@ -0,0 +1,222 @@ +package git + +/* +#include <git2.h> +*/ +import "C" +import ( + "runtime" + "unsafe" +) + +// DescribeOptions represents the describe operation configuration. +// +// You can use DefaultDescribeOptions() to get default options. +type DescribeOptions struct { + // How many tags as candidates to consider to describe the input commit-ish. + // Increasing it above 10 will take slightly longer but may produce a more + // accurate result. 0 will cause only exact matches to be output. + MaxCandidatesTags uint // default: 10 + + // By default describe only shows annotated tags. Change this in order + // to show all refs from refs/tags or refs/. + Strategy DescribeOptionsStrategy // default: DescribeDefault + + // Only consider tags matching the given glob(7) pattern, excluding + // the "refs/tags/" prefix. Can be used to avoid leaking private + // tags from the repo. + Pattern string + + // When calculating the distance from the matching tag or + // reference, only walk down the first-parent ancestry. + OnlyFollowFirstParent bool + + // If no matching tag or reference is found, the describe + // operation would normally fail. If this option is set, it + // will instead fall back to showing the full id of the commit. + ShowCommitOidAsFallback bool +} + +// DefaultDescribeOptions returns default options for the describe operation. +func DefaultDescribeOptions() (DescribeOptions, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + opts := C.git_describe_options{} + ecode := C.git_describe_init_options(&opts, C.GIT_DESCRIBE_OPTIONS_VERSION) + if ecode < 0 { + return DescribeOptions{}, MakeGitError(ecode) + } + + return DescribeOptions{ + MaxCandidatesTags: uint(opts.max_candidates_tags), + Strategy: DescribeOptionsStrategy(opts.describe_strategy), + }, nil +} + +// DescribeFormatOptions can be used for formatting the describe string. +// +// You can use DefaultDescribeFormatOptions() to get default options. +type DescribeFormatOptions struct { + // Size of the abbreviated commit id to use. This value is the + // lower bound for the length of the abbreviated string. + AbbreviatedSize uint // default: 7 + + // Set to use the long format even when a shorter name could be used. + AlwaysUseLongFormat bool + + // If the workdir is dirty and this is set, this string will be + // appended to the description string. + DirtySuffix string +} + +// DefaultDescribeFormatOptions returns default options for formatting +// the output. +func DefaultDescribeFormatOptions() (DescribeFormatOptions, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + opts := C.git_describe_format_options{} + ecode := C.git_describe_init_format_options(&opts, C.GIT_DESCRIBE_FORMAT_OPTIONS_VERSION) + if ecode < 0 { + return DescribeFormatOptions{}, MakeGitError(ecode) + } + + return DescribeFormatOptions{ + AbbreviatedSize: uint(opts.abbreviated_size), + AlwaysUseLongFormat: opts.always_use_long_format == 1, + }, nil +} + +// DescribeOptionsStrategy behaves like the --tags and --all options +// to git-describe, namely they say to look for any reference in +// either refs/tags/ or refs/ respectively. +// +// By default it only shows annotated tags. +type DescribeOptionsStrategy uint + +// Describe strategy options. +const ( + DescribeDefault DescribeOptionsStrategy = C.GIT_DESCRIBE_DEFAULT + DescribeTags DescribeOptionsStrategy = C.GIT_DESCRIBE_TAGS + DescribeAll DescribeOptionsStrategy = C.GIT_DESCRIBE_ALL +) + +// Describe performs the describe operation on the commit. +func (c *Commit) Describe(opts *DescribeOptions) (*DescribeResult, error) { + var resultPtr *C.git_describe_result + + var cDescribeOpts *C.git_describe_options + if opts != nil { + var cpattern *C.char + if len(opts.Pattern) > 0 { + cpattern = C.CString(opts.Pattern) + defer C.free(unsafe.Pointer(cpattern)) + } + + cDescribeOpts = &C.git_describe_options{ + version: C.GIT_DESCRIBE_OPTIONS_VERSION, + max_candidates_tags: C.uint(opts.MaxCandidatesTags), + describe_strategy: C.uint(opts.Strategy), + pattern: cpattern, + only_follow_first_parent: cbool(opts.OnlyFollowFirstParent), + show_commit_oid_as_fallback: cbool(opts.ShowCommitOidAsFallback), + } + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ecode := C.git_describe_commit(&resultPtr, c.gitObject.ptr, cDescribeOpts) + if ecode < 0 { + return nil, MakeGitError(ecode) + } + + return newDescribeResultFromC(resultPtr), nil +} + +// DescribeWorkdir describes the working tree. It means describe HEAD +// and appends <mark> (-dirty by default) if the working tree is dirty. +func (repo *Repository) DescribeWorkdir(opts *DescribeOptions) (*DescribeResult, error) { + var resultPtr *C.git_describe_result + + var cDescribeOpts *C.git_describe_options + if opts != nil { + var cpattern *C.char + if len(opts.Pattern) > 0 { + cpattern = C.CString(opts.Pattern) + defer C.free(unsafe.Pointer(cpattern)) + } + + cDescribeOpts = &C.git_describe_options{ + version: C.GIT_DESCRIBE_OPTIONS_VERSION, + max_candidates_tags: C.uint(opts.MaxCandidatesTags), + describe_strategy: C.uint(opts.Strategy), + pattern: cpattern, + only_follow_first_parent: cbool(opts.OnlyFollowFirstParent), + show_commit_oid_as_fallback: cbool(opts.ShowCommitOidAsFallback), + } + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ecode := C.git_describe_workdir(&resultPtr, repo.ptr, cDescribeOpts) + if ecode < 0 { + return nil, MakeGitError(ecode) + } + + return newDescribeResultFromC(resultPtr), nil +} + +// DescribeResult represents the output from the 'git_describe_commit' +// and 'git_describe_workdir' functions in libgit2. +// +// Use Format() to get a string out of it. +type DescribeResult struct { + ptr *C.git_describe_result +} + +func newDescribeResultFromC(ptr *C.git_describe_result) *DescribeResult { + result := &DescribeResult{ + ptr: ptr, + } + runtime.SetFinalizer(result, (*DescribeResult).Free) + return result +} + +// Format prints the DescribeResult as a string. +func (result *DescribeResult) Format(opts *DescribeFormatOptions) (string, error) { + resultBuf := C.git_buf{} + + var cFormatOpts *C.git_describe_format_options + if opts != nil { + cDirtySuffix := C.CString(opts.DirtySuffix) + defer C.free(unsafe.Pointer(cDirtySuffix)) + + cFormatOpts = &C.git_describe_format_options{ + version: C.GIT_DESCRIBE_FORMAT_OPTIONS_VERSION, + abbreviated_size: C.uint(opts.AbbreviatedSize), + always_use_long_format: cbool(opts.AlwaysUseLongFormat), + dirty_suffix: cDirtySuffix, + } + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ecode := C.git_describe_format(&resultBuf, result.ptr, cFormatOpts) + if ecode < 0 { + return "", MakeGitError(ecode) + } + defer C.git_buf_free(&resultBuf) + + return C.GoString(resultBuf.ptr), nil +} + +// Free cleans up the C reference. +func (result *DescribeResult) Free() { + runtime.SetFinalizer(result, nil) + C.git_describe_result_free(result.ptr) + result.ptr = nil +} diff --git a/describe_test.go b/describe_test.go new file mode 100644 index 0000000..25af107 --- /dev/null +++ b/describe_test.go @@ -0,0 +1,106 @@ +package git + +import ( + "path" + "runtime" + "strings" + "testing" +) + +func TestDescribeCommit(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + describeOpts, err := DefaultDescribeOptions() + checkFatal(t, err) + + formatOpts, err := DefaultDescribeFormatOptions() + checkFatal(t, err) + + commitID, _ := seedTestRepo(t, repo) + + commit, err := repo.LookupCommit(commitID) + checkFatal(t, err) + + // No annotated tags can be used to describe master + _, err = commit.Describe(&describeOpts) + checkDescribeNoRefsFound(t, err) + + // Fallback + fallback := describeOpts + fallback.ShowCommitOidAsFallback = true + result, err := commit.Describe(&fallback) + checkFatal(t, err) + resultStr, err := result.Format(&formatOpts) + checkFatal(t, err) + compareStrings(t, "473bf77", resultStr) + + // Abbreviated + abbreviated := formatOpts + abbreviated.AbbreviatedSize = 2 + result, err = commit.Describe(&fallback) + checkFatal(t, err) + resultStr, err = result.Format(&abbreviated) + checkFatal(t, err) + compareStrings(t, "473b", resultStr) + + createTestTag(t, repo, commit) + + // Exact tag + patternOpts := describeOpts + patternOpts.Pattern = "v[0-9]*" + result, err = commit.Describe(&patternOpts) + checkFatal(t, err) + resultStr, err = result.Format(&formatOpts) + checkFatal(t, err) + compareStrings(t, "v0.0.0", resultStr) + + // Pattern no match + patternOpts.Pattern = "v[1-9]*" + result, err = commit.Describe(&patternOpts) + checkDescribeNoRefsFound(t, err) + + commitID, _ = updateReadme(t, repo, "update1") + commit, err = repo.LookupCommit(commitID) + checkFatal(t, err) + + // Tag-1 + result, err = commit.Describe(&describeOpts) + checkFatal(t, err) + resultStr, err = result.Format(&formatOpts) + checkFatal(t, err) + compareStrings(t, "v0.0.0-1-gd88ef8d", resultStr) + + // Strategy: All + describeOpts.Strategy = DescribeAll + result, err = commit.Describe(&describeOpts) + checkFatal(t, err) + resultStr, err = result.Format(&formatOpts) + checkFatal(t, err) + compareStrings(t, "heads/master", resultStr) + + repo.CreateBranch("hotfix", commit, false) + + // Workdir (branch) + result, err = repo.DescribeWorkdir(&describeOpts) + checkFatal(t, err) + resultStr, err = result.Format(&formatOpts) + checkFatal(t, err) + compareStrings(t, "heads/hotfix", resultStr) +} + +func checkDescribeNoRefsFound(t *testing.T, err error) { + // The failure happens at wherever we were called, not here + _, file, line, ok := runtime.Caller(1) + if !ok { + t.Fatalf("Unable to get caller") + } + if err == nil || !strings.Contains(err.Error(), "No reference found, cannot describe anything") { + t.Fatalf( + "%s:%v: was expecting error 'No reference found, cannot describe anything', got %v", + path.Base(file), + line, + err, + ) + } +} @@ -10,6 +10,7 @@ extern git_annotated_commit* _go_git_annotated_commit_array_get(git_annotated_co */ import "C" import ( + "reflect" "runtime" "unsafe" ) @@ -243,6 +244,36 @@ func (r *Repository) MergeBase(one *Oid, two *Oid) (*Oid, error) { return newOidFromC(&oid), nil } +// MergeBases retrieves the list of merge bases between two commits. +// +// If none are found, an empty slice is returned and the error is set +// approprately +func (r *Repository) MergeBases(one, two *Oid) ([]*Oid, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + var coids C.git_oidarray + ret := C.git_merge_bases(&coids, r.ptr, one.toC(), two.toC()) + if ret < 0 { + return make([]*Oid, 0), MakeGitError(ret) + } + + oids := make([]*Oid, coids.count) + hdr := reflect.SliceHeader { + Data: uintptr(unsafe.Pointer(coids.ids)), + Len: int(coids.count), + Cap: int(coids.count), + } + + goSlice := *(*[]C.git_oid)(unsafe.Pointer(&hdr)) + + for i, cid := range goSlice { + oids[i] = newOidFromC(&cid) + } + + return oids, nil +} + //TODO: int git_merge_base_many(git_oid *out, git_repository *repo, size_t length, const git_oid input_array[]); //TODO: GIT_EXTERN(int) git_merge_base_octopus(git_oid *out,git_repository *repo,size_t length,const git_oid input_array[]); diff --git a/merge_test.go b/merge_test.go index 5c62f5c..ad01319 100644 --- a/merge_test.go +++ b/merge_test.go @@ -2,6 +2,7 @@ package git import ( "testing" + "time" ) func TestMergeWithSelf(t *testing.T) { @@ -88,6 +89,64 @@ func TestMergeTreesWithoutAncestor(t *testing.T) { } +func appendCommit(t *testing.T, repo *Repository) (*Oid, *Oid) { + loc, err := time.LoadLocation("Europe/Berlin") + checkFatal(t, err) + sig := &Signature{ + Name: "Rand Om Hacker", + Email: "[email protected]", + When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), + } + + idx, err := repo.Index() + checkFatal(t, err) + err = idx.AddByPath("README") + checkFatal(t, err) + treeId, err := idx.WriteTree() + checkFatal(t, err) + + message := "This is another commit\n" + tree, err := repo.LookupTree(treeId) + checkFatal(t, err) + + ref, err := repo.References.Lookup("HEAD") + checkFatal(t, err) + + parent, err := ref.Peel(ObjectCommit) + checkFatal(t, err) + + commitId, err := repo.CreateCommit("HEAD", sig, sig, message, tree, parent.(*Commit)) + checkFatal(t, err) + + return commitId, treeId +} + +func TestMergeBase(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + commitAId, _ := seedTestRepo(t, repo) + commitBId, _ := appendCommit(t, repo) + + mergeBase, err := repo.MergeBase(commitAId, commitBId) + checkFatal(t, err) + + if mergeBase.Cmp(commitAId) != 0 { + t.Fatalf("unexpected merge base") + } + + mergeBases, err := repo.MergeBases(commitAId, commitBId) + checkFatal(t, err) + + if len(mergeBases) != 1 { + t.Fatalf("expected merge bases len to be 1, got %v", len(mergeBases)) + } + + if mergeBases[0].Cmp(commitAId) != 0 { + t.Fatalf("unexpected merge base") + } +} + func compareBytes(t *testing.T, expected, actual []byte) { for i, v := range expected { if actual[i] != v { @@ -22,6 +22,7 @@ type Object interface { Id() *Oid Type() ObjectType Owner() *Repository + Peel(t ObjectType) (Object, error) } type gitObject struct { @@ -69,6 +70,31 @@ func (o *gitObject) Free() { C.git_object_free(o.ptr) } +// Peel recursively peels an object until an object of the specified type is met. +// +// If the query cannot be satisfied due to the object model, ErrInvalidSpec +// will be returned (e.g. trying to peel a blob to a tree). +// +// If you pass ObjectAny as the target type, then the object will be peeled +// until the type changes. A tag will be peeled until the referenced object +// is no longer a tag, and a commit will be peeled to a tree. Any other object +// type will return ErrInvalidSpec. +// +// If peeling a tag we discover an object which cannot be peeled to the target +// type due to the object model, an error will be returned. +func (o *gitObject) Peel(t ObjectType) (Object, error) { + var cobj *C.git_object + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if err := C.git_object_peel(&cobj, o.ptr, C.git_otype(t)); err < 0 { + return nil, MakeGitError(err) + } + + return allocObject(cobj, o.repo), nil +} + func allocObject(cobj *C.git_object, repo *Repository) Object { obj := gitObject{ ptr: cobj, diff --git a/object_test.go b/object_test.go index aa295e5..ef6c5a1 100644 --- a/object_test.go +++ b/object_test.go @@ -102,3 +102,63 @@ func TestObjectOwner(t *testing.T) { checkOwner(t, repo, commit) checkOwner(t, repo, tree) } + +func TestObjectPeel(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + commitID, treeID := seedTestRepo(t, repo) + + var obj Object + + commit, err := repo.LookupCommit(commitID) + checkFatal(t, err) + + obj, err = commit.Peel(ObjectAny) + checkFatal(t, err) + + if obj.Type() != ObjectTree { + t.Fatalf("Wrong object type when peeling a commit, expected tree, have %v", obj.Type()) + } + + obj, err = commit.Peel(ObjectTag) + + if !IsErrorCode(err, ErrInvalidSpec) { + t.Fatalf("Wrong error when peeling a commit to a tag, expected ErrInvalidSpec, have %v", err) + } + + tree, err := repo.LookupTree(treeID) + checkFatal(t, err) + + obj, err = tree.Peel(ObjectAny) + + if !IsErrorCode(err, ErrInvalidSpec) { + t.Fatalf("Wrong error when peeling a tree, expected ErrInvalidSpec, have %v", err) + } + + entry := tree.EntryByName("README") + + blob, err := repo.LookupBlob(entry.Id) + checkFatal(t, err) + + obj, err = blob.Peel(ObjectAny) + + if !IsErrorCode(err, ErrInvalidSpec) { + t.Fatalf("Wrong error when peeling a blob, expected ErrInvalidSpec, have %v", err) + } + + tagID := createTestTag(t, repo, commit) + + tag, err := repo.LookupTag(tagID) + checkFatal(t, err) + + obj, err = tag.Peel(ObjectAny) + checkFatal(t, err) + + if obj.Type() != ObjectCommit { + t.Fatalf("Wrong object type when peeling a tag, expected commit, have %v", obj.Type()) + } + + // TODO: Should test a tag that annotates a different object than a commit + // but it's impossible at the moment to tag such an object. +} |
