diff options
| -rw-r--r-- | handles.go | 11 | ||||
| -rw-r--r-- | reference.go | 24 | ||||
| -rw-r--r-- | reference_test.go | 42 | ||||
| -rw-r--r-- | repository.go | 34 | ||||
| -rw-r--r-- | tag.go | 167 | ||||
| -rw-r--r-- | tag_test.go | 157 | ||||
| -rw-r--r-- | tree.go | 18 | ||||
| -rw-r--r-- | tree_test.go | 22 | ||||
| -rw-r--r-- | wrapper.c | 5 |
9 files changed, 444 insertions, 36 deletions
@@ -10,14 +10,15 @@ type HandleList struct { sync.RWMutex // stores the Go pointers handles []interface{} - // indicates which indices are in use - set map[int]bool + // Indicates which indices are in use, and keeps a pointer to slot int variable (the handle) + // in the Go world, so that the Go garbage collector does not free it. + set map[int]*int } func NewHandleList() *HandleList { return &HandleList{ handles: make([]interface{}, 5), - set: make(map[int]bool), + set: make(map[int]*int), } } @@ -25,7 +26,7 @@ func NewHandleList() *HandleList { // list. You must only run this function while holding a write lock. func (v *HandleList) findUnusedSlot() int { for i := 1; i < len(v.handles); i++ { - isUsed := v.set[i] + _, isUsed := v.set[i] if !isUsed { return i } @@ -47,7 +48,7 @@ func (v *HandleList) Track(pointer interface{}) unsafe.Pointer { slot := v.findUnusedSlot() v.handles[slot] = pointer - v.set[slot] = true + v.set[slot] = &slot // Keep a pointer to slot in Go world, so it's not freed by GC. v.Unlock() diff --git a/reference.go b/reference.go index d24e054..140082f 100644 --- a/reference.go +++ b/reference.go @@ -315,6 +315,11 @@ func (v *Reference) IsTag() bool { return C.git_reference_is_tag(v.ptr) == 1 } +// IsNote checks if the reference is a note. +func (v *Reference) IsNote() bool { + return C.git_reference_is_note(v.ptr) == 1 +} + func (v *Reference) Free() { runtime.SetFinalizer(v, nil) C.git_reference_free(v.ptr) @@ -425,3 +430,22 @@ func (v *ReferenceIterator) Free() { runtime.SetFinalizer(v, nil) C.git_reference_iterator_free(v.ptr) } + +// ReferenceIsValidName ensures the reference name is well-formed. +// +// Valid reference names must follow one of two patterns: +// +// 1. Top-level names must contain only capital letters and underscores, +// and must begin and end with a letter. (e.g. "HEAD", "ORIG_HEAD"). +// +// 2. Names prefixed with "refs/" can be almost anything. You must avoid +// the characters '~', '^', ':', ' \ ', '?', '[', and '*', and the sequences +// ".." and " @ {" which have special meaning to revparse. +func ReferenceIsValidName(name string) bool { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + if C.git_reference_is_valid_name(cname) == 1 { + return true + } + return false +} diff --git a/reference_test.go b/reference_test.go index f1546e2..761daf8 100644 --- a/reference_test.go +++ b/reference_test.go @@ -176,6 +176,48 @@ func TestUtil(t *testing.T) { } } +func TestIsNote(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + commitID, _ := seedTestRepo(t, repo) + + sig := &Signature{ + Name: "Rand Om Hacker", + Email: "[email protected]", + When: time.Now(), + } + + refname, err := repo.Notes.DefaultRef() + checkFatal(t, err) + + _, err = repo.Notes.Create(refname, sig, sig, commitID, "This is a note", false) + checkFatal(t, err) + + ref, err := repo.References.Lookup(refname) + checkFatal(t, err) + + if !ref.IsNote() { + t.Fatalf("%s should be a note", ref.Name()) + } + + ref, err = repo.References.Create("refs/heads/foo", commitID, true, "") + checkFatal(t, err) + + if ref.IsNote() { + t.Fatalf("%s should not be a note", ref.Name()) + } +} + +func TestReferenceIsValidName(t *testing.T) { + if !ReferenceIsValidName("HEAD") { + t.Errorf("HEAD should be a valid reference name") + } + if ReferenceIsValidName("HEAD1") { + t.Errorf("HEAD1 should not be a valid reference name") + } +} + func compareStringList(t *testing.T, expected, actual []string) { for i, v := range expected { if actual[i] != v { diff --git a/repository.go b/repository.go index 44509af..62fde6d 100644 --- a/repository.go +++ b/repository.go @@ -27,6 +27,9 @@ type Repository struct { // Notes represents the collection of notes and can be used to // read, write and delete notes from this repository. Notes NoteCollection + // Tags represents the collection of tags and can be used to create, + // list and iterate tags in this repository. + Tags TagsCollection } func newRepositoryFromC(ptr *C.git_repository) *Repository { @@ -36,6 +39,7 @@ func newRepositoryFromC(ptr *C.git_repository) *Repository { repo.Submodules.repo = repo repo.References.repo = repo repo.Notes.repo = repo + repo.Tags.repo = repo runtime.SetFinalizer(repo, (*Repository).Free) @@ -317,36 +321,6 @@ func (v *Repository) CreateCommit( return oid, nil } -func (v *Repository) CreateTag( - name string, commit *Commit, tagger *Signature, message string) (*Oid, error) { - - oid := new(Oid) - - cname := C.CString(name) - defer C.free(unsafe.Pointer(cname)) - - cmessage := C.CString(message) - defer C.free(unsafe.Pointer(cmessage)) - - taggerSig, err := tagger.toC() - if err != nil { - return nil, err - } - defer C.git_signature_free(taggerSig) - - ctarget := commit.gitObject.ptr - - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - ret := C.git_tag_create(oid.toC(), v.ptr, cname, ctarget, taggerSig, cmessage, 0) - if ret < 0 { - return nil, MakeGitError(ret) - } - - return oid, nil -} - func (v *Odb) Free() { runtime.SetFinalizer(v, nil) C.git_odb_free(v.ptr) @@ -2,8 +2,14 @@ package git /* #include <git2.h> + +extern int _go_git_tag_foreach(git_repository *repo, void *payload); */ import "C" +import ( + "runtime" + "unsafe" +) // Tag type Tag struct { @@ -42,3 +48,164 @@ func (t Tag) TargetId() *Oid { func (t Tag) TargetType() ObjectType { return ObjectType(C.git_tag_target_type(t.cast_ptr)) } + +type TagsCollection struct { + repo *Repository +} + +func (c *TagsCollection) Create( + name string, commit *Commit, tagger *Signature, message string) (*Oid, error) { + + oid := new(Oid) + + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + + cmessage := C.CString(message) + defer C.free(unsafe.Pointer(cmessage)) + + taggerSig, err := tagger.toC() + if err != nil { + return nil, err + } + defer C.git_signature_free(taggerSig) + + ctarget := commit.gitObject.ptr + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_tag_create(oid.toC(), c.repo.ptr, cname, ctarget, taggerSig, cmessage, 0) + if ret < 0 { + return nil, MakeGitError(ret) + } + + return oid, nil +} + +// CreateLightweight creates a new lightweight tag pointing to a commit +// and returns the id of the target object. +// +// The name of the tag is validated for consistency (see git_tag_create() for the rules +// https://libgit2.github.com/libgit2/#HEAD/group/tag/git_tag_create) and should +// not conflict with an already existing tag name. +// +// If force is true and a reference already exists with the given name, it'll be replaced. +// +// The created tag is a simple reference and can be queried using +// repo.References.Lookup("refs/tags/<name>"). The name of the tag (eg "v1.0.0") +// is queried with ref.Shorthand(). +func (c *TagsCollection) CreateLightweight(name string, commit *Commit, force bool) (*Oid, error) { + + oid := new(Oid) + + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + + ctarget := commit.gitObject.ptr + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := C.git_tag_create_lightweight(oid.toC(), c.repo.ptr, cname, ctarget, cbool(force)) + if err < 0 { + return nil, MakeGitError(err) + } + + return oid, nil +} + +// List returns the names of all the tags in the repository, +// eg: ["v1.0.1", "v2.0.0"]. +func (c *TagsCollection) List() ([]string, error) { + var strC C.git_strarray + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ecode := C.git_tag_list(&strC, c.repo.ptr) + if ecode < 0 { + return nil, MakeGitError(ecode) + } + defer C.git_strarray_free(&strC) + + tags := makeStringsFromCStrings(strC.strings, int(strC.count)) + return tags, nil +} + +// ListWithMatch returns the names of all the tags in the repository +// that match a given pattern. +// +// The pattern is a standard fnmatch(3) pattern http://man7.org/linux/man-pages/man3/fnmatch.3.html +func (c *TagsCollection) ListWithMatch(pattern string) ([]string, error) { + var strC C.git_strarray + + patternC := C.CString(pattern) + defer C.free(unsafe.Pointer(patternC)) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ecode := C.git_tag_list_match(&strC, patternC, c.repo.ptr) + if ecode < 0 { + return nil, MakeGitError(ecode) + } + defer C.git_strarray_free(&strC) + + tags := makeStringsFromCStrings(strC.strings, int(strC.count)) + return tags, nil +} + +// TagForeachCallback is called for each tag in the repository. +// +// The name is the full ref name eg: "refs/tags/v1.0.0". +// +// Note that the callback is called for lightweight tags as well, +// so repo.LookupTag() will return an error for these tags. Use +// repo.References.Lookup() instead. +type TagForeachCallback func(name string, id *Oid) error +type tagForeachData struct { + callback TagForeachCallback + err error +} + +//export gitTagForeachCb +func gitTagForeachCb(name *C.char, id *C.git_oid, handle unsafe.Pointer) int { + payload := pointerHandles.Get(handle) + data, ok := payload.(*tagForeachData) + if !ok { + panic("could not retrieve tag foreach CB handle") + } + + err := data.callback(C.GoString(name), newOidFromC(id)) + if err != nil { + data.err = err + return C.GIT_EUSER + } + + return 0 +} + +// Foreach calls the callback for each tag in the repository. +func (c *TagsCollection) Foreach(callback TagForeachCallback) error { + data := tagForeachData{ + callback: callback, + err: nil, + } + + handle := pointerHandles.Track(&data) + defer pointerHandles.Untrack(handle) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := C._go_git_tag_foreach(c.repo.ptr, handle) + if err == C.GIT_EUSER { + return data.err + } + if err < 0 { + return MakeGitError(err) + } + + return nil +} diff --git a/tag_test.go b/tag_test.go index 74f9fec..2fdfe00 100644 --- a/tag_test.go +++ b/tag_test.go @@ -1,6 +1,7 @@ package git import ( + "errors" "testing" "time" ) @@ -24,6 +25,146 @@ func TestCreateTag(t *testing.T) { compareStrings(t, commitId.String(), tag.TargetId().String()) } +func TestCreateTagLightweight(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + commitID, _ := seedTestRepo(t, repo) + + commit, err := repo.LookupCommit(commitID) + checkFatal(t, err) + + tagID, err := repo.Tags.CreateLightweight("v0.1.0", commit, false) + checkFatal(t, err) + + _, err = repo.Tags.CreateLightweight("v0.1.0", commit, true) + checkFatal(t, err) + + ref, err := repo.References.Lookup("refs/tags/v0.1.0") + checkFatal(t, err) + + compareStrings(t, "refs/tags/v0.1.0", ref.Name()) + compareStrings(t, "v0.1.0", ref.Shorthand()) + compareStrings(t, tagID.String(), commitID.String()) + compareStrings(t, commitID.String(), ref.Target().String()) +} + +func TestListTags(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + commitID, _ := seedTestRepo(t, repo) + + commit, err := repo.LookupCommit(commitID) + checkFatal(t, err) + + createTag(t, repo, commit, "v1.0.1", "Release v1.0.1") + + commitID, _ = updateReadme(t, repo, "Release version 2") + + commit, err = repo.LookupCommit(commitID) + checkFatal(t, err) + + createTag(t, repo, commit, "v2.0.0", "Release v2.0.0") + + expected := []string{ + "v1.0.1", + "v2.0.0", + } + + actual, err := repo.Tags.List() + checkFatal(t, err) + + compareStringList(t, expected, actual) +} + +func TestListTagsWithMatch(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + commitID, _ := seedTestRepo(t, repo) + + commit, err := repo.LookupCommit(commitID) + checkFatal(t, err) + + createTag(t, repo, commit, "v1.0.1", "Release v1.0.1") + + commitID, _ = updateReadme(t, repo, "Release version 2") + + commit, err = repo.LookupCommit(commitID) + checkFatal(t, err) + + createTag(t, repo, commit, "v2.0.0", "Release v2.0.0") + + expected := []string{ + "v2.0.0", + } + + actual, err := repo.Tags.ListWithMatch("v2*") + checkFatal(t, err) + + compareStringList(t, expected, actual) + + expected = []string{ + "v1.0.1", + } + + actual, err = repo.Tags.ListWithMatch("v1*") + checkFatal(t, err) + + compareStringList(t, expected, actual) +} + +func TestTagForeach(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + commitID, _ := seedTestRepo(t, repo) + + commit, err := repo.LookupCommit(commitID) + checkFatal(t, err) + + tag1 := createTag(t, repo, commit, "v1.0.1", "Release v1.0.1") + + commitID, _ = updateReadme(t, repo, "Release version 2") + + commit, err = repo.LookupCommit(commitID) + checkFatal(t, err) + + tag2 := createTag(t, repo, commit, "v2.0.0", "Release v2.0.0") + + expectedNames := []string{ + "refs/tags/v1.0.1", + "refs/tags/v2.0.0", + } + actualNames := []string{} + expectedOids := []string{ + tag1.String(), + tag2.String(), + } + actualOids := []string{} + + err = repo.Tags.Foreach(func(name string, id *Oid) error { + actualNames = append(actualNames, name) + actualOids = append(actualOids, id.String()) + return nil + }) + checkFatal(t, err) + + compareStringList(t, expectedNames, actualNames) + compareStringList(t, expectedOids, actualOids) + + fakeErr := errors.New("fake error") + + err = repo.Tags.Foreach(func(name string, id *Oid) error { + return fakeErr + }) + + if err != fakeErr { + t.Fatalf("Tags.Foreach() did not return the expected error, got %v", err) + } +} + func compareStrings(t *testing.T, expected, value string) { if value != expected { t.Fatalf("expected '%v', actual '%v'", expected, value) @@ -39,7 +180,21 @@ func createTestTag(t *testing.T, repo *Repository, commit *Commit) *Oid { When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), } - tagId, err := repo.CreateTag("v0.0.0", commit, sig, "This is a tag") + tagId, err := repo.Tags.Create("v0.0.0", commit, sig, "This is a tag") + checkFatal(t, err) + return tagId +} + +func createTag(t *testing.T, repo *Repository, commit *Commit, name, message string) *Oid { + loc, err := time.LoadLocation("Europe/Bucharest") + checkFatal(t, err) + sig := &Signature{ + Name: "Rand Om Hacker", + Email: "[email protected]", + When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), + } + + tagId, err := repo.Tags.Create(name, commit, sig, message) checkFatal(t, err) return tagId } @@ -55,6 +55,24 @@ func (t Tree) EntryByName(filename string) *TreeEntry { return newTreeEntry(entry) } +// EntryById performs a lookup for a tree entry with the given SHA value. +// +// It returns a *TreeEntry that is owned by the Tree. You don't have to +// free it, but you must not use it after the Tree is freed. +// +// Warning: this must examine every entry in the tree, so it is not fast. +func (t Tree) EntryById(id *Oid) *TreeEntry { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + entry := C.git_tree_entry_byid(t.cast_ptr, id.toC()) + if entry == nil { + return nil + } + + return newTreeEntry(entry) +} + // EntryByPath looks up an entry by its full path, recursing into // deeper trees if necessary (i.e. if there are slashes in the path) func (t Tree) EntryByPath(path string) (*TreeEntry, error) { diff --git a/tree_test.go b/tree_test.go new file mode 100644 index 0000000..4c6a4ed --- /dev/null +++ b/tree_test.go @@ -0,0 +1,22 @@ +package git + +import "testing" + +func TestTreeEntryById(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + _, treeID := seedTestRepo(t, repo) + + tree, err := repo.LookupTree(treeID) + checkFatal(t, err) + + id, err := NewOid("257cc5642cb1a054f08cc83f2d943e56fd3ebe99") + checkFatal(t, err) + + entry := tree.EntryById(id) + + if entry == nil { + t.Fatalf("entry id %v was not found", id) + } +} @@ -131,4 +131,9 @@ int _go_git_index_remove_all(git_index *index, const git_strarray *pathspec, voi return git_index_remove_all(index, pathspec, cb, callback); } +int _go_git_tag_foreach(git_repository *repo, void *payload) +{ + return git_tag_foreach(repo, (git_tag_foreach_cb)&gitTagForeachCb, payload); +} + /* EOF */ |
