├── .gitignore ├── testdata └── TestGitRepository.git │ ├── HEAD │ ├── refs │ └── heads │ │ └── master │ ├── objects │ ├── info │ │ ├── packs │ │ └── commit-graph │ └── pack │ │ ├── pack-ccace4e169a0858c13d9ae781a91d76fc33769b8.idx │ │ ├── pack-ccace4e169a0858c13d9ae781a91d76fc33769b8.pack │ │ └── pack-ccace4e169a0858c13d9ae781a91d76fc33769b8.bitmap │ ├── description │ ├── config │ ├── info │ ├── exclude │ └── refs │ └── packed-refs ├── .gitmodules ├── script ├── build-libgit2-static.sh ├── build-libgit2-dynamic.sh ├── install-libgit2.sh ├── check-MakeGitError-thread-lock.go └── build-libgit2.sh ├── go.mod ├── Build_system_dynamic.go ├── Build_system_static.go ├── submodule_test.go ├── Build_bundled_static.go ├── .github └── workflows │ ├── tag.yml │ ├── backport.yml │ └── ci.yml ├── push_test.go ├── features.go ├── patch_test.go ├── delta_string.go ├── LICENSE ├── message_test.go ├── ignore.go ├── reset.go ├── message.go ├── go.sum ├── revparse_test.go ├── blob_test.go ├── branch_test.go ├── refdb.go ├── reset_test.go ├── handles.go ├── mempack_test.go ├── difflinetype_string.go ├── signature.go ├── graph.go ├── refspec_test.go ├── blame_test.go ├── Makefile ├── transport_test.go ├── errorclass_string.go ├── clone_test.go ├── revert_test.go ├── settings_test.go ├── graph_test.go ├── patch.go ├── errorcode_string.go ├── status_test.go ├── revparse.go ├── config_test.go ├── cherrypick.go ├── mempack.go ├── indexer.go ├── note_test.go ├── describe_test.go ├── indexer_test.go ├── revert.go ├── cherrypick_test.go ├── tree_test.go ├── clone.go ├── refspec.go ├── blob.go ├── settings.go ├── repository_test.go ├── packbuilder.go ├── walk.go ├── blame.go ├── object_test.go ├── git_test.go ├── stash_test.go ├── tag_test.go ├── README.md ├── object.go ├── status.go ├── note.go ├── odb_test.go ├── commit.go ├── http.go ├── tree.go └── index_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /static-build/ 2 | /dynamic-build/ 3 | -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 49322bb17d3acc9146f98c97d078513228bbf3c0 2 | -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/objects/info/packs: -------------------------------------------------------------------------------- 1 | P pack-ccace4e169a0858c13d9ae781a91d76fc33769b8.pack 2 | 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/libgit2"] 2 | path = vendor/libgit2 3 | url = https://github.com/libgit2/libgit2 4 | -------------------------------------------------------------------------------- /script/build-libgit2-static.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | exec "$(dirname "$0")/build-libgit2.sh" --static 6 | -------------------------------------------------------------------------------- /script/build-libgit2-dynamic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | exec "$(dirname "$0")/build-libgit2.sh" --dynamic 6 | -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/objects/info/commit-graph: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libgit2/git2go/HEAD/testdata/TestGitRepository.git/objects/info/commit-graph -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | [remote "origin"] 6 | url = https://github.com/libgit2/TestGitRepository 7 | -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/objects/pack/pack-ccace4e169a0858c13d9ae781a91d76fc33769b8.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libgit2/git2go/HEAD/testdata/TestGitRepository.git/objects/pack/pack-ccace4e169a0858c13d9ae781a91d76fc33769b8.idx -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/objects/pack/pack-ccace4e169a0858c13d9ae781a91d76fc33769b8.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libgit2/git2go/HEAD/testdata/TestGitRepository.git/objects/pack/pack-ccace4e169a0858c13d9ae781a91d76fc33769b8.pack -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/objects/pack/pack-ccace4e169a0858c13d9ae781a91d76fc33769b8.bitmap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libgit2/git2go/HEAD/testdata/TestGitRepository.git/objects/pack/pack-ccace4e169a0858c13d9ae781a91d76fc33769b8.bitmap -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/libgit2/git2go/v34 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 7 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c 8 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /Build_system_dynamic.go: -------------------------------------------------------------------------------- 1 | //go:build !static 2 | // +build !static 3 | 4 | package git 5 | 6 | /* 7 | #cgo pkg-config: libgit2 8 | #cgo CFLAGS: -DLIBGIT2_DYNAMIC 9 | #include 10 | 11 | #if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 5 || LIBGIT2_VER_MINOR > 5 12 | # error "Invalid libgit2 version; this git2go supports libgit2 between v1.5.0 and v1.5.0" 13 | #endif 14 | */ 15 | import "C" 16 | -------------------------------------------------------------------------------- /Build_system_static.go: -------------------------------------------------------------------------------- 1 | //go:build static && system_libgit2 2 | // +build static,system_libgit2 3 | 4 | package git 5 | 6 | /* 7 | #cgo pkg-config: libgit2 --static 8 | #cgo CFLAGS: -DLIBGIT2_STATIC 9 | #include 10 | 11 | #if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 5 || LIBGIT2_VER_MINOR > 5 12 | # error "Invalid libgit2 version; this git2go supports libgit2 between v1.5.0 and v1.5.0" 13 | #endif 14 | */ 15 | import "C" 16 | -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/info/refs: -------------------------------------------------------------------------------- 1 | 0966a434eb1a025db6b71485ab63a3bfbea520b6 refs/heads/first-merge 2 | 49322bb17d3acc9146f98c97d078513228bbf3c0 refs/heads/master 3 | 42e4e7c5e507e113ebbb7801b16b52cf867b7ce1 refs/heads/no-parent 4 | d96c4e80345534eccee5ac7b07fc7603b56124cb refs/tags/annotated_tag 5 | c070ad8c08840c8116da865b2d65593a6bb9cd2a refs/tags/annotated_tag^{} 6 | 55a1a760df4b86a02094a904dfa511deb5655905 refs/tags/blob 7 | 8f50ba15d49353813cc6e20298002c0d17b0a9ee refs/tags/commit_tree 8 | 6e0c7bdb9b4ed93212491ee778ca1c65047cab4e refs/tags/nearly-dangling 9 | -------------------------------------------------------------------------------- /testdata/TestGitRepository.git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled sorted 2 | 0966a434eb1a025db6b71485ab63a3bfbea520b6 refs/heads/first-merge 3 | 49322bb17d3acc9146f98c97d078513228bbf3c0 refs/heads/master 4 | 42e4e7c5e507e113ebbb7801b16b52cf867b7ce1 refs/heads/no-parent 5 | d96c4e80345534eccee5ac7b07fc7603b56124cb refs/tags/annotated_tag 6 | ^c070ad8c08840c8116da865b2d65593a6bb9cd2a 7 | 55a1a760df4b86a02094a904dfa511deb5655905 refs/tags/blob 8 | 8f50ba15d49353813cc6e20298002c0d17b0a9ee refs/tags/commit_tree 9 | 6e0c7bdb9b4ed93212491ee778ca1c65047cab4e refs/tags/nearly-dangling 10 | -------------------------------------------------------------------------------- /submodule_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSubmoduleForeach(t *testing.T) { 8 | t.Parallel() 9 | repo := createTestRepo(t) 10 | defer cleanupTestRepo(t, repo) 11 | 12 | seedTestRepo(t, repo) 13 | 14 | _, err := repo.Submodules.Add("http://example.org/submodule", "submodule", true) 15 | checkFatal(t, err) 16 | 17 | i := 0 18 | err = repo.Submodules.Foreach(func(sub *Submodule, name string) error { 19 | i++ 20 | return nil 21 | }) 22 | checkFatal(t, err) 23 | 24 | if i != 1 { 25 | t.Fatalf("expected one submodule found but got %d", i) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Build_bundled_static.go: -------------------------------------------------------------------------------- 1 | //go:build static && !system_libgit2 2 | // +build static,!system_libgit2 3 | 4 | package git 5 | 6 | /* 7 | #cgo windows CFLAGS: -I${SRCDIR}/static-build/install/include/ 8 | #cgo windows LDFLAGS: -L${SRCDIR}/static-build/install/lib/ -lgit2 -lwinhttp -lws2_32 -lole32 -lrpcrt4 -lcrypt32 9 | #cgo !windows pkg-config: --static ${SRCDIR}/static-build/install/lib/pkgconfig/libgit2.pc 10 | #cgo CFLAGS: -DLIBGIT2_STATIC 11 | #include 12 | 13 | #if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 5 || LIBGIT2_VER_MINOR > 5 14 | # error "Invalid libgit2 version; this git2go supports libgit2 between v1.5.0 and v1.5.0" 15 | #endif 16 | */ 17 | import "C" 18 | -------------------------------------------------------------------------------- /script/install-libgit2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Install libgit2 to git2go in dynamic mode on Travis 5 | # 6 | 7 | set -ex 8 | 9 | # We don't want to build libgit2 on the next branch, as we carry a 10 | # submodule with the exact version we support 11 | if [ "x$TRAVIS_BRANCH" = "xnext" ]; then 12 | exit 0 13 | fi 14 | 15 | cd "${HOME}" 16 | LG2VER="0.24.0" 17 | wget -O libgit2-${LG2VER}.tar.gz https://github.com/libgit2/libgit2/archive/v${LG2VER}.tar.gz 18 | tar -xzvf libgit2-${LG2VER}.tar.gz 19 | cd libgit2-${LG2VER} && mkdir build && cd build 20 | cmake -DTHREADSAFE=ON -DBUILD_CLAR=OFF -DCMAKE_BUILD_TYPE="RelWithDebInfo" .. && make && sudo make install 21 | sudo ldconfig 22 | cd "${TRAVIS_BUILD_DIR}" 23 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag new releases 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - release-* 7 | 8 | jobs: 9 | 10 | tag-release: 11 | name: Bump tag in ${{ github.ref }} 12 | 13 | runs-on: ubuntu-20.04 14 | 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v1 18 | with: 19 | fetch-depth: 0 20 | - name: Bump version and push tag 21 | id: bump-version 22 | uses: anothrNick/github-tag-action@43ed073f5c1445ca8b80d920ce2f8fa550ae4e8d 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | WITH_V: true 26 | DEFAULT_BUMP: patch 27 | TAG_CONTEXT: branch 28 | RELEASE_BRANCHES: .* 29 | -------------------------------------------------------------------------------- /push_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRemotePush(t *testing.T) { 8 | t.Parallel() 9 | repo := createBareTestRepo(t) 10 | defer cleanupTestRepo(t, repo) 11 | 12 | localRepo := createTestRepo(t) 13 | defer cleanupTestRepo(t, localRepo) 14 | 15 | remote, err := localRepo.Remotes.Create("test_push", repo.Path()) 16 | checkFatal(t, err) 17 | defer remote.Free() 18 | 19 | seedTestRepo(t, localRepo) 20 | 21 | err = remote.Push([]string{"refs/heads/master"}, nil) 22 | checkFatal(t, err) 23 | 24 | ref, err := localRepo.References.Lookup("refs/remotes/test_push/master") 25 | checkFatal(t, err) 26 | defer ref.Free() 27 | 28 | ref, err = repo.References.Lookup("refs/heads/master") 29 | checkFatal(t, err) 30 | defer ref.Free() 31 | } 32 | -------------------------------------------------------------------------------- /features.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | 8 | type Feature int 9 | 10 | const ( 11 | // libgit2 was built with threading support 12 | FeatureThreads Feature = C.GIT_FEATURE_THREADS 13 | 14 | // libgit2 was built with HTTPS support built-in 15 | FeatureHTTPS Feature = C.GIT_FEATURE_HTTPS 16 | 17 | // libgit2 was build with SSH support built-in 18 | FeatureSSH Feature = C.GIT_FEATURE_SSH 19 | 20 | // libgit2 was built with nanosecond support for files 21 | FeatureNSec Feature = C.GIT_FEATURE_NSEC 22 | ) 23 | 24 | // Features returns a bit-flag of Feature values indicating which features the 25 | // loaded libgit2 library has. 26 | func Features() Feature { 27 | features := C.git_libgit2_features() 28 | 29 | return Feature(features) 30 | } 31 | -------------------------------------------------------------------------------- /patch_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestPatch(t *testing.T) { 9 | t.Parallel() 10 | repo := createTestRepo(t) 11 | defer cleanupTestRepo(t, repo) 12 | 13 | _, originalTreeId := seedTestRepo(t, repo) 14 | originalTree, err := repo.LookupTree(originalTreeId) 15 | 16 | checkFatal(t, err) 17 | 18 | _, newTreeId := updateReadme(t, repo, "file changed\n") 19 | 20 | newTree, err := repo.LookupTree(newTreeId) 21 | checkFatal(t, err) 22 | 23 | opts := &DiffOptions{ 24 | OldPrefix: "a", 25 | NewPrefix: "b", 26 | } 27 | diff, err := repo.DiffTreeToTree(originalTree, newTree, opts) 28 | checkFatal(t, err) 29 | 30 | patch, err := diff.Patch(0) 31 | checkFatal(t, err) 32 | 33 | patchStr, err := patch.String() 34 | checkFatal(t, err) 35 | if strings.Index(patchStr, "diff --git a/README b/README\nindex 257cc56..820734a 100644\n--- a/README\n+++ b/README\n@@ -1 +1 @@\n-foo\n+file changed") == -1 { 36 | t.Fatalf("patch was bad") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /delta_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type Delta -trimprefix Delta -tags static"; DO NOT EDIT. 2 | 3 | package git 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[DeltaUnmodified-0] 12 | _ = x[DeltaAdded-1] 13 | _ = x[DeltaDeleted-2] 14 | _ = x[DeltaModified-3] 15 | _ = x[DeltaRenamed-4] 16 | _ = x[DeltaCopied-5] 17 | _ = x[DeltaIgnored-6] 18 | _ = x[DeltaUntracked-7] 19 | _ = x[DeltaTypeChange-8] 20 | _ = x[DeltaUnreadable-9] 21 | _ = x[DeltaConflicted-10] 22 | } 23 | 24 | const _Delta_name = "UnmodifiedAddedDeletedModifiedRenamedCopiedIgnoredUntrackedTypeChangeUnreadableConflicted" 25 | 26 | var _Delta_index = [...]uint8{0, 10, 15, 22, 30, 37, 43, 50, 59, 69, 79, 89} 27 | 28 | func (i Delta) String() string { 29 | if i < 0 || i >= Delta(len(_Delta_index)-1) { 30 | return "Delta(" + strconv.FormatInt(int64(i), 10) + ")" 31 | } 32 | return _Delta_name[_Delta_index[i]:_Delta_index[i+1]] 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 The git2go contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestTrailers(t *testing.T) { 10 | t.Parallel() 11 | tests := []struct { 12 | input string 13 | expected []Trailer 14 | }{ 15 | { 16 | "commit with zero trailers\n", 17 | []Trailer{}, 18 | }, 19 | { 20 | "commit with one trailer\n\nCo-authored-by: Alice \n", 21 | []Trailer{ 22 | Trailer{Key: "Co-authored-by", Value: "Alice "}, 23 | }, 24 | }, 25 | { 26 | "commit with two trailers\n\nCo-authored-by: Alice \nSigned-off-by: Bob \n", 27 | []Trailer{ 28 | Trailer{Key: "Co-authored-by", Value: "Alice "}, 29 | Trailer{Key: "Signed-off-by", Value: "Bob "}}, 30 | }, 31 | } 32 | for _, test := range tests { 33 | fmt.Printf("%s", test.input) 34 | actual, err := MessageTrailers(test.input) 35 | if err != nil { 36 | t.Errorf("Trailers returned an unexpected error: %v", err) 37 | } 38 | if !reflect.DeepEqual(test.expected, actual) { 39 | t.Errorf("expecting %#v\ngot %#v", test.expected, actual) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ignore.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "runtime" 9 | "unsafe" 10 | ) 11 | 12 | func (v *Repository) AddIgnoreRule(rules string) error { 13 | crules := C.CString(rules) 14 | defer C.free(unsafe.Pointer(crules)) 15 | 16 | runtime.LockOSThread() 17 | defer runtime.UnlockOSThread() 18 | 19 | ret := C.git_ignore_add_rule(v.ptr, crules) 20 | runtime.KeepAlive(v) 21 | if ret < 0 { 22 | return MakeGitError(ret) 23 | } 24 | return nil 25 | } 26 | 27 | func (v *Repository) ClearInternalIgnoreRules() error { 28 | runtime.LockOSThread() 29 | defer runtime.UnlockOSThread() 30 | 31 | ret := C.git_ignore_clear_internal_rules(v.ptr) 32 | runtime.KeepAlive(v) 33 | if ret < 0 { 34 | return MakeGitError(ret) 35 | } 36 | return nil 37 | } 38 | 39 | func (v *Repository) IsPathIgnored(path string) (bool, error) { 40 | var ignored C.int 41 | 42 | cpath := C.CString(path) 43 | defer C.free(unsafe.Pointer(cpath)) 44 | 45 | runtime.LockOSThread() 46 | defer runtime.UnlockOSThread() 47 | 48 | ret := C.git_ignore_path_is_ignored(&ignored, v.ptr, cpath) 49 | runtime.KeepAlive(v) 50 | if ret < 0 { 51 | return false, MakeGitError(ret) 52 | } 53 | return ignored == 1, nil 54 | } 55 | -------------------------------------------------------------------------------- /reset.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import "runtime" 8 | 9 | type ResetType int 10 | 11 | const ( 12 | ResetSoft ResetType = C.GIT_RESET_SOFT 13 | ResetMixed ResetType = C.GIT_RESET_MIXED 14 | ResetHard ResetType = C.GIT_RESET_HARD 15 | ) 16 | 17 | func (r *Repository) ResetToCommit(commit *Commit, resetType ResetType, opts *CheckoutOptions) error { 18 | runtime.LockOSThread() 19 | defer runtime.UnlockOSThread() 20 | 21 | var err error 22 | cOpts := populateCheckoutOptions(&C.git_checkout_options{}, opts, &err) 23 | defer freeCheckoutOptions(cOpts) 24 | 25 | ret := C.git_reset(r.ptr, commit.ptr, C.git_reset_t(resetType), cOpts) 26 | if ret == C.int(ErrorCodeUser) && err != nil { 27 | return err 28 | } 29 | if ret < 0 { 30 | return MakeGitError(ret) 31 | } 32 | return nil 33 | } 34 | 35 | func (r *Repository) ResetDefaultToCommit(commit *Commit, pathspecs []string) error { 36 | cpathspecs := C.git_strarray{} 37 | cpathspecs.count = C.size_t(len(pathspecs)) 38 | cpathspecs.strings = makeCStringsFromStrings(pathspecs) 39 | defer freeStrarray(&cpathspecs) 40 | 41 | runtime.LockOSThread() 42 | defer runtime.UnlockOSThread() 43 | ret := C.git_reset_default(r.ptr, commit.ptr, &cpathspecs) 44 | 45 | if ret < 0 { 46 | return MakeGitError(ret) 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "runtime" 9 | "unsafe" 10 | ) 11 | 12 | // Trailer represents a single git message trailer. 13 | type Trailer struct { 14 | Key string 15 | Value string 16 | } 17 | 18 | // MessageTrailers parses trailers out of a message, returning a slice of 19 | // Trailer structs. Trailers are key/value pairs in the last paragraph of a 20 | // message, not including any patches or conflicts that may be present. 21 | func MessageTrailers(message string) ([]Trailer, error) { 22 | var trailersC C.git_message_trailer_array 23 | 24 | messageC := C.CString(message) 25 | defer C.free(unsafe.Pointer(messageC)) 26 | 27 | runtime.LockOSThread() 28 | defer runtime.UnlockOSThread() 29 | 30 | ecode := C.git_message_trailers(&trailersC, messageC) 31 | if ecode < 0 { 32 | return nil, MakeGitError(ecode) 33 | } 34 | defer C.git_message_trailer_array_free(&trailersC) 35 | trailers := make([]Trailer, trailersC.count) 36 | var trailer *C.git_message_trailer 37 | for i, p := 0, uintptr(unsafe.Pointer(trailersC.trailers)); i < int(trailersC.count); i, p = i+1, p+unsafe.Sizeof(C.git_message_trailer{}) { 38 | trailer = (*C.git_message_trailer)(unsafe.Pointer(p)) 39 | trailers[i] = Trailer{Key: C.GoString(trailer.key), Value: C.GoString(trailer.value)} 40 | } 41 | return trailers, nil 42 | } 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 2 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 3 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 4 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c h1:9HhBz5L/UjnK9XLtiZhYAdue5BVKep3PMmS2LuPDt8k= 5 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 6 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 7 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 8 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88 h1:KmZPnMocC93w341XZp26yTJg8Za7lhb2KhkYmixoeso= 10 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 11 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= 12 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 13 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 14 | -------------------------------------------------------------------------------- /revparse_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRevparse(t *testing.T) { 8 | t.Parallel() 9 | repo := createTestRepo(t) 10 | defer cleanupTestRepo(t, repo) 11 | 12 | commitId, _ := seedTestRepo(t, repo) 13 | 14 | revSpec, err := repo.Revparse("HEAD") 15 | checkFatal(t, err) 16 | 17 | checkObject(t, revSpec.From(), commitId) 18 | } 19 | 20 | func TestRevparseSingle(t *testing.T) { 21 | t.Parallel() 22 | repo := createTestRepo(t) 23 | defer cleanupTestRepo(t, repo) 24 | 25 | commitId, _ := seedTestRepo(t, repo) 26 | 27 | obj, err := repo.RevparseSingle("HEAD") 28 | checkFatal(t, err) 29 | 30 | checkObject(t, obj, commitId) 31 | } 32 | 33 | func TestRevparseExt(t *testing.T) { 34 | t.Parallel() 35 | repo := createTestRepo(t) 36 | defer cleanupTestRepo(t, repo) 37 | 38 | _, treeId := seedTestRepo(t, repo) 39 | 40 | ref, err := repo.References.Create("refs/heads/master", treeId, true, "") 41 | checkFatal(t, err) 42 | 43 | obj, ref, err := repo.RevparseExt("master") 44 | checkFatal(t, err) 45 | 46 | checkObject(t, obj, treeId) 47 | if ref == nil { 48 | t.Fatalf("bad reference") 49 | } 50 | } 51 | 52 | func checkObject(t *testing.T, obj *Object, id *Oid) { 53 | if obj == nil { 54 | t.Fatalf("bad object") 55 | } 56 | 57 | if !obj.Id().Equal(id) { 58 | t.Fatalf("bad object, expected %s, got %s", id.String(), obj.Id().String()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /blob_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | type bufWrapper struct { 9 | buf [64]byte 10 | pointer []byte 11 | } 12 | 13 | func doublePointerBytes() []byte { 14 | o := &bufWrapper{} 15 | o.pointer = o.buf[0:10] 16 | return o.pointer[0:1] 17 | } 18 | 19 | func TestCreateBlobFromBuffer(t *testing.T) { 20 | t.Parallel() 21 | repo := createTestRepo(t) 22 | defer cleanupTestRepo(t, repo) 23 | 24 | id, err := repo.CreateBlobFromBuffer(make([]byte, 0)) 25 | checkFatal(t, err) 26 | 27 | if id.String() != "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391" { 28 | t.Fatal("Empty buffer did not deliver empty blob id") 29 | } 30 | 31 | tests := []struct { 32 | data []byte 33 | isBinary bool 34 | }{ 35 | { 36 | data: []byte("hello there"), 37 | isBinary: false, 38 | }, 39 | { 40 | data: doublePointerBytes(), 41 | isBinary: true, 42 | }, 43 | } 44 | for _, tt := range tests { 45 | data := tt.data 46 | id, err = repo.CreateBlobFromBuffer(data) 47 | checkFatal(t, err) 48 | 49 | blob, err := repo.LookupBlob(id) 50 | checkFatal(t, err) 51 | if !bytes.Equal(blob.Contents(), data) { 52 | t.Fatal("Loaded bytes don't match original bytes:", 53 | blob.Contents(), "!=", data) 54 | } 55 | want := tt.isBinary 56 | if got := blob.IsBinary(); got != want { 57 | t.Fatalf("IsBinary() = %v, want %v", got, want) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /branch_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBranchIterator(t *testing.T) { 8 | t.Parallel() 9 | repo := createTestRepo(t) 10 | defer cleanupTestRepo(t, repo) 11 | 12 | seedTestRepo(t, repo) 13 | 14 | i, err := repo.NewBranchIterator(BranchLocal) 15 | checkFatal(t, err) 16 | 17 | b, bt, err := i.Next() 18 | checkFatal(t, err) 19 | if name, _ := b.Name(); name != "master" { 20 | t.Fatalf("expected master") 21 | } else if bt != BranchLocal { 22 | t.Fatalf("expected BranchLocal, not %v", t) 23 | } 24 | b, bt, err = i.Next() 25 | if !IsErrorCode(err, ErrorCodeIterOver) { 26 | t.Fatal("expected iterover") 27 | } 28 | } 29 | 30 | func TestBranchIteratorEach(t *testing.T) { 31 | t.Parallel() 32 | repo := createTestRepo(t) 33 | defer cleanupTestRepo(t, repo) 34 | 35 | seedTestRepo(t, repo) 36 | 37 | i, err := repo.NewBranchIterator(BranchLocal) 38 | checkFatal(t, err) 39 | 40 | var names []string 41 | f := func(b *Branch, t BranchType) error { 42 | name, err := b.Name() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | names = append(names, name) 48 | return nil 49 | } 50 | 51 | err = i.ForEach(f) 52 | if err != nil && !IsErrorCode(err, ErrorCodeIterOver) { 53 | t.Fatal(err) 54 | } 55 | 56 | if len(names) != 1 { 57 | t.Fatalf("expect 1 branch, but it was %d\n", len(names)) 58 | } 59 | 60 | if names[0] != "master" { 61 | t.Fatalf("expect branch master, but it was %s\n", names[0]) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /refdb.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | #include 6 | 7 | extern void _go_git_refdb_backend_free(git_refdb_backend *backend); 8 | */ 9 | import "C" 10 | import ( 11 | "runtime" 12 | "unsafe" 13 | ) 14 | 15 | type Refdb struct { 16 | doNotCompare 17 | ptr *C.git_refdb 18 | r *Repository 19 | } 20 | 21 | type RefdbBackend struct { 22 | doNotCompare 23 | ptr *C.git_refdb_backend 24 | } 25 | 26 | func (v *Repository) NewRefdb() (refdb *Refdb, err error) { 27 | var ptr *C.git_refdb 28 | 29 | runtime.LockOSThread() 30 | defer runtime.UnlockOSThread() 31 | 32 | ret := C.git_refdb_new(&ptr, v.ptr) 33 | if ret < 0 { 34 | return nil, MakeGitError(ret) 35 | } 36 | 37 | refdb = &Refdb{ptr: ptr, r: v} 38 | runtime.SetFinalizer(refdb, (*Refdb).Free) 39 | return refdb, nil 40 | } 41 | 42 | func NewRefdbBackendFromC(ptr unsafe.Pointer) (backend *RefdbBackend) { 43 | backend = &RefdbBackend{ptr: (*C.git_refdb_backend)(ptr)} 44 | return backend 45 | } 46 | 47 | func (v *Refdb) SetBackend(backend *RefdbBackend) (err error) { 48 | runtime.LockOSThread() 49 | defer runtime.UnlockOSThread() 50 | 51 | ret := C.git_refdb_set_backend(v.ptr, backend.ptr) 52 | runtime.KeepAlive(v) 53 | runtime.KeepAlive(backend) 54 | if ret < 0 { 55 | backend.Free() 56 | return MakeGitError(ret) 57 | } 58 | return nil 59 | } 60 | 61 | func (v *RefdbBackend) Free() { 62 | runtime.SetFinalizer(v, nil) 63 | C._go_git_refdb_backend_free(v.ptr) 64 | } 65 | -------------------------------------------------------------------------------- /reset_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | ) 7 | 8 | func TestResetToCommit(t *testing.T) { 9 | t.Parallel() 10 | repo := createTestRepo(t) 11 | defer cleanupTestRepo(t, repo) 12 | 13 | seedTestRepo(t, repo) 14 | // create commit to reset to 15 | commitId, _ := updateReadme(t, repo, "testing reset") 16 | // create commit to reset from 17 | nextCommitId, _ := updateReadme(t, repo, "will be reset") 18 | 19 | // confirm that we wrote "will be reset" to the readme 20 | newBytes, err := ioutil.ReadFile(pathInRepo(repo, "README")) 21 | checkFatal(t, err) 22 | if string(newBytes) != "will be reset" { 23 | t.Fatalf("expected %s to equal 'will be reset'", string(newBytes)) 24 | } 25 | 26 | // confirm that the head of the repo is the next commit id 27 | head, err := repo.Head() 28 | checkFatal(t, err) 29 | if head.Target().String() != nextCommitId.String() { 30 | t.Fatalf( 31 | "expected to be at latest commit %s, but was %s", 32 | nextCommitId.String(), 33 | head.Target().String(), 34 | ) 35 | } 36 | 37 | commitToResetTo, err := repo.LookupCommit(commitId) 38 | checkFatal(t, err) 39 | 40 | repo.ResetToCommit(commitToResetTo, ResetHard, &CheckoutOptions{}) 41 | 42 | // check that the file now reads "testing reset" like it did before 43 | bytes, err := ioutil.ReadFile(pathInRepo(repo, "README")) 44 | checkFatal(t, err) 45 | if string(bytes) != "testing reset" { 46 | t.Fatalf("expected %s to equal 'testing reset'", string(bytes)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /handles.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "fmt" 9 | "sync" 10 | "unsafe" 11 | ) 12 | 13 | type HandleList struct { 14 | doNotCompare 15 | sync.RWMutex 16 | // stores the Go pointers 17 | handles map[unsafe.Pointer]interface{} 18 | } 19 | 20 | func NewHandleList() *HandleList { 21 | return &HandleList{ 22 | handles: make(map[unsafe.Pointer]interface{}), 23 | } 24 | } 25 | 26 | // Track adds the given pointer to the list of pointers to track and 27 | // returns a pointer value which can be passed to C as an opaque 28 | // pointer. 29 | func (v *HandleList) Track(pointer interface{}) unsafe.Pointer { 30 | handle := C.malloc(1) 31 | 32 | v.Lock() 33 | v.handles[handle] = pointer 34 | v.Unlock() 35 | 36 | return handle 37 | } 38 | 39 | // Untrack stops tracking the pointer given by the handle 40 | func (v *HandleList) Untrack(handle unsafe.Pointer) { 41 | v.Lock() 42 | delete(v.handles, handle) 43 | C.free(handle) 44 | v.Unlock() 45 | } 46 | 47 | // Clear stops tracking all the managed pointers. 48 | func (v *HandleList) Clear() { 49 | v.Lock() 50 | for handle := range v.handles { 51 | delete(v.handles, handle) 52 | C.free(handle) 53 | } 54 | v.Unlock() 55 | } 56 | 57 | // Get retrieves the pointer from the given handle 58 | func (v *HandleList) Get(handle unsafe.Pointer) interface{} { 59 | v.RLock() 60 | defer v.RUnlock() 61 | 62 | ptr, ok := v.handles[handle] 63 | if !ok { 64 | panic(fmt.Sprintf("invalid pointer handle: %p", handle)) 65 | } 66 | 67 | return ptr 68 | } 69 | -------------------------------------------------------------------------------- /mempack_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestMempack(t *testing.T) { 9 | t.Parallel() 10 | 11 | odb, err := NewOdb() 12 | checkFatal(t, err) 13 | 14 | repo, err := NewRepositoryWrapOdb(odb) 15 | checkFatal(t, err) 16 | 17 | mempack, err := NewMempack(odb) 18 | checkFatal(t, err) 19 | 20 | id, err := odb.Write([]byte("hello, world!"), ObjectBlob) 21 | checkFatal(t, err) 22 | 23 | expectedId, err := NewOid("30f51a3fba5274d53522d0f19748456974647b4f") 24 | checkFatal(t, err) 25 | if !expectedId.Equal(id) { 26 | t.Errorf("mismatched id. expected %v, got %v", expectedId.String(), id.String()) 27 | } 28 | 29 | // The object should be available from the odb. 30 | { 31 | obj, err := odb.Read(expectedId) 32 | checkFatal(t, err) 33 | defer obj.Free() 34 | } 35 | 36 | data, err := mempack.Dump(repo) 37 | checkFatal(t, err) 38 | 39 | expectedData := []byte{ 40 | 0x50, 0x41, 0x43, 0x4b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 41 | 0x02, 0x9d, 0x08, 0x82, 0x3b, 0xd8, 0xa8, 0xea, 0xb5, 0x10, 0xad, 0x6a, 42 | 0xc7, 0x5c, 0x82, 0x3c, 0xfd, 0x3e, 0xd3, 0x1e, 43 | } 44 | if !bytes.Equal(expectedData, data) { 45 | t.Errorf("mismatched mempack data. expected %v, got %v", expectedData, data) 46 | } 47 | 48 | mempack.Reset() 49 | 50 | // After the reset, the object should now be unavailable. 51 | { 52 | obj, err := odb.Read(expectedId) 53 | if err == nil { 54 | t.Errorf("object %s unexpectedly found", obj.Id().String()) 55 | obj.Free() 56 | } else if !IsErrorCode(err, ErrorCodeNotFound) { 57 | t.Errorf("unexpected error %v", err) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /difflinetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type DiffLineType -trimprefix DiffLine -tags static"; DO NOT EDIT. 2 | 3 | package git 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[DiffLineContext-32] 12 | _ = x[DiffLineAddition-43] 13 | _ = x[DiffLineDeletion-45] 14 | _ = x[DiffLineContextEOFNL-61] 15 | _ = x[DiffLineAddEOFNL-62] 16 | _ = x[DiffLineDelEOFNL-60] 17 | _ = x[DiffLineFileHdr-70] 18 | _ = x[DiffLineHunkHdr-72] 19 | _ = x[DiffLineBinary-66] 20 | } 21 | 22 | const ( 23 | _DiffLineType_name_0 = "Context" 24 | _DiffLineType_name_1 = "Addition" 25 | _DiffLineType_name_2 = "Deletion" 26 | _DiffLineType_name_3 = "DelEOFNLContextEOFNLAddEOFNL" 27 | _DiffLineType_name_4 = "Binary" 28 | _DiffLineType_name_5 = "FileHdr" 29 | _DiffLineType_name_6 = "HunkHdr" 30 | ) 31 | 32 | var ( 33 | _DiffLineType_index_3 = [...]uint8{0, 8, 20, 28} 34 | ) 35 | 36 | func (i DiffLineType) String() string { 37 | switch { 38 | case i == 32: 39 | return _DiffLineType_name_0 40 | case i == 43: 41 | return _DiffLineType_name_1 42 | case i == 45: 43 | return _DiffLineType_name_2 44 | case 60 <= i && i <= 62: 45 | i -= 60 46 | return _DiffLineType_name_3[_DiffLineType_index_3[i]:_DiffLineType_index_3[i+1]] 47 | case i == 66: 48 | return _DiffLineType_name_4 49 | case i == 70: 50 | return _DiffLineType_name_5 51 | case i == 72: 52 | return _DiffLineType_name_6 53 | default: 54 | return "DiffLineType(" + strconv.FormatInt(int64(i), 10) + ")" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /signature.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "runtime" 9 | "time" 10 | "unsafe" 11 | ) 12 | 13 | type Signature struct { 14 | Name string 15 | Email string 16 | When time.Time 17 | } 18 | 19 | func newSignatureFromC(sig *C.git_signature) *Signature { 20 | if sig == nil { 21 | return nil 22 | } 23 | 24 | // git stores minutes, go wants seconds 25 | loc := time.FixedZone("", int(sig.when.offset)*60) 26 | return &Signature{ 27 | C.GoString(sig.name), 28 | C.GoString(sig.email), 29 | time.Unix(int64(sig.when.time), 0).In(loc), 30 | } 31 | } 32 | 33 | // Offset returns the time zone offset of v.When in minutes, which is what git wants. 34 | func (v *Signature) Offset() int { 35 | _, offset := v.When.Zone() 36 | return offset / 60 37 | } 38 | 39 | func (sig *Signature) toC() (*C.git_signature, error) { 40 | if sig == nil { 41 | return nil, nil 42 | } 43 | 44 | var out *C.git_signature 45 | 46 | runtime.LockOSThread() 47 | defer runtime.UnlockOSThread() 48 | 49 | name := C.CString(sig.Name) 50 | defer C.free(unsafe.Pointer(name)) 51 | 52 | email := C.CString(sig.Email) 53 | defer C.free(unsafe.Pointer(email)) 54 | 55 | ret := C.git_signature_new(&out, name, email, C.git_time_t(sig.When.Unix()), C.int(sig.Offset())) 56 | if ret < 0 { 57 | return nil, MakeGitError(ret) 58 | } 59 | 60 | return out, nil 61 | } 62 | 63 | func (repo *Repository) DefaultSignature() (*Signature, error) { 64 | var out *C.git_signature 65 | 66 | runtime.LockOSThread() 67 | defer runtime.UnlockOSThread() 68 | 69 | cErr := C.git_signature_default(&out, repo.ptr) 70 | runtime.KeepAlive(repo) 71 | if cErr < 0 { 72 | return nil, MakeGitError(cErr) 73 | } 74 | 75 | defer C.git_signature_free(out) 76 | 77 | return newSignatureFromC(out), nil 78 | } 79 | -------------------------------------------------------------------------------- /graph.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "runtime" 9 | ) 10 | 11 | func (repo *Repository) DescendantOf(commit, ancestor *Oid) (bool, error) { 12 | runtime.LockOSThread() 13 | defer runtime.UnlockOSThread() 14 | 15 | ret := C.git_graph_descendant_of(repo.ptr, commit.toC(), ancestor.toC()) 16 | runtime.KeepAlive(repo) 17 | runtime.KeepAlive(commit) 18 | runtime.KeepAlive(ancestor) 19 | if ret < 0 { 20 | return false, MakeGitError(ret) 21 | } 22 | 23 | return (ret > 0), nil 24 | } 25 | 26 | func (repo *Repository) AheadBehind(local, upstream *Oid) (ahead, behind int, err error) { 27 | runtime.LockOSThread() 28 | defer runtime.UnlockOSThread() 29 | 30 | var aheadT C.size_t 31 | var behindT C.size_t 32 | 33 | ret := C.git_graph_ahead_behind(&aheadT, &behindT, repo.ptr, local.toC(), upstream.toC()) 34 | runtime.KeepAlive(repo) 35 | runtime.KeepAlive(local) 36 | runtime.KeepAlive(upstream) 37 | if ret < 0 { 38 | return 0, 0, MakeGitError(ret) 39 | } 40 | 41 | return int(aheadT), int(behindT), nil 42 | } 43 | 44 | // ReachableFromAny returns whether a commit is reachable from any of a list of 45 | // commits by following parent edges. 46 | func (repo *Repository) ReachableFromAny(commit *Oid, descendants []*Oid) (bool, error) { 47 | if len(descendants) == 0 { 48 | return false, nil 49 | } 50 | 51 | coids := make([]C.git_oid, len(descendants)) 52 | for i := 0; i < len(descendants); i++ { 53 | coids[i] = *descendants[i].toC() 54 | } 55 | 56 | runtime.LockOSThread() 57 | defer runtime.UnlockOSThread() 58 | 59 | ret := C.git_graph_reachable_from_any(repo.ptr, commit.toC(), &coids[0], C.size_t(len(descendants))) 60 | runtime.KeepAlive(repo) 61 | runtime.KeepAlive(commit) 62 | runtime.KeepAlive(coids) 63 | runtime.KeepAlive(descendants) 64 | if ret < 0 { 65 | return false, MakeGitError(ret) 66 | } 67 | 68 | return (ret > 0), nil 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport to older releases 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | 9 | backport: 10 | name: Backport change to branch ${{ matrix.branch }} 11 | continue-on-error: true 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | branch: [ 'release-1.3', 'release-1.2', 'release-1.1', 'release-1.0', 'release-0.28', 'release-0.27' ] 16 | 17 | runs-on: ubuntu-20.04 18 | 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v1 22 | with: 23 | fetch-depth: 0 24 | - name: Create a cherry-pick PR 25 | run: | 26 | if ! git diff --quiet HEAD^ HEAD -- vendor/libgit2; then 27 | echo '::warning::Skipping cherry-pick since it is a vendored libgit2 bump' 28 | exit 0 29 | fi 30 | 31 | BRANCH_NAME="cherry-pick-${{ github.run_id }}-${{ matrix.branch }}" 32 | 33 | # Setup usernames and authentication 34 | git config --global user.name "${{ github.actor }}" 35 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 36 | cat <<- EOF > $HOME/.netrc 37 | machine github.com 38 | login ${{ github.actor }} 39 | password ${{ secrets.GITHUB_TOKEN }} 40 | machine api.github.com 41 | login ${{ github.actor }} 42 | password ${{ secrets.GITHUB_TOKEN }} 43 | EOF 44 | chmod 600 $HOME/.netrc 45 | 46 | # Create the cherry-pick commit and create the PR for it. 47 | git checkout "${{ matrix.branch }}" 48 | git switch -c "${BRANCH_NAME}" 49 | git cherry-pick -x "${{ github.sha }}" 50 | git push --set-upstream origin "${BRANCH_NAME}" 51 | GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh pr create \ 52 | --base "${{ matrix.branch }}" \ 53 | --title "$(git --no-pager show --format="%s" --no-patch HEAD)" \ 54 | --body "$(git --no-pager show --format="%b" --no-patch HEAD)" 55 | -------------------------------------------------------------------------------- /refspec_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRefspec(t *testing.T) { 8 | t.Parallel() 9 | 10 | const ( 11 | input = "+refs/heads/*:refs/remotes/origin/*" 12 | mainLocal = "refs/heads/main" 13 | mainRemote = "refs/remotes/origin/main" 14 | ) 15 | 16 | refspec, err := ParseRefspec(input, true) 17 | checkFatal(t, err) 18 | 19 | // Accessors 20 | 21 | s := refspec.String() 22 | if s != input { 23 | t.Errorf("expected string %q, got %q", input, s) 24 | } 25 | 26 | if d := refspec.Direction(); d != ConnectDirectionFetch { 27 | t.Errorf("expected fetch refspec, got direction %v", d) 28 | } 29 | 30 | if pat, expected := refspec.Src(), "refs/heads/*"; pat != expected { 31 | t.Errorf("expected refspec src %q, got %q", expected, pat) 32 | } 33 | 34 | if pat, expected := refspec.Dst(), "refs/remotes/origin/*"; pat != expected { 35 | t.Errorf("expected refspec dst %q, got %q", expected, pat) 36 | } 37 | 38 | if !refspec.Force() { 39 | t.Error("expected refspec force flag") 40 | } 41 | 42 | // SrcMatches 43 | 44 | if !refspec.SrcMatches(mainLocal) { 45 | t.Errorf("refspec source did not match %q", mainLocal) 46 | } 47 | 48 | if refspec.SrcMatches("refs/tags/v1.0") { 49 | t.Error("refspec source matched under refs/tags") 50 | } 51 | 52 | // DstMatches 53 | 54 | if !refspec.DstMatches(mainRemote) { 55 | t.Errorf("refspec destination did not match %q", mainRemote) 56 | } 57 | 58 | if refspec.DstMatches("refs/tags/v1.0") { 59 | t.Error("refspec destination matched under refs/tags") 60 | } 61 | 62 | // Transforms 63 | 64 | fromLocal, err := refspec.Transform(mainLocal) 65 | checkFatal(t, err) 66 | if fromLocal != mainRemote { 67 | t.Errorf("transform by refspec returned %s; expected %s", fromLocal, mainRemote) 68 | } 69 | 70 | fromRemote, err := refspec.Rtransform(mainRemote) 71 | checkFatal(t, err) 72 | if fromRemote != mainLocal { 73 | t.Errorf("rtransform by refspec returned %s; expected %s", fromRemote, mainLocal) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /blame_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestBlame(t *testing.T) { 9 | t.Parallel() 10 | repo := createTestRepo(t) 11 | defer cleanupTestRepo(t, repo) 12 | 13 | commitId1, _ := seedTestRepo(t, repo) 14 | commitId2, _ := updateReadme(t, repo, "foo\nbar\nbaz\n") 15 | 16 | opts := BlameOptions{ 17 | NewestCommit: commitId2, 18 | OldestCommit: nil, 19 | MinLine: 1, 20 | MaxLine: 3, 21 | } 22 | blame, err := repo.BlameFile("README", &opts) 23 | checkFatal(t, err) 24 | defer blame.Free() 25 | if blame.HunkCount() != 2 { 26 | t.Errorf("got hunk count %d, want 2", blame.HunkCount()) 27 | } 28 | 29 | wantHunk1 := BlameHunk{ 30 | LinesInHunk: 1, 31 | FinalCommitId: commitId1, 32 | FinalStartLineNumber: 1, 33 | OrigCommitId: commitId1, 34 | OrigPath: "README", 35 | OrigStartLineNumber: 1, 36 | Boundary: true, 37 | } 38 | wantHunk2 := BlameHunk{ 39 | LinesInHunk: 2, 40 | FinalCommitId: commitId2, 41 | FinalStartLineNumber: 2, 42 | OrigCommitId: commitId2, 43 | OrigPath: "README", 44 | OrigStartLineNumber: 2, 45 | Boundary: false, 46 | } 47 | 48 | hunk1, err := blame.HunkByIndex(0) 49 | checkFatal(t, err) 50 | checkHunk(t, "index 0", hunk1, wantHunk1) 51 | 52 | hunk2, err := blame.HunkByIndex(1) 53 | checkFatal(t, err) 54 | checkHunk(t, "index 1", hunk2, wantHunk2) 55 | 56 | hunkLine1, err := blame.HunkByLine(1) 57 | checkFatal(t, err) 58 | checkHunk(t, "line 1", hunkLine1, wantHunk1) 59 | 60 | hunkLine2, err := blame.HunkByLine(3) 61 | checkFatal(t, err) 62 | checkHunk(t, "line 2", hunkLine2, wantHunk2) 63 | } 64 | 65 | func checkHunk(t *testing.T, label string, hunk, want BlameHunk) { 66 | hunk.FinalSignature = nil 67 | want.FinalSignature = nil 68 | hunk.OrigSignature = nil 69 | want.OrigSignature = nil 70 | if !reflect.DeepEqual(hunk, want) { 71 | t.Fatalf("%s: got hunk %+v, want %+v", label, hunk, want) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST_ARGS ?= --count=1 2 | 3 | default: test 4 | 5 | 6 | generate: static-build/install/lib/libgit2.a 7 | go generate --tags "static" ./... 8 | 9 | # System library 10 | # ============== 11 | # This uses whatever version of libgit2 can be found in the system. 12 | test: 13 | go run script/check-MakeGitError-thread-lock.go 14 | go test $(TEST_ARGS) ./... 15 | 16 | install: 17 | go install ./... 18 | 19 | # Bundled dynamic library 20 | # ======================= 21 | # In order to avoid having to manipulate `git_dynamic.go`, which would prevent 22 | # the system-wide libgit2.so from being used in a sort of ergonomic way, this 23 | # instead moves the complexity of overriding the paths so that the built 24 | # libraries can be found by the build and tests. 25 | .PHONY: build-libgit2-dynamic 26 | build-libgit2-dynamic: 27 | ./script/build-libgit2-dynamic.sh 28 | 29 | dynamic-build/install/lib/libgit2.so: 30 | ./script/build-libgit2-dynamic.sh 31 | 32 | test-dynamic: dynamic-build/install/lib/libgit2.so 33 | PKG_CONFIG_PATH=dynamic-build/install/lib/pkgconfig \ 34 | go run script/check-MakeGitError-thread-lock.go 35 | PKG_CONFIG_PATH=dynamic-build/install/lib/pkgconfig \ 36 | LD_LIBRARY_PATH=dynamic-build/install/lib \ 37 | go test $(TEST_ARGS) ./... 38 | 39 | install-dynamic: dynamic-build/install/lib/libgit2.so 40 | PKG_CONFIG_PATH=dynamic-build/install/lib/pkgconfig \ 41 | go install ./... 42 | 43 | # Bundled static library 44 | # ====================== 45 | # This is mostly used in tests, but can also be used to provide a 46 | # statically-linked library with the bundled version of libgit2. 47 | .PHONY: build-libgit2-static 48 | build-libgit2-static: 49 | ./script/build-libgit2-static.sh 50 | 51 | static-build/install/lib/libgit2.a: 52 | ./script/build-libgit2-static.sh 53 | 54 | test-static: static-build/install/lib/libgit2.a 55 | go run script/check-MakeGitError-thread-lock.go 56 | go test --tags "static" $(TEST_ARGS) ./... 57 | 58 | install-static: static-build/install/lib/libgit2.a 59 | go install --tags "static" ./... 60 | -------------------------------------------------------------------------------- /transport_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | type testSmartSubtransport struct { 10 | } 11 | 12 | func (t *testSmartSubtransport) Action(url string, action SmartServiceAction) (SmartSubtransportStream, error) { 13 | return &testSmartSubtransportStream{}, nil 14 | } 15 | 16 | func (t *testSmartSubtransport) Close() error { 17 | return nil 18 | } 19 | 20 | func (t *testSmartSubtransport) Free() { 21 | } 22 | 23 | type testSmartSubtransportStream struct { 24 | } 25 | 26 | func (s *testSmartSubtransportStream) Read(buf []byte) (int, error) { 27 | payload := "" + 28 | "001e# service=git-upload-pack\n" + 29 | "0000005d0000000000000000000000000000000000000000 HEAD\x00symref=HEAD:refs/heads/master agent=libgit\n" + 30 | "003f0000000000000000000000000000000000000000 refs/heads/master\n" + 31 | "0000" 32 | 33 | return copy(buf, []byte(payload)), io.EOF 34 | } 35 | 36 | func (s *testSmartSubtransportStream) Write(buf []byte) (int, error) { 37 | return 0, io.EOF 38 | } 39 | 40 | func (s *testSmartSubtransportStream) Free() { 41 | } 42 | 43 | func TestTransport(t *testing.T) { 44 | t.Parallel() 45 | repo := createTestRepo(t) 46 | defer cleanupTestRepo(t, repo) 47 | 48 | callback := func(remote *Remote, transport *Transport) (SmartSubtransport, error) { 49 | return &testSmartSubtransport{}, nil 50 | } 51 | registeredSmartTransport, err := NewRegisteredSmartTransport("foo", true, callback) 52 | checkFatal(t, err) 53 | defer registeredSmartTransport.Free() 54 | 55 | remote, err := repo.Remotes.Create("test", "foo://bar") 56 | checkFatal(t, err) 57 | defer remote.Free() 58 | 59 | err = remote.ConnectFetch(nil, nil, nil) 60 | checkFatal(t, err) 61 | 62 | remoteHeads, err := remote.Ls() 63 | checkFatal(t, err) 64 | 65 | expectedRemoteHeads := []RemoteHead{ 66 | {&Oid{}, "HEAD"}, 67 | {&Oid{}, "refs/heads/master"}, 68 | } 69 | if !reflect.DeepEqual(expectedRemoteHeads, remoteHeads) { 70 | t.Errorf("mismatched remote heads. expected %v, got %v", expectedRemoteHeads, remoteHeads) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /script/check-MakeGitError-thread-lock.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "go/ast" 9 | "go/build" 10 | "go/parser" 11 | "go/printer" 12 | "go/token" 13 | "log" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | ) 18 | 19 | var ( 20 | fset = token.NewFileSet() 21 | ) 22 | 23 | func main() { 24 | log.SetFlags(0) 25 | 26 | bpkg, err := build.ImportDir(".", 0) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | pkgs, err := parser.ParseDir(fset, bpkg.Dir, func(fi os.FileInfo) bool { return filepath.Ext(fi.Name()) == ".go" }, 0) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | for _, pkg := range pkgs { 37 | if err := checkPkg(pkg); err != nil { 38 | log.Fatal(err) 39 | } 40 | } 41 | if len(pkgs) == 0 { 42 | log.Fatal("No packages to check.") 43 | } 44 | } 45 | 46 | var ignoreViolationsInFunc = map[string]bool{ 47 | "MakeGitError": true, 48 | "MakeGitError2": true, 49 | } 50 | 51 | func checkPkg(pkg *ast.Package) error { 52 | var violations []string 53 | ast.Inspect(pkg, func(node ast.Node) bool { 54 | switch node := node.(type) { 55 | case *ast.FuncDecl: 56 | var b bytes.Buffer 57 | if err := printer.Fprint(&b, fset, node); err != nil { 58 | log.Fatal(err) 59 | } 60 | src := b.String() 61 | 62 | if strings.Contains(src, "MakeGitError") && !strings.Contains(src, "runtime.LockOSThread()") && !strings.Contains(src, "defer runtime.UnlockOSThread()") && !ignoreViolationsInFunc[node.Name.Name] { 63 | pos := fset.Position(node.Pos()) 64 | violations = append(violations, fmt.Sprintf("%s at %s:%d", node.Name.Name, pos.Filename, pos.Line)) 65 | } 66 | } 67 | return true 68 | }) 69 | if len(violations) > 0 { 70 | return fmt.Errorf("%d non-thread-locked calls to MakeGitError found. To fix, add the following to each func below that calls MakeGitError, before the cgo call that might produce the error:\n\n\truntime.LockOSThread()\n\tdefer runtime.UnlockOSThread()\n\n%s", len(violations), strings.Join(violations, "\n")) 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /errorclass_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ErrorClass -trimprefix ErrorClass -tags static"; DO NOT EDIT. 2 | 3 | package git 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ErrorClassNone-0] 12 | _ = x[ErrorClassNoMemory-1] 13 | _ = x[ErrorClassOS-2] 14 | _ = x[ErrorClassInvalid-3] 15 | _ = x[ErrorClassReference-4] 16 | _ = x[ErrorClassZlib-5] 17 | _ = x[ErrorClassRepository-6] 18 | _ = x[ErrorClassConfig-7] 19 | _ = x[ErrorClassRegex-8] 20 | _ = x[ErrorClassOdb-9] 21 | _ = x[ErrorClassIndex-10] 22 | _ = x[ErrorClassObject-11] 23 | _ = x[ErrorClassNet-12] 24 | _ = x[ErrorClassTag-13] 25 | _ = x[ErrorClassTree-14] 26 | _ = x[ErrorClassIndexer-15] 27 | _ = x[ErrorClassSSL-16] 28 | _ = x[ErrorClassSubmodule-17] 29 | _ = x[ErrorClassThread-18] 30 | _ = x[ErrorClassStash-19] 31 | _ = x[ErrorClassCheckout-20] 32 | _ = x[ErrorClassFetchHead-21] 33 | _ = x[ErrorClassMerge-22] 34 | _ = x[ErrorClassSSH-23] 35 | _ = x[ErrorClassFilter-24] 36 | _ = x[ErrorClassRevert-25] 37 | _ = x[ErrorClassCallback-26] 38 | _ = x[ErrorClassRebase-29] 39 | _ = x[ErrorClassPatch-31] 40 | } 41 | 42 | const ( 43 | _ErrorClass_name_0 = "NoneNoMemoryOSInvalidReferenceZlibRepositoryConfigRegexOdbIndexObjectNetTagTreeIndexerSSLSubmoduleThreadStashCheckoutFetchHeadMergeSSHFilterRevertCallback" 44 | _ErrorClass_name_1 = "Rebase" 45 | _ErrorClass_name_2 = "Patch" 46 | ) 47 | 48 | var ( 49 | _ErrorClass_index_0 = [...]uint8{0, 4, 12, 14, 21, 30, 34, 44, 50, 55, 58, 63, 69, 72, 75, 79, 86, 89, 98, 104, 109, 117, 126, 131, 134, 140, 146, 154} 50 | ) 51 | 52 | func (i ErrorClass) String() string { 53 | switch { 54 | case 0 <= i && i <= 26: 55 | return _ErrorClass_name_0[_ErrorClass_index_0[i]:_ErrorClass_index_0[i+1]] 56 | case i == 29: 57 | return _ErrorClass_name_1 58 | case i == 31: 59 | return _ErrorClass_name_2 60 | default: 61 | return "ErrorClass(" + strconv.FormatInt(int64(i), 10) + ")" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /clone_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | REMOTENAME = "testremote" 11 | ) 12 | 13 | func TestClone(t *testing.T) { 14 | t.Parallel() 15 | repo := createTestRepo(t) 16 | defer cleanupTestRepo(t, repo) 17 | 18 | seedTestRepo(t, repo) 19 | 20 | path, err := ioutil.TempDir("", "git2go") 21 | checkFatal(t, err) 22 | 23 | ref, err := repo.References.Lookup("refs/heads/master") 24 | checkFatal(t, err) 25 | 26 | repo2, err := Clone(repo.Path(), path, &CloneOptions{Bare: true}) 27 | defer cleanupTestRepo(t, repo2) 28 | 29 | checkFatal(t, err) 30 | 31 | ref2, err := repo2.References.Lookup("refs/heads/master") 32 | checkFatal(t, err) 33 | 34 | if ref.Cmp(ref2) != 0 { 35 | t.Fatal("reference in clone does not match original ref") 36 | } 37 | } 38 | 39 | func TestCloneWithCallback(t *testing.T) { 40 | t.Parallel() 41 | testPayload := 0 42 | 43 | repo := createTestRepo(t) 44 | defer cleanupTestRepo(t, repo) 45 | 46 | seedTestRepo(t, repo) 47 | 48 | path, err := ioutil.TempDir("", "git2go") 49 | checkFatal(t, err) 50 | 51 | opts := CloneOptions{ 52 | Bare: true, 53 | RemoteCreateCallback: func(r *Repository, name, url string) (*Remote, error) { 54 | testPayload += 1 55 | return r.Remotes.Create(REMOTENAME, url) 56 | }, 57 | } 58 | 59 | repo2, err := Clone(repo.Path(), path, &opts) 60 | defer cleanupTestRepo(t, repo2) 61 | 62 | checkFatal(t, err) 63 | 64 | if testPayload != 1 { 65 | t.Fatal("Payload's value has not been changed") 66 | } 67 | 68 | remote, err := repo2.Remotes.Lookup(REMOTENAME) 69 | if err != nil || remote == nil { 70 | t.Fatal("Remote was not created properly") 71 | } 72 | defer remote.Free() 73 | } 74 | 75 | // TestCloneWithExternalHTTPUrl 76 | func TestCloneWithExternalHTTPUrl(t *testing.T) { 77 | 78 | path, err := ioutil.TempDir("", "git2go") 79 | defer os.RemoveAll(path) 80 | 81 | // clone the repo 82 | url := "https://github.com/libgit2/TestGitRepository" 83 | _, err = Clone(url, path, &CloneOptions{}) 84 | if err != nil { 85 | t.Fatal("cannot clone remote repo via https, error: ", err) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /revert_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const ( 8 | expectedRevertedReadmeContents = "foo\n" 9 | ) 10 | 11 | func TestRevert(t *testing.T) { 12 | t.Parallel() 13 | repo := createTestRepo(t) 14 | defer cleanupTestRepo(t, repo) 15 | 16 | seedTestRepo(t, repo) 17 | commitID, _ := updateReadme(t, repo, content) 18 | 19 | commit, err := repo.LookupCommit(commitID) 20 | checkFatal(t, err) 21 | 22 | revertOptions, err := DefaultRevertOptions() 23 | checkFatal(t, err) 24 | 25 | err = repo.Revert(commit, &revertOptions) 26 | checkFatal(t, err) 27 | 28 | actualReadmeContents := readReadme(t, repo) 29 | 30 | if actualReadmeContents != expectedRevertedReadmeContents { 31 | t.Fatalf(`README has incorrect contents after revert. Expected: "%v", Actual: "%v"`, 32 | expectedRevertedReadmeContents, actualReadmeContents) 33 | } 34 | 35 | state := repo.State() 36 | if state != RepositoryStateRevert { 37 | t.Fatalf("Incorrect repository state. Expected: %v, Actual: %v", RepositoryStateRevert, state) 38 | } 39 | 40 | err = repo.StateCleanup() 41 | checkFatal(t, err) 42 | 43 | state = repo.State() 44 | if state != RepositoryStateNone { 45 | t.Fatalf("Incorrect repository state. Expected: %v, Actual: %v", RepositoryStateNone, state) 46 | } 47 | } 48 | 49 | func TestRevertCommit(t *testing.T) { 50 | t.Parallel() 51 | repo := createTestRepo(t) 52 | defer cleanupTestRepo(t, repo) 53 | 54 | seedTestRepo(t, repo) 55 | commitID, _ := updateReadme(t, repo, content) 56 | 57 | commit, err := repo.LookupCommit(commitID) 58 | checkFatal(t, err) 59 | 60 | revertOptions, err := DefaultRevertOptions() 61 | checkFatal(t, err) 62 | 63 | index, err := repo.RevertCommit(commit, commit, 0, &revertOptions.MergeOptions) 64 | checkFatal(t, err) 65 | defer index.Free() 66 | 67 | err = repo.CheckoutIndex(index, &revertOptions.CheckoutOptions) 68 | checkFatal(t, err) 69 | 70 | actualReadmeContents := readReadme(t, repo) 71 | 72 | if actualReadmeContents != expectedRevertedReadmeContents { 73 | t.Fatalf(`README has incorrect contents after revert. Expected: "%v", Actual: "%v"`, 74 | expectedRevertedReadmeContents, actualReadmeContents) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /settings_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type pathPair struct { 8 | Level ConfigLevel 9 | Path string 10 | } 11 | 12 | func TestSearchPath(t *testing.T) { 13 | paths := []pathPair{ 14 | pathPair{ConfigLevelSystem, "/tmp/system"}, 15 | pathPair{ConfigLevelGlobal, "/tmp/global"}, 16 | pathPair{ConfigLevelXDG, "/tmp/xdg"}, 17 | } 18 | 19 | for _, pair := range paths { 20 | err := SetSearchPath(pair.Level, pair.Path) 21 | checkFatal(t, err) 22 | 23 | actual, err := SearchPath(pair.Level) 24 | checkFatal(t, err) 25 | 26 | if pair.Path != actual { 27 | t.Fatal("Search paths don't match") 28 | } 29 | } 30 | } 31 | 32 | func TestMmapSizes(t *testing.T) { 33 | size := 42 * 1024 34 | 35 | err := SetMwindowSize(size) 36 | checkFatal(t, err) 37 | 38 | actual, err := MwindowSize() 39 | if size != actual { 40 | t.Fatal("Sizes don't match") 41 | } 42 | 43 | err = SetMwindowMappedLimit(size) 44 | checkFatal(t, err) 45 | 46 | actual, err = MwindowMappedLimit() 47 | if size != actual { 48 | t.Fatal("Sizes don't match") 49 | } 50 | } 51 | 52 | func TestEnableCaching(t *testing.T) { 53 | err := EnableCaching(false) 54 | checkFatal(t, err) 55 | 56 | err = EnableCaching(true) 57 | checkFatal(t, err) 58 | } 59 | 60 | func TestEnableStrictHashVerification(t *testing.T) { 61 | err := EnableStrictHashVerification(false) 62 | checkFatal(t, err) 63 | 64 | err = EnableStrictHashVerification(true) 65 | checkFatal(t, err) 66 | } 67 | 68 | func TestEnableFsyncGitDir(t *testing.T) { 69 | err := EnableFsyncGitDir(false) 70 | checkFatal(t, err) 71 | 72 | err = EnableFsyncGitDir(true) 73 | checkFatal(t, err) 74 | } 75 | 76 | func TestCachedMemory(t *testing.T) { 77 | current, allowed, err := CachedMemory() 78 | checkFatal(t, err) 79 | 80 | if current < 0 { 81 | t.Fatal("current < 0") 82 | } 83 | 84 | if allowed < 0 { 85 | t.Fatal("allowed < 0") 86 | } 87 | } 88 | 89 | func TestSetCacheMaxSize(t *testing.T) { 90 | err := SetCacheMaxSize(0) 91 | checkFatal(t, err) 92 | 93 | err = SetCacheMaxSize(1024 * 1024) 94 | checkFatal(t, err) 95 | 96 | // revert to default 256MB 97 | err = SetCacheMaxSize(256 * 1024 * 1024) 98 | checkFatal(t, err) 99 | } 100 | -------------------------------------------------------------------------------- /graph_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestReachableFromAny(t *testing.T) { 8 | repo, err := OpenRepository("testdata/TestGitRepository.git") 9 | checkFatal(t, err) 10 | defer repo.Free() 11 | 12 | for name, tc := range map[string]struct { 13 | reachable bool 14 | commit string 15 | descendants []string 16 | }{ 17 | "empty": { 18 | reachable: false, 19 | commit: "49322bb17d3acc9146f98c97d078513228bbf3c0", 20 | }, 21 | "same": { 22 | reachable: true, 23 | commit: "49322bb17d3acc9146f98c97d078513228bbf3c0", 24 | descendants: []string{"49322bb17d3acc9146f98c97d078513228bbf3c0"}, 25 | }, 26 | "unreachable": { 27 | reachable: false, 28 | commit: "ac7e7e44c1885efb472ad54a78327d66bfc4ecef", 29 | descendants: []string{"58be4659bb571194ed4562d04b359d26216f526e"}, 30 | }, 31 | "unreachable-reverse": { 32 | reachable: false, 33 | commit: "58be4659bb571194ed4562d04b359d26216f526e", 34 | descendants: []string{"ac7e7e44c1885efb472ad54a78327d66bfc4ecef"}, 35 | }, 36 | "root": { 37 | reachable: false, 38 | commit: "42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", 39 | descendants: []string{ 40 | "ac7e7e44c1885efb472ad54a78327d66bfc4ecef", 41 | "d86a2aada2f5e7ccf6f11880bfb9ab404e8a8864", 42 | "f73b95671f326616d66b2afb3bdfcdbbce110b44", 43 | "d0114ab8ac326bab30e3a657a0397578c5a1af88", 44 | }, 45 | }, 46 | "head": { 47 | reachable: false, 48 | commit: "49322bb17d3acc9146f98c97d078513228bbf3c0", 49 | descendants: []string{ 50 | "ac7e7e44c1885efb472ad54a78327d66bfc4ecef", 51 | "d86a2aada2f5e7ccf6f11880bfb9ab404e8a8864", 52 | "f73b95671f326616d66b2afb3bdfcdbbce110b44", 53 | "d0114ab8ac326bab30e3a657a0397578c5a1af88", 54 | }, 55 | }, 56 | } { 57 | tc := tc 58 | t.Run(name, func(t *testing.T) { 59 | commit, err := NewOid(tc.commit) 60 | checkFatal(t, err) 61 | 62 | descendants := make([]*Oid, len(tc.descendants)) 63 | for i, o := range tc.descendants { 64 | descendants[i], err = NewOid(o) 65 | checkFatal(t, err) 66 | } 67 | reachable, err := repo.ReachableFromAny(commit, descendants) 68 | checkFatal(t, err) 69 | 70 | if reachable != tc.reachable { 71 | t.Errorf("ReachableFromAny(%s, %v) = %v, wanted %v", tc.commit, tc.descendants, reachable, tc.reachable) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /patch.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "runtime" 9 | "unsafe" 10 | ) 11 | 12 | type Patch struct { 13 | doNotCompare 14 | ptr *C.git_patch 15 | } 16 | 17 | func newPatchFromC(ptr *C.git_patch) *Patch { 18 | if ptr == nil { 19 | return nil 20 | } 21 | 22 | patch := &Patch{ 23 | ptr: ptr, 24 | } 25 | 26 | runtime.SetFinalizer(patch, (*Patch).Free) 27 | return patch 28 | } 29 | 30 | func (patch *Patch) Free() error { 31 | if patch.ptr == nil { 32 | return ErrInvalid 33 | } 34 | runtime.SetFinalizer(patch, nil) 35 | C.git_patch_free(patch.ptr) 36 | patch.ptr = nil 37 | return nil 38 | } 39 | 40 | func (patch *Patch) String() (string, error) { 41 | if patch.ptr == nil { 42 | return "", ErrInvalid 43 | } 44 | 45 | runtime.LockOSThread() 46 | defer runtime.UnlockOSThread() 47 | 48 | var buf C.git_buf 49 | 50 | ecode := C.git_patch_to_buf(&buf, patch.ptr) 51 | runtime.KeepAlive(patch) 52 | if ecode < 0 { 53 | return "", MakeGitError(ecode) 54 | } 55 | defer C.git_buf_dispose(&buf) 56 | 57 | return C.GoString(buf.ptr), nil 58 | } 59 | 60 | func toPointer(data []byte) (ptr unsafe.Pointer) { 61 | if len(data) > 0 { 62 | ptr = unsafe.Pointer(&data[0]) 63 | } else { 64 | ptr = unsafe.Pointer(nil) 65 | } 66 | return 67 | } 68 | 69 | func (v *Repository) PatchFromBuffers(oldPath, newPath string, oldBuf, newBuf []byte, opts *DiffOptions) (*Patch, error) { 70 | var patchPtr *C.git_patch 71 | 72 | oldPtr := toPointer(oldBuf) 73 | newPtr := toPointer(newBuf) 74 | 75 | cOldPath := C.CString(oldPath) 76 | defer C.free(unsafe.Pointer(cOldPath)) 77 | 78 | cNewPath := C.CString(newPath) 79 | defer C.free(unsafe.Pointer(cNewPath)) 80 | 81 | var err error 82 | copts := populateDiffOptions(&C.git_diff_options{}, opts, v, &err) 83 | defer freeDiffOptions(copts) 84 | 85 | runtime.LockOSThread() 86 | defer runtime.UnlockOSThread() 87 | 88 | ret := C.git_patch_from_buffers(&patchPtr, oldPtr, C.size_t(len(oldBuf)), cOldPath, newPtr, C.size_t(len(newBuf)), cNewPath, copts) 89 | runtime.KeepAlive(oldBuf) 90 | runtime.KeepAlive(newBuf) 91 | if ret == C.int(ErrorCodeUser) && err != nil { 92 | return nil, err 93 | } 94 | if ret < 0 { 95 | return nil, MakeGitError(ret) 96 | } 97 | 98 | return newPatchFromC(patchPtr), nil 99 | } 100 | -------------------------------------------------------------------------------- /script/build-libgit2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Since CMake cannot build the static and dynamic libraries in the same 4 | # directory, this script helps build both static and dynamic versions of it and 5 | # have the common flags in one place instead of split between two places. 6 | 7 | set -e 8 | 9 | usage() { 10 | echo "Usage: $0 <--dynamic|--static> [--system]">&2 11 | exit 1 12 | } 13 | 14 | if [ "$#" -eq "0" ]; then 15 | usage 16 | fi 17 | 18 | ROOT=${ROOT-"$(cd "$(dirname "$0")/.." && echo "${PWD}")"} 19 | VENDORED_PATH=${VENDORED_PATH-"${ROOT}/vendor/libgit2"} 20 | BUILD_SYSTEM=OFF 21 | 22 | while [ $# -gt 0 ]; do 23 | case "$1" in 24 | --static) 25 | BUILD_PATH="${ROOT}/static-build" 26 | BUILD_SHARED_LIBS=OFF 27 | ;; 28 | 29 | --dynamic) 30 | BUILD_PATH="${ROOT}/dynamic-build" 31 | BUILD_SHARED_LIBS=ON 32 | ;; 33 | 34 | --system) 35 | BUILD_SYSTEM=ON 36 | ;; 37 | 38 | *) 39 | usage 40 | ;; 41 | esac 42 | shift 43 | done 44 | 45 | if [ -z "${BUILD_SHARED_LIBS}" ]; then 46 | usage 47 | fi 48 | 49 | if [ -n "${BUILD_LIBGIT_REF}" ]; then 50 | git -C "${VENDORED_PATH}" checkout "${BUILD_LIBGIT_REF}" 51 | trap "git submodule update --init" EXIT 52 | fi 53 | 54 | BUILD_DEPRECATED_HARD="ON" 55 | if [ "${BUILD_SYSTEM}" = "ON" ]; then 56 | BUILD_INSTALL_PREFIX=${SYSTEM_INSTALL_PREFIX-"/usr"} 57 | # Most system-wide installations won't intentionally omit deprecated symbols. 58 | BUILD_DEPRECATED_HARD="OFF" 59 | else 60 | BUILD_INSTALL_PREFIX="${BUILD_PATH}/install" 61 | mkdir -p "${BUILD_PATH}/install/lib" 62 | fi 63 | 64 | USE_BUNDLED_ZLIB="ON" 65 | if [ "${USE_CHROMIUM_ZLIB}" = "ON" ]; then 66 | USE_BUNDLED_ZLIB="Chromium" 67 | fi 68 | 69 | mkdir -p "${BUILD_PATH}/build" && 70 | cd "${BUILD_PATH}/build" && 71 | cmake -DTHREADSAFE=ON \ 72 | -DBUILD_CLAR=OFF \ 73 | -DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \ 74 | -DREGEX_BACKEND=builtin \ 75 | -DUSE_BUNDLED_ZLIB="${USE_BUNDLED_ZLIB}" \ 76 | -DUSE_HTTPS=OFF \ 77 | -DUSE_SSH=OFF \ 78 | -DCMAKE_C_FLAGS=-fPIC \ 79 | -DCMAKE_BUILD_TYPE="RelWithDebInfo" \ 80 | -DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \ 81 | -DCMAKE_INSTALL_LIBDIR="lib" \ 82 | -DDEPRECATE_HARD="${BUILD_DEPRECATE_HARD}" \ 83 | "${VENDORED_PATH}" 84 | 85 | if which make nproc >/dev/null && [ -f Makefile ]; then 86 | # Make the build parallel if make is available and cmake used Makefiles. 87 | exec make "-j$(nproc --all)" install 88 | else 89 | exec cmake --build . --target install 90 | fi 91 | -------------------------------------------------------------------------------- /errorcode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ErrorCode -trimprefix ErrorCode -tags static"; DO NOT EDIT. 2 | 3 | package git 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ErrorCodeOK-0] 12 | _ = x[ErrorCodeGeneric - -1] 13 | _ = x[ErrorCodeNotFound - -3] 14 | _ = x[ErrorCodeExists - -4] 15 | _ = x[ErrorCodeAmbiguous - -5] 16 | _ = x[ErrorCodeBuffs - -6] 17 | _ = x[ErrorCodeUser - -7] 18 | _ = x[ErrorCodeBareRepo - -8] 19 | _ = x[ErrorCodeUnbornBranch - -9] 20 | _ = x[ErrorCodeUnmerged - -10] 21 | _ = x[ErrorCodeNonFastForward - -11] 22 | _ = x[ErrorCodeInvalidSpec - -12] 23 | _ = x[ErrorCodeConflict - -13] 24 | _ = x[ErrorCodeLocked - -14] 25 | _ = x[ErrorCodeModified - -15] 26 | _ = x[ErrorCodeAuth - -16] 27 | _ = x[ErrorCodeCertificate - -17] 28 | _ = x[ErrorCodeApplied - -18] 29 | _ = x[ErrorCodePeel - -19] 30 | _ = x[ErrorCodeEOF - -20] 31 | _ = x[ErrorCodeInvalid - -21] 32 | _ = x[ErrorCodeUncommitted - -22] 33 | _ = x[ErrorCodeDirectory - -23] 34 | _ = x[ErrorCodeMergeConflict - -24] 35 | _ = x[ErrorCodePassthrough - -30] 36 | _ = x[ErrorCodeIterOver - -31] 37 | _ = x[ErrorCodeRetry - -32] 38 | _ = x[ErrorCodeMismatch - -33] 39 | _ = x[ErrorCodeIndexDirty - -34] 40 | _ = x[ErrorCodeApplyFail - -35] 41 | } 42 | 43 | const ( 44 | _ErrorCode_name_0 = "ApplyFailIndexDirtyMismatchRetryIterOverPassthrough" 45 | _ErrorCode_name_1 = "MergeConflictDirectoryUncommittedInvalidEOFPeelAppliedCertificateAuthModifiedLockedConflictInvalidSpecNonFastForwardUnmergedUnbornBranchBareRepoUserBuffsAmbiguousExistsNotFound" 46 | _ErrorCode_name_2 = "GenericOK" 47 | ) 48 | 49 | var ( 50 | _ErrorCode_index_0 = [...]uint8{0, 9, 19, 27, 32, 40, 51} 51 | _ErrorCode_index_1 = [...]uint8{0, 13, 22, 33, 40, 43, 47, 54, 65, 69, 77, 83, 91, 102, 116, 124, 136, 144, 148, 153, 162, 168, 176} 52 | _ErrorCode_index_2 = [...]uint8{0, 7, 9} 53 | ) 54 | 55 | func (i ErrorCode) String() string { 56 | switch { 57 | case -35 <= i && i <= -30: 58 | i -= -35 59 | return _ErrorCode_name_0[_ErrorCode_index_0[i]:_ErrorCode_index_0[i+1]] 60 | case -24 <= i && i <= -3: 61 | i -= -24 62 | return _ErrorCode_name_1[_ErrorCode_index_1[i]:_ErrorCode_index_1[i+1]] 63 | case -1 <= i && i <= 0: 64 | i -= -1 65 | return _ErrorCode_name_2[_ErrorCode_index_2[i]:_ErrorCode_index_2[i+1]] 66 | default: 67 | return "ErrorCode(" + strconv.FormatInt(int64(i), 10) + ")" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /status_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "io/ioutil" 5 | "path" 6 | "testing" 7 | ) 8 | 9 | func TestStatusFile(t *testing.T) { 10 | t.Parallel() 11 | repo := createTestRepo(t) 12 | defer cleanupTestRepo(t, repo) 13 | 14 | state := repo.State() 15 | if state != RepositoryStateNone { 16 | t.Fatal("Incorrect repository state: ", state) 17 | } 18 | 19 | err := ioutil.WriteFile(path.Join(path.Dir(repo.Workdir()), "hello.txt"), []byte("Hello, World"), 0644) 20 | checkFatal(t, err) 21 | 22 | status, err := repo.StatusFile("hello.txt") 23 | checkFatal(t, err) 24 | 25 | if status != StatusWtNew { 26 | t.Fatal("Incorrect status flags: ", status) 27 | } 28 | } 29 | 30 | func TestStatusList(t *testing.T) { 31 | t.Parallel() 32 | repo := createTestRepo(t) 33 | defer cleanupTestRepo(t, repo) 34 | 35 | // This commits the test repo README, so it doesn't show up in the status list and there's a head to compare to 36 | seedTestRepo(t, repo) 37 | 38 | err := ioutil.WriteFile(path.Join(path.Dir(repo.Workdir()), "hello.txt"), []byte("Hello, World"), 0644) 39 | checkFatal(t, err) 40 | 41 | opts := &StatusOptions{} 42 | opts.Show = StatusShowIndexAndWorkdir 43 | opts.Flags = StatusOptIncludeUntracked | StatusOptRenamesHeadToIndex | StatusOptSortCaseSensitively 44 | 45 | statusList, err := repo.StatusList(opts) 46 | checkFatal(t, err) 47 | 48 | entryCount, err := statusList.EntryCount() 49 | checkFatal(t, err) 50 | 51 | if entryCount != 1 { 52 | t.Fatal("Incorrect number of status entries: ", entryCount) 53 | } 54 | 55 | entry, err := statusList.ByIndex(0) 56 | checkFatal(t, err) 57 | if entry.Status != StatusWtNew { 58 | t.Fatal("Incorrect status flags: ", entry.Status) 59 | } 60 | if entry.IndexToWorkdir.NewFile.Path != "hello.txt" { 61 | t.Fatal("Incorrect entry path: ", entry.IndexToWorkdir.NewFile.Path) 62 | } 63 | } 64 | 65 | func TestStatusNothing(t *testing.T) { 66 | t.Parallel() 67 | repo := createTestRepo(t) 68 | defer cleanupTestRepo(t, repo) 69 | 70 | seedTestRepo(t, repo) 71 | 72 | opts := &StatusOptions{ 73 | Show: StatusShowIndexAndWorkdir, 74 | Flags: StatusOptIncludeUntracked | StatusOptRenamesHeadToIndex | StatusOptSortCaseSensitively, 75 | } 76 | 77 | statusList, err := repo.StatusList(opts) 78 | checkFatal(t, err) 79 | 80 | entryCount, err := statusList.EntryCount() 81 | checkFatal(t, err) 82 | 83 | if entryCount != 0 { 84 | t.Fatal("expected no statuses in empty repo") 85 | } 86 | 87 | _, err = statusList.ByIndex(0) 88 | if err == nil { 89 | t.Error("expected error getting status by index") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /revparse.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | 6 | extern void _go_git_revspec_free(git_revspec *revspec); 7 | */ 8 | import "C" 9 | import ( 10 | "runtime" 11 | "unsafe" 12 | ) 13 | 14 | type RevparseFlag int 15 | 16 | const ( 17 | RevparseSingle RevparseFlag = C.GIT_REVPARSE_SINGLE 18 | RevparseRange RevparseFlag = C.GIT_REVPARSE_RANGE 19 | RevparseMergeBase RevparseFlag = C.GIT_REVPARSE_MERGE_BASE 20 | ) 21 | 22 | type Revspec struct { 23 | doNotCompare 24 | to *Object 25 | from *Object 26 | flags RevparseFlag 27 | } 28 | 29 | func (rs *Revspec) To() *Object { 30 | return rs.to 31 | } 32 | 33 | func (rs *Revspec) From() *Object { 34 | return rs.from 35 | } 36 | 37 | func (rs *Revspec) Flags() RevparseFlag { 38 | return rs.flags 39 | } 40 | 41 | func newRevspecFromC(ptr *C.git_revspec, repo *Repository) *Revspec { 42 | var to *Object 43 | var from *Object 44 | 45 | if ptr.to != nil { 46 | to = allocObject(ptr.to, repo) 47 | } 48 | 49 | if ptr.from != nil { 50 | from = allocObject(ptr.from, repo) 51 | } 52 | 53 | return &Revspec{ 54 | to: to, 55 | from: from, 56 | flags: RevparseFlag(ptr.flags), 57 | } 58 | } 59 | 60 | func (r *Repository) Revparse(spec string) (*Revspec, error) { 61 | cspec := C.CString(spec) 62 | defer C.free(unsafe.Pointer(cspec)) 63 | 64 | var crevspec C.git_revspec 65 | 66 | runtime.LockOSThread() 67 | defer runtime.UnlockOSThread() 68 | 69 | ecode := C.git_revparse(&crevspec, r.ptr, cspec) 70 | if ecode != 0 { 71 | return nil, MakeGitError(ecode) 72 | } 73 | 74 | return newRevspecFromC(&crevspec, r), nil 75 | } 76 | 77 | func (v *Repository) RevparseSingle(spec string) (*Object, error) { 78 | cspec := C.CString(spec) 79 | defer C.free(unsafe.Pointer(cspec)) 80 | 81 | var ptr *C.git_object 82 | 83 | runtime.LockOSThread() 84 | defer runtime.UnlockOSThread() 85 | 86 | ecode := C.git_revparse_single(&ptr, v.ptr, cspec) 87 | if ecode < 0 { 88 | return nil, MakeGitError(ecode) 89 | } 90 | 91 | return allocObject(ptr, v), nil 92 | } 93 | 94 | func (r *Repository) RevparseExt(spec string) (*Object, *Reference, error) { 95 | cspec := C.CString(spec) 96 | defer C.free(unsafe.Pointer(cspec)) 97 | 98 | var obj *C.git_object 99 | var ref *C.git_reference 100 | 101 | runtime.LockOSThread() 102 | defer runtime.UnlockOSThread() 103 | 104 | ecode := C.git_revparse_ext(&obj, &ref, r.ptr, cspec) 105 | if ecode != 0 { 106 | return nil, nil, MakeGitError(ecode) 107 | } 108 | 109 | if ref == nil { 110 | return allocObject(obj, r), nil, nil 111 | } 112 | 113 | return allocObject(obj, r), newReferenceFromC(ref, r), nil 114 | } 115 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | var tempConfig = "./temp.gitconfig" 9 | 10 | func setupConfig() (*Config, error) { 11 | var ( 12 | c *Config 13 | err error 14 | ) 15 | 16 | c, err = OpenOndisk(tempConfig) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | err = c.SetString("foo.bar", "baz") 22 | if err != nil { 23 | return nil, err 24 | } 25 | err = c.SetBool("foo.bool", true) 26 | if err != nil { 27 | return nil, err 28 | } 29 | err = c.SetInt32("foo.int32", 32) 30 | if err != nil { 31 | return nil, err 32 | } 33 | err = c.SetInt64("foo.int64", 64) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return c, err 39 | } 40 | 41 | func cleanupConfig() { 42 | os.Remove(tempConfig) 43 | } 44 | 45 | type TestRunner func(*Config, *testing.T) 46 | 47 | var tests = []TestRunner{ 48 | // LookupString 49 | func(c *Config, t *testing.T) { 50 | val, err := c.LookupString("foo.bar") 51 | if err != nil { 52 | t.Errorf("Got LookupString error: '%v', expected none\n", err) 53 | } 54 | if val != "baz" { 55 | t.Errorf("Got '%s' from LookupString, expected 'bar'\n", val) 56 | } 57 | }, 58 | // LookupBool 59 | func(c *Config, t *testing.T) { 60 | val, err := c.LookupBool("foo.bool") 61 | if err != nil { 62 | t.Errorf("Got LookupBool error: '%v', expected none\n", err) 63 | } 64 | if !val { 65 | t.Errorf("Got %t from LookupBool, expected 'false'\n", val) 66 | } 67 | }, 68 | // LookupInt32 69 | func(c *Config, t *testing.T) { 70 | val, err := c.LookupInt32("foo.int32") 71 | if err != nil { 72 | t.Errorf("Got LookupInt32 error: '%v', expected none\n", err) 73 | } 74 | if val != 32 { 75 | t.Errorf("Got %v, expected 32\n", val) 76 | } 77 | }, 78 | // LookupInt64 79 | func(c *Config, t *testing.T) { 80 | val, err := c.LookupInt64("foo.int64") 81 | if err != nil { 82 | t.Errorf("Got LookupInt64 error: '%v', expected none\n", err) 83 | } 84 | if val != 64 { 85 | t.Errorf("Got %v, expected 64\n", val) 86 | } 87 | }, 88 | } 89 | 90 | func TestConfigLookups(t *testing.T) { 91 | t.Parallel() 92 | var ( 93 | err error 94 | c *Config 95 | ) 96 | 97 | c, err = setupConfig() 98 | defer cleanupConfig() 99 | 100 | if err != nil { 101 | t.Errorf("Setup error: '%v'. Expected none\n", err) 102 | return 103 | } 104 | defer c.Free() 105 | 106 | for _, test := range tests { 107 | test(c, t) 108 | } 109 | } 110 | 111 | func TestOpenDefault(t *testing.T) { 112 | 113 | c, err := OpenDefault() 114 | if err != nil { 115 | t.Errorf("OpenDefault error: '%v'. Expected none\n", err) 116 | return 117 | } 118 | defer c.Free() 119 | } 120 | -------------------------------------------------------------------------------- /cherrypick.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "runtime" 9 | ) 10 | 11 | type CherrypickOptions struct { 12 | Mainline uint 13 | MergeOptions MergeOptions 14 | CheckoutOptions CheckoutOptions 15 | } 16 | 17 | func cherrypickOptionsFromC(c *C.git_cherrypick_options) CherrypickOptions { 18 | opts := CherrypickOptions{ 19 | Mainline: uint(c.mainline), 20 | MergeOptions: mergeOptionsFromC(&c.merge_opts), 21 | CheckoutOptions: checkoutOptionsFromC(&c.checkout_opts), 22 | } 23 | return opts 24 | } 25 | 26 | func populateCherrypickOptions(copts *C.git_cherrypick_options, opts *CherrypickOptions, errorTarget *error) *C.git_cherrypick_options { 27 | C.git_cherrypick_options_init(copts, C.GIT_CHERRYPICK_OPTIONS_VERSION) 28 | if opts == nil { 29 | return nil 30 | } 31 | copts.mainline = C.uint(opts.Mainline) 32 | populateMergeOptions(&copts.merge_opts, &opts.MergeOptions) 33 | populateCheckoutOptions(&copts.checkout_opts, &opts.CheckoutOptions, errorTarget) 34 | return copts 35 | } 36 | 37 | func freeCherrypickOpts(copts *C.git_cherrypick_options) { 38 | if copts == nil { 39 | return 40 | } 41 | freeMergeOptions(&copts.merge_opts) 42 | freeCheckoutOptions(&copts.checkout_opts) 43 | } 44 | 45 | func DefaultCherrypickOptions() (CherrypickOptions, error) { 46 | c := C.git_cherrypick_options{} 47 | 48 | runtime.LockOSThread() 49 | defer runtime.UnlockOSThread() 50 | 51 | ecode := C.git_cherrypick_options_init(&c, C.GIT_CHERRYPICK_OPTIONS_VERSION) 52 | if ecode < 0 { 53 | return CherrypickOptions{}, MakeGitError(ecode) 54 | } 55 | defer freeCherrypickOpts(&c) 56 | return cherrypickOptionsFromC(&c), nil 57 | } 58 | 59 | func (v *Repository) Cherrypick(commit *Commit, opts CherrypickOptions) error { 60 | runtime.LockOSThread() 61 | defer runtime.UnlockOSThread() 62 | 63 | var err error 64 | cOpts := populateCherrypickOptions(&C.git_cherrypick_options{}, &opts, &err) 65 | defer freeCherrypickOpts(cOpts) 66 | 67 | ret := C.git_cherrypick(v.ptr, commit.cast_ptr, cOpts) 68 | runtime.KeepAlive(v) 69 | runtime.KeepAlive(commit) 70 | if ret == C.int(ErrorCodeUser) && err != nil { 71 | return err 72 | } 73 | if ret < 0 { 74 | return MakeGitError(ret) 75 | } 76 | return nil 77 | } 78 | 79 | func (r *Repository) CherrypickCommit(pick, our *Commit, opts CherrypickOptions) (*Index, error) { 80 | runtime.LockOSThread() 81 | defer runtime.UnlockOSThread() 82 | 83 | cOpts := populateMergeOptions(&C.git_merge_options{}, &opts.MergeOptions) 84 | defer freeMergeOptions(cOpts) 85 | 86 | var ptr *C.git_index 87 | ret := C.git_cherrypick_commit(&ptr, r.ptr, pick.cast_ptr, our.cast_ptr, C.uint(opts.Mainline), cOpts) 88 | runtime.KeepAlive(pick) 89 | runtime.KeepAlive(our) 90 | if ret < 0 { 91 | return nil, MakeGitError(ret) 92 | } 93 | return newIndexFromC(ptr, r), nil 94 | } 95 | -------------------------------------------------------------------------------- /mempack.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | #include 6 | 7 | extern int git_mempack_new(git_odb_backend **out); 8 | extern int git_mempack_dump(git_buf *pack, git_repository *repo, git_odb_backend *backend); 9 | extern int git_mempack_reset(git_odb_backend *backend); 10 | extern void _go_git_odb_backend_free(git_odb_backend *backend); 11 | */ 12 | import "C" 13 | 14 | import ( 15 | "runtime" 16 | "unsafe" 17 | ) 18 | 19 | // Mempack is a custom ODB backend that permits packing object in-memory. 20 | type Mempack struct { 21 | doNotCompare 22 | ptr *C.git_odb_backend 23 | } 24 | 25 | // NewMempack creates a new mempack instance and registers it to the ODB. 26 | func NewMempack(odb *Odb) (mempack *Mempack, err error) { 27 | mempack = new(Mempack) 28 | 29 | runtime.LockOSThread() 30 | defer runtime.UnlockOSThread() 31 | 32 | ret := C.git_mempack_new(&mempack.ptr) 33 | if ret < 0 { 34 | return nil, MakeGitError(ret) 35 | } 36 | 37 | ret = C.git_odb_add_backend(odb.ptr, mempack.ptr, C.int(999)) 38 | runtime.KeepAlive(odb) 39 | if ret < 0 { 40 | // Since git_odb_add_alternate() takes ownership of the ODB backend, the 41 | // only case in which we free the mempack's memory is if it fails to be 42 | // added to the ODB. 43 | C._go_git_odb_backend_free(mempack.ptr) 44 | return nil, MakeGitError(ret) 45 | } 46 | 47 | return mempack, nil 48 | } 49 | 50 | // Dump dumps all the queued in-memory writes to a packfile. 51 | // 52 | // It is the caller's responsibility to ensure that the generated packfile is 53 | // available to the repository (e.g. by writing it to disk, or doing something 54 | // crazy like distributing it across several copies of the repository over a 55 | // network). 56 | // 57 | // Once the generated packfile is available to the repository, call 58 | // Mempack.Reset to cleanup the memory store. 59 | // 60 | // Calling Mempack.Reset before the packfile has been written to disk will 61 | // result in an inconsistent repository (the objects in the memory store won't 62 | // be accessible). 63 | func (mempack *Mempack) Dump(repository *Repository) ([]byte, error) { 64 | buf := C.git_buf{} 65 | 66 | runtime.LockOSThread() 67 | defer runtime.UnlockOSThread() 68 | 69 | ret := C.git_mempack_dump(&buf, repository.ptr, mempack.ptr) 70 | runtime.KeepAlive(repository) 71 | if ret < 0 { 72 | return nil, MakeGitError(ret) 73 | } 74 | defer C.git_buf_dispose(&buf) 75 | 76 | return C.GoBytes(unsafe.Pointer(buf.ptr), C.int(buf.size)), nil 77 | } 78 | 79 | // Reset resets the memory packer by clearing all the queued objects. 80 | // 81 | // This assumes that Mempack.Dump has been called before to store all the 82 | // queued objects into a single packfile. 83 | func (mempack *Mempack) Reset() error { 84 | runtime.LockOSThread() 85 | defer runtime.UnlockOSThread() 86 | 87 | ret := C.git_mempack_reset(mempack.ptr) 88 | if ret < 0 { 89 | return MakeGitError(ret) 90 | } 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /indexer.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | 6 | extern const git_oid * git_indexer_hash(const git_indexer *idx); 7 | extern int git_indexer_append(git_indexer *idx, const void *data, size_t size, git_transfer_progress *stats); 8 | extern int git_indexer_commit(git_indexer *idx, git_transfer_progress *stats); 9 | extern int _go_git_indexer_new(git_indexer **out, const char *path, unsigned int mode, git_odb *odb, void *progress_cb_payload); 10 | extern void git_indexer_free(git_indexer *idx); 11 | */ 12 | import "C" 13 | import ( 14 | "reflect" 15 | "runtime" 16 | "unsafe" 17 | ) 18 | 19 | // Indexer can post-process packfiles and create an .idx file for efficient 20 | // lookup. 21 | type Indexer struct { 22 | doNotCompare 23 | ptr *C.git_indexer 24 | stats C.git_transfer_progress 25 | ccallbacks C.git_remote_callbacks 26 | } 27 | 28 | // NewIndexer creates a new indexer instance. 29 | func NewIndexer(packfilePath string, odb *Odb, callback TransferProgressCallback) (indexer *Indexer, err error) { 30 | var odbPtr *C.git_odb = nil 31 | if odb != nil { 32 | odbPtr = odb.ptr 33 | } 34 | 35 | indexer = new(Indexer) 36 | populateRemoteCallbacks(&indexer.ccallbacks, &RemoteCallbacks{TransferProgressCallback: callback}, nil) 37 | 38 | runtime.LockOSThread() 39 | defer runtime.UnlockOSThread() 40 | 41 | cstr := C.CString(packfilePath) 42 | defer C.free(unsafe.Pointer(cstr)) 43 | 44 | ret := C._go_git_indexer_new(&indexer.ptr, cstr, 0, odbPtr, indexer.ccallbacks.payload) 45 | runtime.KeepAlive(odb) 46 | if ret < 0 { 47 | untrackCallbacksPayload(&indexer.ccallbacks) 48 | return nil, MakeGitError(ret) 49 | } 50 | 51 | runtime.SetFinalizer(indexer, (*Indexer).Free) 52 | return indexer, nil 53 | } 54 | 55 | // Write adds data to the indexer. 56 | func (indexer *Indexer) Write(data []byte) (int, error) { 57 | header := (*reflect.SliceHeader)(unsafe.Pointer(&data)) 58 | ptr := unsafe.Pointer(header.Data) 59 | size := C.size_t(header.Len) 60 | 61 | runtime.LockOSThread() 62 | defer runtime.UnlockOSThread() 63 | 64 | ret := C.git_indexer_append(indexer.ptr, ptr, size, &indexer.stats) 65 | runtime.KeepAlive(indexer) 66 | if ret < 0 { 67 | return 0, MakeGitError(ret) 68 | } 69 | 70 | return len(data), nil 71 | } 72 | 73 | // Commit finalizes the pack and index. It resolves any pending deltas and 74 | // writes out the index file. 75 | // 76 | // It also returns the packfile's hash. A packfile's name is derived from the 77 | // sorted hashing of all object names. 78 | func (indexer *Indexer) Commit() (*Oid, error) { 79 | runtime.LockOSThread() 80 | defer runtime.UnlockOSThread() 81 | 82 | ret := C.git_indexer_commit(indexer.ptr, &indexer.stats) 83 | if ret < 0 { 84 | return nil, MakeGitError(ret) 85 | } 86 | 87 | id := newOidFromC(C.git_indexer_hash(indexer.ptr)) 88 | runtime.KeepAlive(indexer) 89 | return id, nil 90 | } 91 | 92 | // Free frees the indexer and its resources. 93 | func (indexer *Indexer) Free() { 94 | untrackCallbacksPayload(&indexer.ccallbacks) 95 | runtime.SetFinalizer(indexer, nil) 96 | C.git_indexer_free(indexer.ptr) 97 | } 98 | -------------------------------------------------------------------------------- /note_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestCreateNote(t *testing.T) { 11 | t.Parallel() 12 | repo := createTestRepo(t) 13 | defer cleanupTestRepo(t, repo) 14 | 15 | commitId, _ := seedTestRepo(t, repo) 16 | 17 | commit, err := repo.LookupCommit(commitId) 18 | checkFatal(t, err) 19 | 20 | note, noteId := createTestNote(t, repo, commit) 21 | 22 | compareStrings(t, "I am a note\n", note.Message()) 23 | compareStrings(t, noteId.String(), note.Id().String()) 24 | compareStrings(t, "alice", note.Author().Name) 25 | compareStrings(t, "alice@example.com", note.Author().Email) 26 | compareStrings(t, "alice", note.Committer().Name) 27 | compareStrings(t, "alice@example.com", note.Committer().Email) 28 | } 29 | 30 | func TestNoteIterator(t *testing.T) { 31 | t.Parallel() 32 | repo := createTestRepo(t) 33 | defer cleanupTestRepo(t, repo) 34 | 35 | seedTestRepo(t, repo) 36 | 37 | notes := make([]*Note, 5) 38 | for i := range notes { 39 | commitId, _ := updateReadme(t, repo, fmt.Sprintf("README v%d\n", i+1)) 40 | commit, err := repo.LookupCommit(commitId) 41 | checkFatal(t, err) 42 | 43 | note, _ := createTestNote(t, repo, commit) 44 | notes[i] = note 45 | } 46 | 47 | iter, err := repo.NewNoteIterator("") 48 | checkFatal(t, err) 49 | for { 50 | noteId, commitId, err := iter.Next() 51 | if err != nil { 52 | if !IsErrorCode(err, ErrorCodeIterOver) { 53 | checkFatal(t, err) 54 | } 55 | break 56 | } 57 | 58 | note, err := repo.Notes.Read("", commitId) 59 | checkFatal(t, err) 60 | 61 | if !reflect.DeepEqual(note.Id(), noteId) { 62 | t.Errorf("expected note oid '%v', actual '%v'", note.Id(), noteId) 63 | } 64 | } 65 | } 66 | 67 | func TestRemoveNote(t *testing.T) { 68 | t.Parallel() 69 | repo := createTestRepo(t) 70 | defer cleanupTestRepo(t, repo) 71 | 72 | commitId, _ := seedTestRepo(t, repo) 73 | 74 | commit, err := repo.LookupCommit(commitId) 75 | checkFatal(t, err) 76 | 77 | note, _ := createTestNote(t, repo, commit) 78 | 79 | _, err = repo.Notes.Read("", commit.Id()) 80 | checkFatal(t, err) 81 | 82 | err = repo.Notes.Remove("", note.Author(), note.Committer(), commitId) 83 | checkFatal(t, err) 84 | 85 | _, err = repo.Notes.Read("", commit.Id()) 86 | if err == nil { 87 | t.Fatal("note remove failed") 88 | } 89 | } 90 | 91 | func TestDefaultNoteRef(t *testing.T) { 92 | t.Parallel() 93 | repo := createTestRepo(t) 94 | defer cleanupTestRepo(t, repo) 95 | 96 | ref, err := repo.Notes.DefaultRef() 97 | checkFatal(t, err) 98 | 99 | compareStrings(t, "refs/notes/commits", ref) 100 | } 101 | 102 | func createTestNote(t *testing.T, repo *Repository, commit *Commit) (*Note, *Oid) { 103 | loc, err := time.LoadLocation("Europe/Berlin") 104 | sig := &Signature{ 105 | Name: "alice", 106 | Email: "alice@example.com", 107 | When: time.Date(2015, 01, 05, 13, 0, 0, 0, loc), 108 | } 109 | 110 | noteId, err := repo.Notes.Create("", sig, sig, commit.Id(), "I am a note\n", false) 111 | checkFatal(t, err) 112 | 113 | note, err := repo.Notes.Read("", commit.Id()) 114 | checkFatal(t, err) 115 | 116 | return note, noteId 117 | } 118 | -------------------------------------------------------------------------------- /describe_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "path" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestDescribeCommit(t *testing.T) { 11 | t.Parallel() 12 | repo := createTestRepo(t) 13 | defer cleanupTestRepo(t, repo) 14 | 15 | describeOpts, err := DefaultDescribeOptions() 16 | checkFatal(t, err) 17 | 18 | formatOpts, err := DefaultDescribeFormatOptions() 19 | checkFatal(t, err) 20 | 21 | commitID, _ := seedTestRepo(t, repo) 22 | 23 | commit, err := repo.LookupCommit(commitID) 24 | checkFatal(t, err) 25 | 26 | // No annotated tags can be used to describe master 27 | _, err = commit.Describe(&describeOpts) 28 | checkDescribeNoRefsFound(t, err) 29 | 30 | // Fallback 31 | fallback := describeOpts 32 | fallback.ShowCommitOidAsFallback = true 33 | result, err := commit.Describe(&fallback) 34 | checkFatal(t, err) 35 | resultStr, err := result.Format(&formatOpts) 36 | checkFatal(t, err) 37 | compareStrings(t, "473bf77", resultStr) 38 | 39 | // Abbreviated 40 | abbreviated := formatOpts 41 | abbreviated.AbbreviatedSize = 2 42 | result, err = commit.Describe(&fallback) 43 | checkFatal(t, err) 44 | resultStr, err = result.Format(&abbreviated) 45 | checkFatal(t, err) 46 | compareStrings(t, "473b", resultStr) 47 | 48 | createTestTag(t, repo, commit) 49 | 50 | // Exact tag 51 | patternOpts := describeOpts 52 | patternOpts.Pattern = "v[0-9]*" 53 | result, err = commit.Describe(&patternOpts) 54 | checkFatal(t, err) 55 | resultStr, err = result.Format(&formatOpts) 56 | checkFatal(t, err) 57 | compareStrings(t, "v0.0.0", resultStr) 58 | 59 | // Pattern no match 60 | patternOpts.Pattern = "v[1-9]*" 61 | result, err = commit.Describe(&patternOpts) 62 | checkDescribeNoRefsFound(t, err) 63 | 64 | commitID, _ = updateReadme(t, repo, "update1") 65 | commit, err = repo.LookupCommit(commitID) 66 | checkFatal(t, err) 67 | 68 | // Tag-1 69 | result, err = commit.Describe(&describeOpts) 70 | checkFatal(t, err) 71 | resultStr, err = result.Format(&formatOpts) 72 | checkFatal(t, err) 73 | compareStrings(t, "v0.0.0-1-gd88ef8d", resultStr) 74 | 75 | // Strategy: All 76 | describeOpts.Strategy = DescribeAll 77 | result, err = commit.Describe(&describeOpts) 78 | checkFatal(t, err) 79 | resultStr, err = result.Format(&formatOpts) 80 | checkFatal(t, err) 81 | compareStrings(t, "heads/master", resultStr) 82 | 83 | repo.CreateBranch("hotfix", commit, false) 84 | 85 | // Workdir (branch) 86 | result, err = repo.DescribeWorkdir(&describeOpts) 87 | checkFatal(t, err) 88 | resultStr, err = result.Format(&formatOpts) 89 | checkFatal(t, err) 90 | compareStrings(t, "heads/hotfix", resultStr) 91 | } 92 | 93 | func checkDescribeNoRefsFound(t *testing.T, err error) { 94 | // The failure happens at wherever we were called, not here 95 | _, file, line, ok := runtime.Caller(1) 96 | expectedString := "no reference found, cannot describe anything" 97 | if !ok { 98 | t.Fatalf("Unable to get caller") 99 | } 100 | if err == nil || !strings.Contains(err.Error(), expectedString) { 101 | t.Fatalf( 102 | "%s:%v: was expecting error %v, got %v", 103 | path.Base(file), 104 | line, 105 | expectedString, 106 | err, 107 | ) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /indexer_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "testing" 9 | ) 10 | 11 | var ( 12 | // This is a packfile with three objects. The second is a delta which 13 | // depends on the third, which is also a delta. 14 | outOfOrderPack = []byte{ 15 | 0x50, 0x41, 0x43, 0x4b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 16 | 0x32, 0x78, 0x9c, 0x63, 0x67, 0x00, 0x00, 0x00, 0x10, 0x00, 0x08, 0x76, 17 | 0xe6, 0x8f, 0xe8, 0x12, 0x9b, 0x54, 0x6b, 0x10, 0x1a, 0xee, 0x95, 0x10, 18 | 0xc5, 0x32, 0x8e, 0x7f, 0x21, 0xca, 0x1d, 0x18, 0x78, 0x9c, 0x63, 0x62, 19 | 0x66, 0x4e, 0xcb, 0xcf, 0x07, 0x00, 0x02, 0xac, 0x01, 0x4d, 0x75, 0x01, 20 | 0xd7, 0x71, 0x36, 0x66, 0xf4, 0xde, 0x82, 0x27, 0x76, 0xc7, 0x62, 0x2c, 21 | 0x10, 0xf1, 0xb0, 0x7d, 0xe2, 0x80, 0xdc, 0x78, 0x9c, 0x63, 0x62, 0x62, 22 | 0x62, 0xb7, 0x03, 0x00, 0x00, 0x69, 0x00, 0x4c, 0xde, 0x7d, 0xaa, 0xe4, 23 | 0x19, 0x87, 0x58, 0x80, 0x61, 0x09, 0x9a, 0x33, 0xca, 0x7a, 0x31, 0x92, 24 | 0x6f, 0xae, 0x66, 0x75, 25 | } 26 | ) 27 | 28 | func TestIndexerOutOfOrder(t *testing.T) { 29 | t.Parallel() 30 | 31 | tmpPath, err := ioutil.TempDir("", "git2go") 32 | checkFatal(t, err) 33 | defer os.RemoveAll(tmpPath) 34 | 35 | var finalStats TransferProgress 36 | idx, err := NewIndexer(tmpPath, nil, func(stats TransferProgress) error { 37 | finalStats = stats 38 | return nil 39 | }) 40 | checkFatal(t, err) 41 | defer idx.Free() 42 | 43 | _, err = idx.Write(outOfOrderPack) 44 | checkFatal(t, err) 45 | oid, err := idx.Commit() 46 | checkFatal(t, err) 47 | 48 | // The packfile contains the hash as the last 20 bytes. 49 | expectedOid := NewOidFromBytes(outOfOrderPack[len(outOfOrderPack)-20:]) 50 | if !expectedOid.Equal(oid) { 51 | t.Errorf("mismatched packfile hash, expected %v, got %v", expectedOid, oid) 52 | } 53 | if finalStats.TotalObjects != 3 { 54 | t.Errorf("mismatched transferred objects, expected 3, got %v", finalStats.TotalObjects) 55 | } 56 | if finalStats.ReceivedObjects != 3 { 57 | t.Errorf("mismatched received objects, expected 3, got %v", finalStats.ReceivedObjects) 58 | } 59 | if finalStats.IndexedObjects != 3 { 60 | t.Errorf("mismatched indexed objects, expected 3, got %v", finalStats.IndexedObjects) 61 | } 62 | 63 | odb, err := NewOdb() 64 | checkFatal(t, err) 65 | defer odb.Free() 66 | 67 | backend, err := NewOdbBackendOnePack(path.Join(tmpPath, fmt.Sprintf("pack-%s.idx", oid.String()))) 68 | checkFatal(t, err) 69 | // Transfer the ownership of the backend to the odb, no freeing needed. 70 | err = odb.AddBackend(backend, 1) 71 | checkFatal(t, err) 72 | 73 | packfileObjects := 0 74 | err = odb.ForEach(func(id *Oid) error { 75 | packfileObjects += 1 76 | return nil 77 | }) 78 | checkFatal(t, err) 79 | if packfileObjects != 3 { 80 | t.Errorf("mismatched packfile objects, expected 3, got %v", packfileObjects) 81 | } 82 | 83 | // Inspect one of the well-known objects in the packfile. 84 | obj, err := odb.Read(NewOidFromBytes([]byte{ 85 | 0x19, 0x10, 0x28, 0x15, 0x66, 0x3d, 0x23, 0xf8, 0xb7, 0x5a, 0x47, 0xe7, 86 | 0xa0, 0x19, 0x65, 0xdc, 0xdc, 0x96, 0x46, 0x8c, 87 | })) 88 | checkFatal(t, err) 89 | defer obj.Free() 90 | if "foo" != string(obj.Data()) { 91 | t.Errorf("mismatched packfile object contents, expected foo, got %q", string(obj.Data())) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /revert.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "runtime" 9 | ) 10 | 11 | // RevertOptions contains options for performing a revert 12 | type RevertOptions struct { 13 | Mainline uint 14 | MergeOptions MergeOptions 15 | CheckoutOptions CheckoutOptions 16 | } 17 | 18 | func populateRevertOptions(copts *C.git_revert_options, opts *RevertOptions, errorTarget *error) *C.git_revert_options { 19 | C.git_revert_options_init(copts, C.GIT_REVERT_OPTIONS_VERSION) 20 | if opts == nil { 21 | return nil 22 | } 23 | copts.mainline = C.uint(opts.Mainline) 24 | populateMergeOptions(&copts.merge_opts, &opts.MergeOptions) 25 | populateCheckoutOptions(&copts.checkout_opts, &opts.CheckoutOptions, errorTarget) 26 | return copts 27 | } 28 | 29 | func revertOptionsFromC(copts *C.git_revert_options) RevertOptions { 30 | return RevertOptions{ 31 | Mainline: uint(copts.mainline), 32 | MergeOptions: mergeOptionsFromC(&copts.merge_opts), 33 | CheckoutOptions: checkoutOptionsFromC(&copts.checkout_opts), 34 | } 35 | } 36 | 37 | func freeRevertOptions(copts *C.git_revert_options) { 38 | if copts != nil { 39 | return 40 | } 41 | freeMergeOptions(&copts.merge_opts) 42 | freeCheckoutOptions(&copts.checkout_opts) 43 | } 44 | 45 | // DefaultRevertOptions initialises a RevertOptions struct with default values 46 | func DefaultRevertOptions() (RevertOptions, error) { 47 | copts := C.git_revert_options{} 48 | 49 | runtime.LockOSThread() 50 | defer runtime.UnlockOSThread() 51 | 52 | ecode := C.git_revert_options_init(&copts, C.GIT_REVERT_OPTIONS_VERSION) 53 | if ecode < 0 { 54 | return RevertOptions{}, MakeGitError(ecode) 55 | } 56 | 57 | defer freeRevertOptions(&copts) 58 | return revertOptionsFromC(&copts), nil 59 | } 60 | 61 | // Revert the provided commit leaving the index updated with the results of the revert 62 | func (r *Repository) Revert(commit *Commit, revertOptions *RevertOptions) error { 63 | runtime.LockOSThread() 64 | defer runtime.UnlockOSThread() 65 | 66 | var err error 67 | cOpts := populateRevertOptions(&C.git_revert_options{}, revertOptions, &err) 68 | defer freeRevertOptions(cOpts) 69 | 70 | ret := C.git_revert(r.ptr, commit.cast_ptr, cOpts) 71 | runtime.KeepAlive(r) 72 | runtime.KeepAlive(commit) 73 | 74 | if ret == C.int(ErrorCodeUser) && err != nil { 75 | return err 76 | } 77 | if ret < 0 { 78 | return MakeGitError(ret) 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // RevertCommit reverts the provided commit against "ourCommit" 85 | // The returned index contains the result of the revert and should be freed 86 | func (r *Repository) RevertCommit(revertCommit *Commit, ourCommit *Commit, mainline uint, mergeOptions *MergeOptions) (*Index, error) { 87 | runtime.LockOSThread() 88 | defer runtime.UnlockOSThread() 89 | 90 | cOpts := populateMergeOptions(&C.git_merge_options{}, mergeOptions) 91 | defer freeMergeOptions(cOpts) 92 | 93 | var index *C.git_index 94 | 95 | ecode := C.git_revert_commit(&index, r.ptr, revertCommit.cast_ptr, ourCommit.cast_ptr, C.uint(mainline), cOpts) 96 | runtime.KeepAlive(revertCommit) 97 | runtime.KeepAlive(ourCommit) 98 | 99 | if ecode < 0 { 100 | return nil, MakeGitError(ecode) 101 | } 102 | 103 | return newIndexFromC(index, r), nil 104 | } 105 | -------------------------------------------------------------------------------- /cherrypick_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | ) 7 | 8 | func checkout(t *testing.T, repo *Repository, commit *Commit) { 9 | tree, err := commit.Tree() 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | 14 | err = repo.CheckoutTree(tree, &CheckoutOptions{Strategy: CheckoutSafe}) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | err = repo.SetHeadDetached(commit.Id()) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | const content = "Herro, Worrd!" 26 | 27 | func readReadme(t *testing.T, repo *Repository) string { 28 | bytes, err := ioutil.ReadFile(pathInRepo(repo, "README")) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | return string(bytes) 33 | } 34 | 35 | func TestCherrypick(t *testing.T) { 36 | t.Parallel() 37 | repo := createTestRepo(t) 38 | defer cleanupTestRepo(t, repo) 39 | 40 | c1, _ := seedTestRepo(t, repo) 41 | c2, _ := updateReadme(t, repo, content) 42 | 43 | commit1, err := repo.LookupCommit(c1) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | commit2, err := repo.LookupCommit(c2) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | checkout(t, repo, commit1) 53 | 54 | if readReadme(t, repo) == content { 55 | t.Fatalf("README has wrong content after checking out initial commit") 56 | } 57 | 58 | opts, err := DefaultCherrypickOptions() 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | err = repo.Cherrypick(commit2, opts) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | if readReadme(t, repo) != content { 69 | t.Fatalf("README has wrong contents after cherry-picking") 70 | } 71 | 72 | state := repo.State() 73 | if state != RepositoryStateCherrypick { 74 | t.Fatal("Incorrect repository state: ", state) 75 | } 76 | 77 | err = repo.StateCleanup() 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | state = repo.State() 83 | if state != RepositoryStateNone { 84 | t.Fatal("Incorrect repository state: ", state) 85 | } 86 | } 87 | 88 | func TestCherrypickCommit(t *testing.T) { 89 | t.Parallel() 90 | repo := createTestRepo(t) 91 | defer cleanupTestRepo(t, repo) 92 | 93 | c1, _ := seedTestRepo(t, repo) 94 | c2, _ := updateReadme(t, repo, content) 95 | 96 | commit1, err := repo.LookupCommit(c1) 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | commit2, err := repo.LookupCommit(c2) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | 105 | checkout(t, repo, commit1) 106 | 107 | if got := readReadme(t, repo); got == content { 108 | t.Fatalf("README = %q, want %q", got, content) 109 | } 110 | 111 | opts, err := DefaultCherrypickOptions() 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | 116 | idx, err := repo.CherrypickCommit(commit2, commit1, opts) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | defer idx.Free() 121 | 122 | // The file is only updated in the index, not in the working directory. 123 | if got := readReadme(t, repo); got == content { 124 | t.Errorf("README = %q, want %q", got, content) 125 | } 126 | if got := repo.State(); got != RepositoryStateNone { 127 | t.Errorf("repo.State() = %v, want %v", got, RepositoryStateCherrypick) 128 | } 129 | 130 | if got := idx.EntryCount(); got != 1 { 131 | t.Fatalf("idx.EntryCount() = %v, want %v", got, 1) 132 | } 133 | entry, err := idx.EntryByIndex(0) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | if entry.Path != "README" { 138 | t.Errorf("entry.Path = %v, want %v", entry.Path, "README") 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestTreeEntryById(t *testing.T) { 9 | t.Parallel() 10 | repo := createTestRepo(t) 11 | defer cleanupTestRepo(t, repo) 12 | 13 | _, treeID := seedTestRepo(t, repo) 14 | 15 | tree, err := repo.LookupTree(treeID) 16 | checkFatal(t, err) 17 | 18 | id, err := NewOid("257cc5642cb1a054f08cc83f2d943e56fd3ebe99") 19 | checkFatal(t, err) 20 | 21 | entry := tree.EntryById(id) 22 | 23 | if entry == nil { 24 | t.Fatalf("entry id %v was not found", id) 25 | } 26 | } 27 | 28 | func TestTreeBuilderInsert(t *testing.T) { 29 | t.Parallel() 30 | repo := createTestRepo(t) 31 | defer cleanupTestRepo(t, repo) 32 | 33 | subTree, err := repo.TreeBuilder() 34 | if err != nil { 35 | t.Fatalf("TreeBuilder: %v", err) 36 | } 37 | defer subTree.Free() 38 | 39 | odb, err := repo.Odb() 40 | if err != nil { 41 | t.Fatalf("repo.Odb: %v", err) 42 | } 43 | blobId, err := odb.Write([]byte("hello"), ObjectBlob) 44 | if err != nil { 45 | t.Fatalf("odb.Write: %v", err) 46 | } 47 | if err = subTree.Insert("subfile", blobId, FilemodeBlobExecutable); err != nil { 48 | t.Fatalf("TreeBuilder.Insert: %v", err) 49 | } 50 | treeID, err := subTree.Write() 51 | if err != nil { 52 | t.Fatalf("TreeBuilder.Write: %v", err) 53 | } 54 | 55 | tree, err := repo.LookupTree(treeID) 56 | if err != nil { 57 | t.Fatalf("LookupTree: %v", err) 58 | } 59 | 60 | entry, err := tree.EntryByPath("subfile") 61 | if err != nil { 62 | t.Fatalf("tree.EntryByPath(%q): %v", "subfile", err) 63 | } 64 | 65 | if !entry.Id.Equal(blobId) { 66 | t.Fatalf("got oid %v, want %v", entry.Id, blobId) 67 | } 68 | } 69 | 70 | func TestTreeWalk(t *testing.T) { 71 | t.Parallel() 72 | repo, err := OpenRepository("testdata/TestGitRepository.git") 73 | checkFatal(t, err) 74 | treeID, err := NewOid("6020a3b8d5d636e549ccbd0c53e2764684bb3125") 75 | checkFatal(t, err) 76 | 77 | tree, err := repo.LookupTree(treeID) 78 | checkFatal(t, err) 79 | 80 | var callCount int 81 | err = tree.Walk(func(name string, entry *TreeEntry) error { 82 | callCount++ 83 | 84 | return nil 85 | }) 86 | checkFatal(t, err) 87 | if callCount != 11 { 88 | t.Fatalf("got called %v times, want %v", callCount, 11) 89 | } 90 | } 91 | 92 | func TestTreeWalkSkip(t *testing.T) { 93 | t.Parallel() 94 | repo, err := OpenRepository("testdata/TestGitRepository.git") 95 | checkFatal(t, err) 96 | treeID, err := NewOid("6020a3b8d5d636e549ccbd0c53e2764684bb3125") 97 | checkFatal(t, err) 98 | 99 | tree, err := repo.LookupTree(treeID) 100 | checkFatal(t, err) 101 | 102 | var callCount int 103 | err = tree.Walk(func(name string, entry *TreeEntry) error { 104 | callCount++ 105 | 106 | return TreeWalkSkip 107 | }) 108 | checkFatal(t, err) 109 | if callCount != 4 { 110 | t.Fatalf("got called %v times, want %v", callCount, 4) 111 | } 112 | } 113 | 114 | func TestTreeWalkStop(t *testing.T) { 115 | t.Parallel() 116 | repo, err := OpenRepository("testdata/TestGitRepository.git") 117 | checkFatal(t, err) 118 | treeID, err := NewOid("6020a3b8d5d636e549ccbd0c53e2764684bb3125") 119 | checkFatal(t, err) 120 | 121 | tree, err := repo.LookupTree(treeID) 122 | checkFatal(t, err) 123 | 124 | var callCount int 125 | stopError := errors.New("stop") 126 | err = tree.Walk(func(name string, entry *TreeEntry) error { 127 | callCount++ 128 | 129 | return stopError 130 | }) 131 | if err != stopError { 132 | t.Fatalf("got error %v, want %v", err, stopError) 133 | } 134 | if callCount != 1 { 135 | t.Fatalf("got called %v times, want %v", callCount, 1) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /clone.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | 6 | extern void _go_git_populate_clone_callbacks(git_clone_options *opts); 7 | */ 8 | import "C" 9 | import ( 10 | "runtime" 11 | "unsafe" 12 | ) 13 | 14 | type RemoteCreateCallback func(repo *Repository, name, url string) (*Remote, error) 15 | 16 | type CloneOptions struct { 17 | CheckoutOptions CheckoutOptions 18 | FetchOptions FetchOptions 19 | Bare bool 20 | CheckoutBranch string 21 | RemoteCreateCallback RemoteCreateCallback 22 | } 23 | 24 | func Clone(url string, path string, options *CloneOptions) (*Repository, error) { 25 | curl := C.CString(url) 26 | defer C.free(unsafe.Pointer(curl)) 27 | 28 | cpath := C.CString(path) 29 | defer C.free(unsafe.Pointer(cpath)) 30 | 31 | var err error 32 | cOptions := populateCloneOptions(&C.git_clone_options{}, options, &err) 33 | defer freeCloneOptions(cOptions) 34 | 35 | if len(options.CheckoutBranch) != 0 { 36 | cOptions.checkout_branch = C.CString(options.CheckoutBranch) 37 | } 38 | 39 | runtime.LockOSThread() 40 | defer runtime.UnlockOSThread() 41 | 42 | var ptr *C.git_repository 43 | ret := C.git_clone(&ptr, curl, cpath, cOptions) 44 | 45 | if ret == C.int(ErrorCodeUser) && err != nil { 46 | return nil, err 47 | } 48 | if ret < 0 { 49 | return nil, MakeGitError(ret) 50 | } 51 | 52 | return newRepositoryFromC(ptr), nil 53 | } 54 | 55 | //export remoteCreateCallback 56 | func remoteCreateCallback( 57 | out **C.git_remote, 58 | crepo *C.git_repository, 59 | cname, curl *C.char, 60 | handle unsafe.Pointer, 61 | ) C.int { 62 | name := C.GoString(cname) 63 | url := C.GoString(curl) 64 | repo := newRepositoryFromC(crepo) 65 | repo.weak = true 66 | defer repo.Free() 67 | 68 | data, ok := pointerHandles.Get(handle).(*cloneCallbackData) 69 | if !ok { 70 | panic("invalid remote create callback") 71 | } 72 | 73 | remote, err := data.options.RemoteCreateCallback(repo, name, url) 74 | 75 | if err != nil { 76 | *data.errorTarget = err 77 | return C.int(ErrorCodeUser) 78 | } 79 | if remote == nil { 80 | panic("no remote created by callback") 81 | } 82 | 83 | *out = remote.ptr 84 | 85 | // clear finalizer as the calling C function will 86 | // free the remote itself 87 | runtime.SetFinalizer(remote, nil) 88 | remote.repo.Remotes.untrackRemote(remote) 89 | 90 | return C.int(ErrorCodeOK) 91 | } 92 | 93 | type cloneCallbackData struct { 94 | options *CloneOptions 95 | errorTarget *error 96 | } 97 | 98 | func populateCloneOptions(copts *C.git_clone_options, opts *CloneOptions, errorTarget *error) *C.git_clone_options { 99 | C.git_clone_options_init(copts, C.GIT_CLONE_OPTIONS_VERSION) 100 | if opts == nil { 101 | return nil 102 | } 103 | populateCheckoutOptions(&copts.checkout_opts, &opts.CheckoutOptions, errorTarget) 104 | populateFetchOptions(&copts.fetch_opts, &opts.FetchOptions, errorTarget) 105 | copts.bare = cbool(opts.Bare) 106 | 107 | if opts.RemoteCreateCallback != nil { 108 | data := &cloneCallbackData{ 109 | options: opts, 110 | errorTarget: errorTarget, 111 | } 112 | // Go v1.1 does not allow to assign a C function pointer 113 | C._go_git_populate_clone_callbacks(copts) 114 | copts.remote_cb_payload = pointerHandles.Track(data) 115 | } 116 | 117 | return copts 118 | } 119 | 120 | func freeCloneOptions(copts *C.git_clone_options) { 121 | if copts == nil { 122 | return 123 | } 124 | 125 | freeCheckoutOptions(&copts.checkout_opts) 126 | freeFetchOptions(&copts.fetch_opts) 127 | 128 | if copts.remote_cb_payload != nil { 129 | pointerHandles.Untrack(copts.remote_cb_payload) 130 | } 131 | 132 | C.free(unsafe.Pointer(copts.checkout_branch)) 133 | } 134 | -------------------------------------------------------------------------------- /refspec.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "runtime" 9 | "unsafe" 10 | ) 11 | 12 | type Refspec struct { 13 | doNotCompare 14 | ptr *C.git_refspec 15 | } 16 | 17 | // ParseRefspec parses a given refspec string 18 | func ParseRefspec(input string, isFetch bool) (*Refspec, error) { 19 | var ptr *C.git_refspec 20 | 21 | cinput := C.CString(input) 22 | defer C.free(unsafe.Pointer(cinput)) 23 | 24 | runtime.LockOSThread() 25 | defer runtime.UnlockOSThread() 26 | 27 | ret := C.git_refspec_parse(&ptr, cinput, cbool(isFetch)) 28 | if ret < 0 { 29 | return nil, MakeGitError(ret) 30 | } 31 | 32 | spec := &Refspec{ptr: ptr} 33 | runtime.SetFinalizer(spec, (*Refspec).Free) 34 | return spec, nil 35 | } 36 | 37 | // Free releases a refspec object which has been created by ParseRefspec 38 | func (s *Refspec) Free() { 39 | runtime.SetFinalizer(s, nil) 40 | C.git_refspec_free(s.ptr) 41 | } 42 | 43 | // Direction returns the refspec's direction 44 | func (s *Refspec) Direction() ConnectDirection { 45 | direction := C.git_refspec_direction(s.ptr) 46 | return ConnectDirection(direction) 47 | } 48 | 49 | // Src returns the refspec's source specifier 50 | func (s *Refspec) Src() string { 51 | var ret string 52 | cstr := C.git_refspec_src(s.ptr) 53 | 54 | if cstr != nil { 55 | ret = C.GoString(cstr) 56 | } 57 | 58 | runtime.KeepAlive(s) 59 | return ret 60 | } 61 | 62 | // Dst returns the refspec's destination specifier 63 | func (s *Refspec) Dst() string { 64 | var ret string 65 | cstr := C.git_refspec_dst(s.ptr) 66 | 67 | if cstr != nil { 68 | ret = C.GoString(cstr) 69 | } 70 | 71 | runtime.KeepAlive(s) 72 | return ret 73 | } 74 | 75 | // Force returns the refspec's force-update setting 76 | func (s *Refspec) Force() bool { 77 | force := C.git_refspec_force(s.ptr) 78 | return force != 0 79 | } 80 | 81 | // String returns the refspec's string representation 82 | func (s *Refspec) String() string { 83 | var ret string 84 | cstr := C.git_refspec_string(s.ptr) 85 | 86 | if cstr != nil { 87 | ret = C.GoString(cstr) 88 | } 89 | 90 | runtime.KeepAlive(s) 91 | return ret 92 | } 93 | 94 | // SrcMatches checks if a refspec's source descriptor matches a reference 95 | func (s *Refspec) SrcMatches(refname string) bool { 96 | cname := C.CString(refname) 97 | defer C.free(unsafe.Pointer(cname)) 98 | 99 | matches := C.git_refspec_src_matches(s.ptr, cname) 100 | return matches != 0 101 | } 102 | 103 | // SrcMatches checks if a refspec's destination descriptor matches a reference 104 | func (s *Refspec) DstMatches(refname string) bool { 105 | cname := C.CString(refname) 106 | defer C.free(unsafe.Pointer(cname)) 107 | 108 | matches := C.git_refspec_dst_matches(s.ptr, cname) 109 | return matches != 0 110 | } 111 | 112 | // Transform a reference to its target following the refspec's rules 113 | func (s *Refspec) Transform(refname string) (string, error) { 114 | buf := C.git_buf{} 115 | 116 | cname := C.CString(refname) 117 | defer C.free(unsafe.Pointer(cname)) 118 | 119 | runtime.LockOSThread() 120 | defer runtime.UnlockOSThread() 121 | 122 | ret := C.git_refspec_transform(&buf, s.ptr, cname) 123 | if ret < 0 { 124 | return "", MakeGitError(ret) 125 | } 126 | defer C.git_buf_dispose(&buf) 127 | 128 | return C.GoString(buf.ptr), nil 129 | } 130 | 131 | // Rtransform converts a target reference to its source reference following the 132 | // refspec's rules 133 | func (s *Refspec) Rtransform(refname string) (string, error) { 134 | buf := C.git_buf{} 135 | 136 | cname := C.CString(refname) 137 | defer C.free(unsafe.Pointer(cname)) 138 | 139 | runtime.LockOSThread() 140 | defer runtime.UnlockOSThread() 141 | 142 | ret := C.git_refspec_rtransform(&buf, s.ptr, cname) 143 | if ret < 0 { 144 | return "", MakeGitError(ret) 145 | } 146 | defer C.git_buf_dispose(&buf) 147 | 148 | return C.GoString(buf.ptr), nil 149 | } 150 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: git2go CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | - release-* 8 | - v* 9 | 10 | jobs: 11 | 12 | build-static: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | go: [ '1.11', '1.12', '1.13', '1.14', '1.15', '1.16', '1.17' ] 17 | name: Go ${{ matrix.go }} 18 | 19 | runs-on: ubuntu-20.04 20 | 21 | steps: 22 | - name: Set up Go 23 | uses: actions/setup-go@v1 24 | with: 25 | go-version: ${{ matrix.go }} 26 | id: go 27 | - name: Check out code into the Go module directory 28 | uses: actions/checkout@v1 29 | - name: Build 30 | run: | 31 | git submodule update --init 32 | sudo apt-get install -y --no-install-recommends libssh2-1-dev 33 | make build-libgit2-static 34 | - name: Test 35 | run: make TEST_ARGS=-test.v test-static 36 | 37 | build-dynamic: 38 | strategy: 39 | fail-fast: false 40 | name: Go (dynamic) 41 | 42 | runs-on: ubuntu-20.04 43 | 44 | steps: 45 | - name: Set up Go 46 | uses: actions/setup-go@v1 47 | with: 48 | go-version: '1.17' 49 | id: go 50 | - name: Check out code into the Go module directory 51 | uses: actions/checkout@v1 52 | - name: Build 53 | run: | 54 | git submodule update --init 55 | sudo apt-get install -y --no-install-recommends libssh2-1-dev 56 | make build-libgit2-dynamic 57 | - name: Test 58 | run: make TEST_ARGS=-test.v test-dynamic 59 | 60 | build-system-dynamic: 61 | strategy: 62 | fail-fast: false 63 | matrix: 64 | libgit2: 65 | - 'v1.5.0' 66 | name: Go (system-wide, dynamic) 67 | 68 | runs-on: ubuntu-20.04 69 | 70 | steps: 71 | - name: Set up Go 72 | uses: actions/setup-go@v1 73 | with: 74 | go-version: '1.17' 75 | id: go 76 | - name: Check out code into the Go module directory 77 | uses: actions/checkout@v1 78 | - name: Build libgit2 ${{ matrix.libgit2 }} 79 | run: | 80 | git submodule update --init 81 | sudo apt-get install -y --no-install-recommends libssh2-1-dev 82 | sudo env BUILD_LIBGIT_REF=${{ matrix.libgit2 }} ./script/build-libgit2.sh --dynamic --system 83 | - name: Test 84 | run: make TEST_ARGS=-test.v test 85 | 86 | build-system-static: 87 | strategy: 88 | fail-fast: false 89 | name: Go (system-wide, static) 90 | 91 | runs-on: ubuntu-20.04 92 | 93 | steps: 94 | - name: Set up Go 95 | uses: actions/setup-go@v1 96 | with: 97 | go-version: '1.17' 98 | id: go 99 | - name: Check out code into the Go module directory 100 | uses: actions/checkout@v1 101 | - name: Build libgit2 102 | run: | 103 | git submodule update --init 104 | sudo apt-get install -y --no-install-recommends libssh2-1-dev 105 | sudo ./script/build-libgit2.sh --static --system 106 | - name: Test 107 | run: go test --count=1 --tags "static,system_libgit2" ./... 108 | 109 | check-generate: 110 | name: Check generated files were not modified 111 | runs-on: ubuntu-20.04 112 | steps: 113 | - name: Set up Go 114 | uses: actions/setup-go@v1 115 | with: 116 | go-version: '1.17' 117 | id: go 118 | - name: Check out code into the Go module directory 119 | uses: actions/checkout@v2 120 | - name: Install libgit2 build dependencies 121 | run: | 122 | git submodule update --init 123 | sudo apt-get install -y --no-install-recommends libssh2-1-dev 124 | go install golang.org/x/tools/cmd/stringer@latest 125 | - name: Generate files 126 | run: | 127 | export PATH=$(go env GOPATH)/bin:$PATH 128 | make generate 129 | - name: Check nothing changed 130 | run: git diff --quiet --exit-code || (echo "detected changes after generate" ; git status ; exit 1) 131 | -------------------------------------------------------------------------------- /blob.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | #include 6 | 7 | int _go_git_writestream_write(git_writestream *stream, const char *buffer, size_t len); 8 | void _go_git_writestream_free(git_writestream *stream); 9 | */ 10 | import "C" 11 | import ( 12 | "reflect" 13 | "runtime" 14 | "unsafe" 15 | ) 16 | 17 | type Blob struct { 18 | doNotCompare 19 | Object 20 | cast_ptr *C.git_blob 21 | } 22 | 23 | func (b *Blob) AsObject() *Object { 24 | return &b.Object 25 | } 26 | 27 | func (v *Blob) Size() int64 { 28 | ret := int64(C.git_blob_rawsize(v.cast_ptr)) 29 | runtime.KeepAlive(v) 30 | return ret 31 | } 32 | 33 | func (v *Blob) Contents() []byte { 34 | size := C.int(C.git_blob_rawsize(v.cast_ptr)) 35 | buffer := unsafe.Pointer(C.git_blob_rawcontent(v.cast_ptr)) 36 | 37 | goBytes := C.GoBytes(buffer, size) 38 | runtime.KeepAlive(v) 39 | 40 | return goBytes 41 | } 42 | 43 | func (v *Blob) IsBinary() bool { 44 | ret := C.git_blob_is_binary(v.cast_ptr) == 1 45 | runtime.KeepAlive(v) 46 | return ret 47 | } 48 | 49 | func (repo *Repository) CreateBlobFromBuffer(data []byte) (*Oid, error) { 50 | runtime.LockOSThread() 51 | defer runtime.UnlockOSThread() 52 | 53 | var id C.git_oid 54 | var size C.size_t 55 | 56 | // Go 1.6 added some increased checking of passing pointer to 57 | // C, but its check depends on its expectations of what we 58 | // pass to the C function, so unless we take the address of 59 | // its contents at the call site itself, it can fail when 60 | // 'data' is a slice of a slice. 61 | // 62 | // When we're given an empty slice, create a dummy one where 0 63 | // isn't out of bounds. 64 | if len(data) > 0 { 65 | size = C.size_t(len(data)) 66 | } else { 67 | data = []byte{0} 68 | size = C.size_t(0) 69 | } 70 | 71 | ecode := C.git_blob_create_from_buffer(&id, repo.ptr, unsafe.Pointer(&data[0]), size) 72 | runtime.KeepAlive(repo) 73 | if ecode < 0 { 74 | return nil, MakeGitError(ecode) 75 | } 76 | return newOidFromC(&id), nil 77 | } 78 | 79 | func (repo *Repository) CreateFromStream(hintPath string) (*BlobWriteStream, error) { 80 | var chintPath *C.char = nil 81 | var stream *C.git_writestream 82 | 83 | if len(hintPath) > 0 { 84 | chintPath = C.CString(hintPath) 85 | defer C.free(unsafe.Pointer(chintPath)) 86 | } 87 | 88 | runtime.LockOSThread() 89 | defer runtime.UnlockOSThread() 90 | 91 | ecode := C.git_blob_create_from_stream(&stream, repo.ptr, chintPath) 92 | if ecode < 0 { 93 | return nil, MakeGitError(ecode) 94 | } 95 | 96 | return newBlobWriteStreamFromC(stream, repo), nil 97 | } 98 | 99 | type BlobWriteStream struct { 100 | doNotCompare 101 | ptr *C.git_writestream 102 | repo *Repository 103 | } 104 | 105 | func newBlobWriteStreamFromC(ptr *C.git_writestream, repo *Repository) *BlobWriteStream { 106 | stream := &BlobWriteStream{ 107 | ptr: ptr, 108 | repo: repo, 109 | } 110 | 111 | runtime.SetFinalizer(stream, (*BlobWriteStream).Free) 112 | return stream 113 | } 114 | 115 | // Implement io.Writer 116 | func (stream *BlobWriteStream) Write(p []byte) (int, error) { 117 | header := (*reflect.SliceHeader)(unsafe.Pointer(&p)) 118 | ptr := (*C.char)(unsafe.Pointer(header.Data)) 119 | size := C.size_t(header.Len) 120 | 121 | runtime.LockOSThread() 122 | defer runtime.UnlockOSThread() 123 | 124 | ecode := C._go_git_writestream_write(stream.ptr, ptr, size) 125 | runtime.KeepAlive(stream) 126 | if ecode < 0 { 127 | return 0, MakeGitError(ecode) 128 | } 129 | 130 | return len(p), nil 131 | } 132 | 133 | func (stream *BlobWriteStream) Free() { 134 | runtime.SetFinalizer(stream, nil) 135 | C._go_git_writestream_free(stream.ptr) 136 | } 137 | 138 | func (stream *BlobWriteStream) Commit() (*Oid, error) { 139 | oid := C.git_oid{} 140 | 141 | runtime.LockOSThread() 142 | defer runtime.UnlockOSThread() 143 | 144 | ecode := C.git_blob_create_from_stream_commit(&oid, stream.ptr) 145 | runtime.KeepAlive(stream) 146 | if ecode < 0 { 147 | return nil, MakeGitError(ecode) 148 | } 149 | 150 | return newOidFromC(&oid), nil 151 | } 152 | -------------------------------------------------------------------------------- /settings.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | 6 | int _go_git_opts_get_search_path(int level, git_buf *buf) 7 | { 8 | return git_libgit2_opts(GIT_OPT_GET_SEARCH_PATH, level, buf); 9 | } 10 | 11 | int _go_git_opts_set_search_path(int level, const char *path) 12 | { 13 | return git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, level, path); 14 | } 15 | 16 | int _go_git_opts_set_size_t(int opt, size_t val) 17 | { 18 | return git_libgit2_opts(opt, val); 19 | } 20 | 21 | int _go_git_opts_set_cache_object_limit(git_object_t type, size_t size) 22 | { 23 | return git_libgit2_opts(GIT_OPT_SET_CACHE_OBJECT_LIMIT, type, size); 24 | } 25 | 26 | int _go_git_opts_get_size_t(int opt, size_t *val) 27 | { 28 | return git_libgit2_opts(opt, val); 29 | } 30 | 31 | int _go_git_opts_get_size_t_size_t(int opt, size_t *val1, size_t *val2) 32 | { 33 | return git_libgit2_opts(opt, val1, val2); 34 | } 35 | */ 36 | import "C" 37 | import ( 38 | "runtime" 39 | "unsafe" 40 | ) 41 | 42 | func SearchPath(level ConfigLevel) (string, error) { 43 | var buf C.git_buf 44 | defer C.git_buf_dispose(&buf) 45 | 46 | runtime.LockOSThread() 47 | defer runtime.UnlockOSThread() 48 | 49 | err := C._go_git_opts_get_search_path(C.int(level), &buf) 50 | if err < 0 { 51 | return "", MakeGitError(err) 52 | } 53 | 54 | return C.GoString(buf.ptr), nil 55 | } 56 | 57 | func SetSearchPath(level ConfigLevel, path string) error { 58 | cpath := C.CString(path) 59 | defer C.free(unsafe.Pointer(cpath)) 60 | 61 | runtime.LockOSThread() 62 | defer runtime.UnlockOSThread() 63 | 64 | err := C._go_git_opts_set_search_path(C.int(level), cpath) 65 | if err < 0 { 66 | return MakeGitError(err) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func MwindowSize() (int, error) { 73 | return getSizet(C.GIT_OPT_GET_MWINDOW_SIZE) 74 | } 75 | 76 | func SetMwindowSize(size int) error { 77 | return setSizet(C.GIT_OPT_SET_MWINDOW_SIZE, size) 78 | } 79 | 80 | func MwindowMappedLimit() (int, error) { 81 | return getSizet(C.GIT_OPT_GET_MWINDOW_MAPPED_LIMIT) 82 | } 83 | 84 | func SetMwindowMappedLimit(size int) error { 85 | return setSizet(C.GIT_OPT_SET_MWINDOW_MAPPED_LIMIT, size) 86 | } 87 | 88 | func EnableCaching(enabled bool) error { 89 | if enabled { 90 | return setSizet(C.GIT_OPT_ENABLE_CACHING, 1) 91 | } else { 92 | return setSizet(C.GIT_OPT_ENABLE_CACHING, 0) 93 | } 94 | } 95 | 96 | func EnableStrictHashVerification(enabled bool) error { 97 | if enabled { 98 | return setSizet(C.GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION, 1) 99 | } else { 100 | return setSizet(C.GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION, 0) 101 | } 102 | } 103 | 104 | func EnableFsyncGitDir(enabled bool) error { 105 | if enabled { 106 | return setSizet(C.GIT_OPT_ENABLE_FSYNC_GITDIR, 1) 107 | } else { 108 | return setSizet(C.GIT_OPT_ENABLE_FSYNC_GITDIR, 0) 109 | } 110 | } 111 | 112 | func CachedMemory() (current int, allowed int, err error) { 113 | return getSizetSizet(C.GIT_OPT_GET_CACHED_MEMORY) 114 | } 115 | 116 | // deprecated: You should use `CachedMemory()` instead. 117 | func GetCachedMemory() (current int, allowed int, err error) { 118 | return CachedMemory() 119 | } 120 | 121 | func SetCacheMaxSize(maxSize int) error { 122 | return setSizet(C.GIT_OPT_SET_CACHE_MAX_SIZE, maxSize) 123 | } 124 | 125 | func SetCacheObjectLimit(objectType ObjectType, size int) error { 126 | runtime.LockOSThread() 127 | defer runtime.UnlockOSThread() 128 | 129 | err := C._go_git_opts_set_cache_object_limit(C.git_object_t(objectType), C.size_t(size)) 130 | if err < 0 { 131 | return MakeGitError(err) 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func getSizet(opt C.int) (int, error) { 138 | runtime.LockOSThread() 139 | defer runtime.UnlockOSThread() 140 | 141 | var val C.size_t 142 | err := C._go_git_opts_get_size_t(opt, &val) 143 | if err < 0 { 144 | return 0, MakeGitError(err) 145 | } 146 | 147 | return int(val), nil 148 | } 149 | 150 | func getSizetSizet(opt C.int) (int, int, error) { 151 | runtime.LockOSThread() 152 | defer runtime.UnlockOSThread() 153 | 154 | var val1, val2 C.size_t 155 | err := C._go_git_opts_get_size_t_size_t(opt, &val1, &val2) 156 | if err < 0 { 157 | return 0, 0, MakeGitError(err) 158 | } 159 | 160 | return int(val1), int(val2), nil 161 | } 162 | 163 | func setSizet(opt C.int, val int) error { 164 | runtime.LockOSThread() 165 | defer runtime.UnlockOSThread() 166 | 167 | cval := C.size_t(val) 168 | err := C._go_git_opts_set_size_t(opt, cval) 169 | if err < 0 { 170 | return MakeGitError(err) 171 | } 172 | 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /repository_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestCreateCommitBuffer(t *testing.T) { 9 | t.Parallel() 10 | repo := createTestRepo(t) 11 | defer cleanupTestRepo(t, repo) 12 | 13 | loc, err := time.LoadLocation("Europe/Berlin") 14 | checkFatal(t, err) 15 | sig := &Signature{ 16 | Name: "Rand Om Hacker", 17 | Email: "random@hacker.com", 18 | When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), 19 | } 20 | 21 | idx, err := repo.Index() 22 | checkFatal(t, err) 23 | err = idx.AddByPath("README") 24 | checkFatal(t, err) 25 | err = idx.Write() 26 | checkFatal(t, err) 27 | treeId, err := idx.WriteTree() 28 | checkFatal(t, err) 29 | 30 | message := "This is a commit\n" 31 | tree, err := repo.LookupTree(treeId) 32 | checkFatal(t, err) 33 | 34 | for encoding, expected := range map[MessageEncoding]string{ 35 | MessageEncodingUTF8: `tree b7119b11e8ef7a1a5a34d3ac87f5b075228ac81e 36 | author Rand Om Hacker 1362576600 +0100 37 | committer Rand Om Hacker 1362576600 +0100 38 | 39 | This is a commit 40 | `, 41 | MessageEncoding("ASCII"): `tree b7119b11e8ef7a1a5a34d3ac87f5b075228ac81e 42 | author Rand Om Hacker 1362576600 +0100 43 | committer Rand Om Hacker 1362576600 +0100 44 | encoding ASCII 45 | 46 | This is a commit 47 | `, 48 | } { 49 | encoding := encoding 50 | expected := expected 51 | t.Run(string(encoding), func(t *testing.T) { 52 | buf, err := repo.CreateCommitBuffer(sig, sig, encoding, message, tree) 53 | checkFatal(t, err) 54 | 55 | if expected != string(buf) { 56 | t.Errorf("mismatched commit buffer, expected %v, got %v", expected, string(buf)) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestCreateCommitFromIds(t *testing.T) { 63 | t.Parallel() 64 | repo := createTestRepo(t) 65 | defer cleanupTestRepo(t, repo) 66 | 67 | loc, err := time.LoadLocation("Europe/Berlin") 68 | checkFatal(t, err) 69 | sig := &Signature{ 70 | Name: "Rand Om Hacker", 71 | Email: "random@hacker.com", 72 | When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), 73 | } 74 | 75 | idx, err := repo.Index() 76 | checkFatal(t, err) 77 | err = idx.AddByPath("README") 78 | checkFatal(t, err) 79 | err = idx.Write() 80 | checkFatal(t, err) 81 | treeId, err := idx.WriteTree() 82 | checkFatal(t, err) 83 | 84 | message := "This is a commit\n" 85 | tree, err := repo.LookupTree(treeId) 86 | checkFatal(t, err) 87 | expectedCommitId, err := repo.CreateCommit("HEAD", sig, sig, message, tree) 88 | checkFatal(t, err) 89 | 90 | commitId, err := repo.CreateCommitFromIds("", sig, sig, message, treeId) 91 | checkFatal(t, err) 92 | 93 | if !expectedCommitId.Equal(commitId) { 94 | t.Errorf("mismatched commit ids, expected %v, got %v", expectedCommitId.String(), commitId.String()) 95 | } 96 | } 97 | 98 | func TestRepositorySetConfig(t *testing.T) { 99 | repo := createTestRepo(t) 100 | defer cleanupTestRepo(t, repo) 101 | 102 | loc, err := time.LoadLocation("Europe/Berlin") 103 | checkFatal(t, err) 104 | sig := &Signature{ 105 | Name: "Rand Om Hacker", 106 | Email: "random@hacker.com", 107 | When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), 108 | } 109 | 110 | idx, err := repo.Index() 111 | checkFatal(t, err) 112 | err = idx.AddByPath("README") 113 | 114 | treeId, err := idx.WriteTree() 115 | checkFatal(t, err) 116 | 117 | message := "This is a commit\n" 118 | tree, err := repo.LookupTree(treeId) 119 | checkFatal(t, err) 120 | _, err = repo.CreateCommit("HEAD", sig, sig, message, tree) 121 | checkFatal(t, err) 122 | 123 | repoConfig, err := repo.Config() 124 | checkFatal(t, err) 125 | 126 | temp := Config{} 127 | localConfig, err := temp.OpenLevel(repoConfig, ConfigLevelLocal) 128 | checkFatal(t, err) 129 | repoConfig = nil 130 | 131 | err = repo.SetConfig(localConfig) 132 | checkFatal(t, err) 133 | 134 | configFieldName := "core.filemode" 135 | err = localConfig.SetBool(configFieldName, true) 136 | checkFatal(t, err) 137 | 138 | localConfig = nil 139 | 140 | repoConfig, err = repo.Config() 141 | checkFatal(t, err) 142 | 143 | result, err := repoConfig.LookupBool(configFieldName) 144 | checkFatal(t, err) 145 | if result != true { 146 | t.Fatal("result must be true") 147 | } 148 | } 149 | 150 | func TestRepositoryItemPath(t *testing.T) { 151 | repo := createTestRepo(t) 152 | defer cleanupTestRepo(t, repo) 153 | 154 | gitDir, err := repo.ItemPath(RepositoryItemGitDir) 155 | checkFatal(t, err) 156 | if gitDir == "" { 157 | t.Error("expected not empty gitDir") 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /packbuilder.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | #include 6 | #include 7 | 8 | extern int _go_git_packbuilder_foreach(git_packbuilder *pb, void *payload); 9 | */ 10 | import "C" 11 | import ( 12 | "io" 13 | "os" 14 | "runtime" 15 | "unsafe" 16 | ) 17 | 18 | type Packbuilder struct { 19 | doNotCompare 20 | ptr *C.git_packbuilder 21 | r *Repository 22 | } 23 | 24 | func (repo *Repository) NewPackbuilder() (*Packbuilder, error) { 25 | runtime.LockOSThread() 26 | defer runtime.UnlockOSThread() 27 | 28 | var ptr *C.git_packbuilder 29 | ret := C.git_packbuilder_new(&ptr, repo.ptr) 30 | if ret != 0 { 31 | return nil, MakeGitError(ret) 32 | } 33 | return newPackbuilderFromC(ptr, repo), nil 34 | } 35 | 36 | func newPackbuilderFromC(ptr *C.git_packbuilder, r *Repository) *Packbuilder { 37 | pb := &Packbuilder{ptr: ptr, r: r} 38 | runtime.SetFinalizer(pb, (*Packbuilder).Free) 39 | return pb 40 | } 41 | 42 | func (pb *Packbuilder) Free() { 43 | runtime.SetFinalizer(pb, nil) 44 | C.git_packbuilder_free(pb.ptr) 45 | } 46 | 47 | func (pb *Packbuilder) Insert(id *Oid, name string) error { 48 | cname := C.CString(name) 49 | defer C.free(unsafe.Pointer(cname)) 50 | 51 | runtime.LockOSThread() 52 | defer runtime.UnlockOSThread() 53 | 54 | ret := C.git_packbuilder_insert(pb.ptr, id.toC(), cname) 55 | runtime.KeepAlive(pb) 56 | runtime.KeepAlive(id) 57 | if ret != 0 { 58 | return MakeGitError(ret) 59 | } 60 | return nil 61 | } 62 | 63 | func (pb *Packbuilder) InsertCommit(id *Oid) error { 64 | runtime.LockOSThread() 65 | defer runtime.UnlockOSThread() 66 | 67 | ret := C.git_packbuilder_insert_commit(pb.ptr, id.toC()) 68 | runtime.KeepAlive(pb) 69 | runtime.KeepAlive(id) 70 | if ret != 0 { 71 | return MakeGitError(ret) 72 | } 73 | return nil 74 | } 75 | 76 | func (pb *Packbuilder) InsertTree(id *Oid) error { 77 | runtime.LockOSThread() 78 | defer runtime.UnlockOSThread() 79 | 80 | ret := C.git_packbuilder_insert_tree(pb.ptr, id.toC()) 81 | runtime.KeepAlive(pb) 82 | runtime.KeepAlive(id) 83 | if ret != 0 { 84 | return MakeGitError(ret) 85 | } 86 | return nil 87 | } 88 | 89 | func (pb *Packbuilder) InsertWalk(walk *RevWalk) error { 90 | runtime.LockOSThread() 91 | defer runtime.UnlockOSThread() 92 | 93 | ret := C.git_packbuilder_insert_walk(pb.ptr, walk.ptr) 94 | runtime.KeepAlive(pb) 95 | runtime.KeepAlive(walk) 96 | if ret != 0 { 97 | return MakeGitError(ret) 98 | } 99 | return nil 100 | } 101 | 102 | func (pb *Packbuilder) ObjectCount() uint32 { 103 | ret := uint32(C.git_packbuilder_object_count(pb.ptr)) 104 | runtime.KeepAlive(pb) 105 | return ret 106 | } 107 | 108 | func (pb *Packbuilder) WriteToFile(name string, mode os.FileMode) error { 109 | cname := C.CString(name) 110 | defer C.free(unsafe.Pointer(cname)) 111 | 112 | runtime.LockOSThread() 113 | defer runtime.UnlockOSThread() 114 | 115 | ret := C.git_packbuilder_write(pb.ptr, cname, C.uint(mode.Perm()), nil, nil) 116 | runtime.KeepAlive(pb) 117 | if ret != 0 { 118 | return MakeGitError(ret) 119 | } 120 | return nil 121 | } 122 | 123 | func (pb *Packbuilder) Write(w io.Writer) error { 124 | return pb.ForEach(func(slice []byte) error { 125 | _, err := w.Write(slice) 126 | return err 127 | }) 128 | } 129 | 130 | func (pb *Packbuilder) Written() uint32 { 131 | ret := uint32(C.git_packbuilder_written(pb.ptr)) 132 | runtime.KeepAlive(pb) 133 | return ret 134 | } 135 | 136 | type PackbuilderForeachCallback func([]byte) error 137 | type packbuilderCallbackData struct { 138 | callback PackbuilderForeachCallback 139 | errorTarget *error 140 | } 141 | 142 | //export packbuilderForEachCallback 143 | func packbuilderForEachCallback(buf unsafe.Pointer, size C.size_t, handle unsafe.Pointer) C.int { 144 | payload := pointerHandles.Get(handle) 145 | data, ok := payload.(*packbuilderCallbackData) 146 | if !ok { 147 | panic("could not get packbuilder CB data") 148 | } 149 | 150 | slice := C.GoBytes(buf, C.int(size)) 151 | 152 | err := data.callback(slice) 153 | if err != nil { 154 | *data.errorTarget = err 155 | return C.int(ErrorCodeUser) 156 | } 157 | 158 | return C.int(ErrorCodeOK) 159 | } 160 | 161 | // ForEach repeatedly calls the callback with new packfile data until 162 | // there is no more data or the callback returns an error 163 | func (pb *Packbuilder) ForEach(callback PackbuilderForeachCallback) error { 164 | var err error 165 | data := packbuilderCallbackData{ 166 | callback: callback, 167 | errorTarget: &err, 168 | } 169 | handle := pointerHandles.Track(&data) 170 | defer pointerHandles.Untrack(handle) 171 | 172 | runtime.LockOSThread() 173 | defer runtime.UnlockOSThread() 174 | 175 | ret := C._go_git_packbuilder_foreach(pb.ptr, handle) 176 | runtime.KeepAlive(pb) 177 | if ret == C.int(ErrorCodeUser) && err != nil { 178 | return err 179 | } 180 | if ret < 0 { 181 | return MakeGitError(ret) 182 | } 183 | 184 | return nil 185 | } 186 | -------------------------------------------------------------------------------- /walk.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | 8 | import ( 9 | "runtime" 10 | "unsafe" 11 | ) 12 | 13 | // RevWalk 14 | 15 | type SortType uint 16 | 17 | const ( 18 | SortNone SortType = C.GIT_SORT_NONE 19 | SortTopological SortType = C.GIT_SORT_TOPOLOGICAL 20 | SortTime SortType = C.GIT_SORT_TIME 21 | SortReverse SortType = C.GIT_SORT_REVERSE 22 | ) 23 | 24 | type RevWalk struct { 25 | doNotCompare 26 | ptr *C.git_revwalk 27 | repo *Repository 28 | } 29 | 30 | func revWalkFromC(repo *Repository, c *C.git_revwalk) *RevWalk { 31 | v := &RevWalk{ptr: c, repo: repo} 32 | runtime.SetFinalizer(v, (*RevWalk).Free) 33 | return v 34 | } 35 | 36 | func (v *RevWalk) Reset() { 37 | C.git_revwalk_reset(v.ptr) 38 | runtime.KeepAlive(v) 39 | } 40 | 41 | func (v *RevWalk) Push(id *Oid) error { 42 | runtime.LockOSThread() 43 | defer runtime.UnlockOSThread() 44 | 45 | ecode := C.git_revwalk_push(v.ptr, id.toC()) 46 | runtime.KeepAlive(v) 47 | runtime.KeepAlive(id) 48 | if ecode < 0 { 49 | return MakeGitError(ecode) 50 | } 51 | return nil 52 | } 53 | 54 | func (v *RevWalk) PushGlob(glob string) error { 55 | runtime.LockOSThread() 56 | defer runtime.UnlockOSThread() 57 | 58 | cstr := C.CString(glob) 59 | defer C.free(unsafe.Pointer(cstr)) 60 | 61 | ecode := C.git_revwalk_push_glob(v.ptr, cstr) 62 | runtime.KeepAlive(v) 63 | if ecode < 0 { 64 | return MakeGitError(ecode) 65 | } 66 | return nil 67 | } 68 | 69 | func (v *RevWalk) PushRange(r string) error { 70 | runtime.LockOSThread() 71 | defer runtime.UnlockOSThread() 72 | 73 | cstr := C.CString(r) 74 | defer C.free(unsafe.Pointer(cstr)) 75 | 76 | ecode := C.git_revwalk_push_range(v.ptr, cstr) 77 | runtime.KeepAlive(v) 78 | if ecode < 0 { 79 | return MakeGitError(ecode) 80 | } 81 | return nil 82 | } 83 | 84 | func (v *RevWalk) PushRef(r string) error { 85 | runtime.LockOSThread() 86 | defer runtime.UnlockOSThread() 87 | 88 | cstr := C.CString(r) 89 | defer C.free(unsafe.Pointer(cstr)) 90 | 91 | ecode := C.git_revwalk_push_ref(v.ptr, cstr) 92 | runtime.KeepAlive(v) 93 | if ecode < 0 { 94 | return MakeGitError(ecode) 95 | } 96 | return nil 97 | } 98 | 99 | func (v *RevWalk) PushHead() (err error) { 100 | runtime.LockOSThread() 101 | defer runtime.UnlockOSThread() 102 | 103 | ecode := C.git_revwalk_push_head(v.ptr) 104 | runtime.KeepAlive(v) 105 | if ecode < 0 { 106 | err = MakeGitError(ecode) 107 | } 108 | return nil 109 | } 110 | 111 | func (v *RevWalk) Hide(id *Oid) error { 112 | runtime.LockOSThread() 113 | defer runtime.UnlockOSThread() 114 | 115 | ecode := C.git_revwalk_hide(v.ptr, id.toC()) 116 | runtime.KeepAlive(v) 117 | runtime.KeepAlive(id) 118 | if ecode < 0 { 119 | return MakeGitError(ecode) 120 | } 121 | return nil 122 | } 123 | 124 | func (v *RevWalk) HideGlob(glob string) error { 125 | runtime.LockOSThread() 126 | defer runtime.UnlockOSThread() 127 | 128 | cstr := C.CString(glob) 129 | defer C.free(unsafe.Pointer(cstr)) 130 | 131 | ecode := C.git_revwalk_hide_glob(v.ptr, cstr) 132 | runtime.KeepAlive(v) 133 | if ecode < 0 { 134 | return MakeGitError(ecode) 135 | } 136 | return nil 137 | } 138 | 139 | func (v *RevWalk) HideRef(r string) error { 140 | runtime.LockOSThread() 141 | defer runtime.UnlockOSThread() 142 | 143 | cstr := C.CString(r) 144 | defer C.free(unsafe.Pointer(cstr)) 145 | 146 | ecode := C.git_revwalk_hide_ref(v.ptr, cstr) 147 | runtime.KeepAlive(v) 148 | if ecode < 0 { 149 | return MakeGitError(ecode) 150 | } 151 | return nil 152 | } 153 | 154 | func (v *RevWalk) HideHead() (err error) { 155 | runtime.LockOSThread() 156 | defer runtime.UnlockOSThread() 157 | 158 | ecode := C.git_revwalk_hide_head(v.ptr) 159 | runtime.KeepAlive(v) 160 | if ecode < 0 { 161 | err = MakeGitError(ecode) 162 | } 163 | return nil 164 | } 165 | 166 | func (v *RevWalk) Next(id *Oid) (err error) { 167 | runtime.LockOSThread() 168 | defer runtime.UnlockOSThread() 169 | 170 | ret := C.git_revwalk_next(id.toC(), v.ptr) 171 | runtime.KeepAlive(v) 172 | switch { 173 | case ret < 0: 174 | err = MakeGitError(ret) 175 | } 176 | 177 | return 178 | } 179 | 180 | type RevWalkIterator func(commit *Commit) bool 181 | 182 | func (v *RevWalk) Iterate(fun RevWalkIterator) (err error) { 183 | oid := new(Oid) 184 | for { 185 | err = v.Next(oid) 186 | if IsErrorCode(err, ErrorCodeIterOver) { 187 | return nil 188 | } 189 | if err != nil { 190 | return err 191 | } 192 | 193 | commit, err := v.repo.LookupCommit(oid) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | cont := fun(commit) 199 | if !cont { 200 | break 201 | } 202 | } 203 | 204 | return nil 205 | } 206 | 207 | func (v *RevWalk) SimplifyFirstParent() { 208 | C.git_revwalk_simplify_first_parent(v.ptr) 209 | runtime.KeepAlive(v) 210 | } 211 | 212 | func (v *RevWalk) Sorting(sm SortType) { 213 | C.git_revwalk_sorting(v.ptr, C.uint(sm)) 214 | runtime.KeepAlive(v) 215 | } 216 | 217 | func (v *RevWalk) Free() { 218 | runtime.SetFinalizer(v, nil) 219 | C.git_revwalk_free(v.ptr) 220 | } 221 | -------------------------------------------------------------------------------- /blame.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "runtime" 9 | "unsafe" 10 | ) 11 | 12 | type BlameOptions struct { 13 | Flags BlameOptionsFlag 14 | MinMatchCharacters uint16 15 | NewestCommit *Oid 16 | OldestCommit *Oid 17 | MinLine uint32 18 | MaxLine uint32 19 | } 20 | 21 | func DefaultBlameOptions() (BlameOptions, error) { 22 | runtime.LockOSThread() 23 | defer runtime.UnlockOSThread() 24 | 25 | opts := C.git_blame_options{} 26 | ecode := C.git_blame_options_init(&opts, C.GIT_BLAME_OPTIONS_VERSION) 27 | if ecode < 0 { 28 | return BlameOptions{}, MakeGitError(ecode) 29 | } 30 | 31 | return BlameOptions{ 32 | Flags: BlameOptionsFlag(opts.flags), 33 | MinMatchCharacters: uint16(opts.min_match_characters), 34 | NewestCommit: newOidFromC(&opts.newest_commit), 35 | OldestCommit: newOidFromC(&opts.oldest_commit), 36 | MinLine: uint32(opts.min_line), 37 | MaxLine: uint32(opts.max_line), 38 | }, nil 39 | } 40 | 41 | type BlameOptionsFlag uint32 42 | 43 | const ( 44 | BlameNormal BlameOptionsFlag = C.GIT_BLAME_NORMAL 45 | BlameTrackCopiesSameFile BlameOptionsFlag = C.GIT_BLAME_TRACK_COPIES_SAME_FILE 46 | BlameTrackCopiesSameCommitMoves BlameOptionsFlag = C.GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES 47 | BlameTrackCopiesSameCommitCopies BlameOptionsFlag = C.GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES 48 | BlameTrackCopiesAnyCommitCopies BlameOptionsFlag = C.GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES 49 | BlameFirstParent BlameOptionsFlag = C.GIT_BLAME_FIRST_PARENT 50 | BlameUseMailmap BlameOptionsFlag = C.GIT_BLAME_USE_MAILMAP 51 | BlameIgnoreWhitespace BlameOptionsFlag = C.GIT_BLAME_IGNORE_WHITESPACE 52 | ) 53 | 54 | func (v *Repository) BlameFile(path string, opts *BlameOptions) (*Blame, error) { 55 | var blamePtr *C.git_blame 56 | 57 | var copts *C.git_blame_options 58 | if opts != nil { 59 | copts = &C.git_blame_options{ 60 | version: C.GIT_BLAME_OPTIONS_VERSION, 61 | flags: C.uint32_t(opts.Flags), 62 | min_match_characters: C.uint16_t(opts.MinMatchCharacters), 63 | min_line: C.size_t(opts.MinLine), 64 | max_line: C.size_t(opts.MaxLine), 65 | } 66 | if opts.NewestCommit != nil { 67 | copts.newest_commit = *opts.NewestCommit.toC() 68 | } 69 | if opts.OldestCommit != nil { 70 | copts.oldest_commit = *opts.OldestCommit.toC() 71 | } 72 | } 73 | 74 | cpath := C.CString(path) 75 | defer C.free(unsafe.Pointer(cpath)) 76 | 77 | runtime.LockOSThread() 78 | defer runtime.UnlockOSThread() 79 | 80 | ecode := C.git_blame_file(&blamePtr, v.ptr, cpath, copts) 81 | runtime.KeepAlive(v) 82 | if ecode < 0 { 83 | return nil, MakeGitError(ecode) 84 | } 85 | 86 | return newBlameFromC(blamePtr), nil 87 | } 88 | 89 | type Blame struct { 90 | doNotCompare 91 | ptr *C.git_blame 92 | } 93 | 94 | func (blame *Blame) HunkCount() int { 95 | ret := int(C.git_blame_get_hunk_count(blame.ptr)) 96 | runtime.KeepAlive(blame) 97 | 98 | return ret 99 | } 100 | 101 | func (blame *Blame) HunkByIndex(index int) (BlameHunk, error) { 102 | ptr := C.git_blame_get_hunk_byindex(blame.ptr, C.uint32_t(index)) 103 | runtime.KeepAlive(blame) 104 | if ptr == nil { 105 | return BlameHunk{}, ErrInvalid 106 | } 107 | return blameHunkFromC(ptr), nil 108 | } 109 | 110 | func (blame *Blame) HunkByLine(lineno int) (BlameHunk, error) { 111 | ptr := C.git_blame_get_hunk_byline(blame.ptr, C.size_t(lineno)) 112 | runtime.KeepAlive(blame) 113 | if ptr == nil { 114 | return BlameHunk{}, ErrInvalid 115 | } 116 | return blameHunkFromC(ptr), nil 117 | } 118 | 119 | func newBlameFromC(ptr *C.git_blame) *Blame { 120 | if ptr == nil { 121 | return nil 122 | } 123 | 124 | blame := &Blame{ 125 | ptr: ptr, 126 | } 127 | 128 | runtime.SetFinalizer(blame, (*Blame).Free) 129 | return blame 130 | } 131 | 132 | func (blame *Blame) Free() error { 133 | if blame.ptr == nil { 134 | return ErrInvalid 135 | } 136 | runtime.SetFinalizer(blame, nil) 137 | C.git_blame_free(blame.ptr) 138 | blame.ptr = nil 139 | return nil 140 | } 141 | 142 | type BlameHunk struct { 143 | LinesInHunk uint16 144 | FinalCommitId *Oid 145 | FinalStartLineNumber uint16 146 | FinalSignature *Signature 147 | OrigCommitId *Oid 148 | OrigPath string 149 | OrigStartLineNumber uint16 150 | OrigSignature *Signature 151 | Boundary bool 152 | } 153 | 154 | func blameHunkFromC(hunk *C.git_blame_hunk) BlameHunk { 155 | return BlameHunk{ 156 | LinesInHunk: uint16(hunk.lines_in_hunk), 157 | FinalCommitId: newOidFromC(&hunk.final_commit_id), 158 | FinalStartLineNumber: uint16(hunk.final_start_line_number), 159 | FinalSignature: newSignatureFromC(hunk.final_signature), 160 | OrigCommitId: newOidFromC(&hunk.orig_commit_id), 161 | OrigPath: C.GoString(hunk.orig_path), 162 | OrigStartLineNumber: uint16(hunk.orig_start_line_number), 163 | OrigSignature: newSignatureFromC(hunk.orig_signature), 164 | Boundary: hunk.boundary == 1, 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /object_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestObjectPoymorphism(t *testing.T) { 9 | t.Parallel() 10 | repo := createTestRepo(t) 11 | defer cleanupTestRepo(t, repo) 12 | 13 | commitId, treeId := seedTestRepo(t, repo) 14 | 15 | var obj *Object 16 | 17 | commit, err := repo.LookupCommit(commitId) 18 | checkFatal(t, err) 19 | 20 | obj = &commit.Object 21 | if obj.Type() != ObjectCommit { 22 | t.Fatalf("Wrong object type, expected commit, have %v", obj.Type()) 23 | } 24 | 25 | commitTree, err := commit.Tree() 26 | checkFatal(t, err) 27 | commitTree.EntryCount() 28 | 29 | tree, err := repo.LookupTree(treeId) 30 | checkFatal(t, err) 31 | 32 | obj = &tree.Object 33 | if obj.Type() != ObjectTree { 34 | t.Fatalf("Wrong object type, expected tree, have %v", obj.Type()) 35 | } 36 | 37 | tree2, err := obj.AsTree() 38 | if err != nil { 39 | t.Fatalf("Converting back to *Tree is not ok") 40 | } 41 | 42 | entry := tree2.EntryByName("README") 43 | if entry == nil { 44 | t.Fatalf("Tree did not have expected \"README\" entry") 45 | } 46 | 47 | if entry.Filemode != FilemodeBlob { 48 | t.Fatal("Wrong filemode for \"README\"") 49 | } 50 | 51 | _, err = obj.AsCommit() 52 | if err == nil { 53 | t.Fatalf("*Tree is somehow the same as *Commit") 54 | } 55 | 56 | obj, err = repo.Lookup(tree.Id()) 57 | checkFatal(t, err) 58 | 59 | _, err = obj.AsTree() 60 | if err != nil { 61 | t.Fatalf("Lookup creates the wrong type") 62 | } 63 | 64 | if obj.Type() != ObjectTree { 65 | t.Fatalf("Type() doesn't agree with dynamic type") 66 | } 67 | 68 | obj, err = repo.RevparseSingle("HEAD") 69 | checkFatal(t, err) 70 | if obj.Type() != ObjectCommit || obj.Id().String() != commit.Id().String() { 71 | t.Fatalf("Failed to parse the right revision") 72 | } 73 | 74 | obj, err = repo.RevparseSingle("HEAD^{tree}") 75 | checkFatal(t, err) 76 | if obj.Type() != ObjectTree || obj.Id().String() != tree.Id().String() { 77 | t.Fatalf("Failed to parse the right revision") 78 | } 79 | } 80 | 81 | func checkOwner(t *testing.T, repo *Repository, obj Object) { 82 | owner := obj.Owner() 83 | if owner == nil { 84 | t.Fatal("bad owner") 85 | } 86 | 87 | if owner.ptr != repo.ptr { 88 | t.Fatalf("bad owner, got %v expected %v\n", owner.ptr, repo.ptr) 89 | } 90 | } 91 | 92 | func TestObjectOwner(t *testing.T) { 93 | t.Parallel() 94 | repo := createTestRepo(t) 95 | defer cleanupTestRepo(t, repo) 96 | 97 | commitId, treeId := seedTestRepo(t, repo) 98 | 99 | commit, err := repo.LookupCommit(commitId) 100 | checkFatal(t, err) 101 | 102 | tree, err := repo.LookupTree(treeId) 103 | checkFatal(t, err) 104 | 105 | checkOwner(t, repo, commit.Object) 106 | checkOwner(t, repo, tree.Object) 107 | } 108 | 109 | func checkShortId(t *testing.T, Id, shortId string) { 110 | if len(shortId) < 7 || len(shortId) >= len(Id) { 111 | t.Fatalf("bad shortId length %d", len(shortId)) 112 | } 113 | 114 | if !strings.HasPrefix(Id, shortId) { 115 | t.Fatalf("bad shortId, should be prefix of %s, but is %s\n", Id, shortId) 116 | } 117 | } 118 | 119 | func TestObjectShortId(t *testing.T) { 120 | t.Parallel() 121 | repo := createTestRepo(t) 122 | defer cleanupTestRepo(t, repo) 123 | 124 | commitId, _ := seedTestRepo(t, repo) 125 | 126 | commit, err := repo.LookupCommit(commitId) 127 | checkFatal(t, err) 128 | 129 | shortId, err := commit.ShortId() 130 | checkFatal(t, err) 131 | 132 | checkShortId(t, commitId.String(), shortId) 133 | } 134 | 135 | func TestObjectPeel(t *testing.T) { 136 | t.Parallel() 137 | repo := createTestRepo(t) 138 | defer cleanupTestRepo(t, repo) 139 | 140 | commitID, treeID := seedTestRepo(t, repo) 141 | 142 | var obj *Object 143 | 144 | commit, err := repo.LookupCommit(commitID) 145 | checkFatal(t, err) 146 | 147 | obj, err = commit.Peel(ObjectAny) 148 | checkFatal(t, err) 149 | 150 | if obj.Type() != ObjectTree { 151 | t.Fatalf("Wrong object type when peeling a commit, expected tree, have %v", obj.Type()) 152 | } 153 | 154 | obj, err = commit.Peel(ObjectTag) 155 | 156 | if !IsErrorCode(err, ErrorCodeInvalidSpec) { 157 | t.Fatalf("Wrong error when peeling a commit to a tag, expected ErrorCodeInvalidSpec, have %v", err) 158 | } 159 | 160 | tree, err := repo.LookupTree(treeID) 161 | checkFatal(t, err) 162 | 163 | obj, err = tree.Peel(ObjectAny) 164 | 165 | if !IsErrorCode(err, ErrorCodeInvalidSpec) { 166 | t.Fatalf("Wrong error when peeling a tree, expected ErrorCodeInvalidSpec, have %v", err) 167 | } 168 | 169 | entry := tree.EntryByName("README") 170 | 171 | blob, err := repo.LookupBlob(entry.Id) 172 | checkFatal(t, err) 173 | 174 | obj, err = blob.Peel(ObjectAny) 175 | 176 | if !IsErrorCode(err, ErrorCodeInvalidSpec) { 177 | t.Fatalf("Wrong error when peeling a blob, expected ErrorCodeInvalidSpec, have %v", err) 178 | } 179 | 180 | tagID := createTestTag(t, repo, commit) 181 | 182 | tag, err := repo.LookupTag(tagID) 183 | checkFatal(t, err) 184 | 185 | obj, err = tag.Peel(ObjectAny) 186 | checkFatal(t, err) 187 | 188 | if obj.Type() != ObjectCommit { 189 | t.Fatalf("Wrong object type when peeling a tag, expected commit, have %v", obj.Type()) 190 | } 191 | 192 | // TODO: Should test a tag that annotates a different object than a commit 193 | // but it's impossible at the moment to tag such an object. 194 | } 195 | -------------------------------------------------------------------------------- /git_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | if err := registerManagedHTTP(); err != nil { 15 | panic(err) 16 | } 17 | 18 | ret := m.Run() 19 | 20 | if err := unregisterManagedTransports(); err != nil { 21 | panic(err) 22 | } 23 | 24 | // Ensure that we are not leaking any pointer handles. 25 | pointerHandles.Lock() 26 | if len(pointerHandles.handles) > 0 { 27 | for h, ptr := range pointerHandles.handles { 28 | fmt.Printf("%016p: %v %+v\n", h, reflect.TypeOf(ptr), ptr) 29 | } 30 | panic("pointer handle list not empty") 31 | } 32 | pointerHandles.Unlock() 33 | 34 | // Or remote pointers. 35 | remotePointers.Lock() 36 | if len(remotePointers.pointers) > 0 { 37 | for ptr, remote := range remotePointers.pointers { 38 | fmt.Printf("%016p: %+v\n", ptr, remote) 39 | } 40 | panic("remote pointer list not empty") 41 | } 42 | remotePointers.Unlock() 43 | 44 | Shutdown() 45 | 46 | os.Exit(ret) 47 | } 48 | 49 | func cleanupTestRepo(t *testing.T, r *Repository) { 50 | var err error 51 | if r.IsBare() { 52 | err = os.RemoveAll(r.Path()) 53 | } else { 54 | err = os.RemoveAll(r.Workdir()) 55 | } 56 | checkFatal(t, err) 57 | 58 | r.Free() 59 | } 60 | 61 | func createTestRepo(t *testing.T) *Repository { 62 | // figure out where we can create the test repo 63 | path, err := ioutil.TempDir("", "git2go") 64 | checkFatal(t, err) 65 | repo, err := InitRepository(path, false) 66 | checkFatal(t, err) 67 | 68 | tmpfile := "README" 69 | err = ioutil.WriteFile(path+"/"+tmpfile, []byte("foo\n"), 0644) 70 | 71 | checkFatal(t, err) 72 | 73 | return repo 74 | } 75 | 76 | func createBareTestRepo(t *testing.T) *Repository { 77 | // figure out where we can create the test repo 78 | path, err := ioutil.TempDir("", "git2go") 79 | checkFatal(t, err) 80 | repo, err := InitRepository(path, true) 81 | checkFatal(t, err) 82 | 83 | return repo 84 | } 85 | 86 | // commitOptions contains any extra options for creating commits in the seed repo 87 | type commitOptions struct { 88 | CommitSigningCallback 89 | } 90 | 91 | func seedTestRepo(t *testing.T, repo *Repository) (*Oid, *Oid) { 92 | return seedTestRepoOpt(t, repo, commitOptions{}) 93 | } 94 | 95 | func seedTestRepoOpt(t *testing.T, repo *Repository, opts commitOptions) (*Oid, *Oid) { 96 | loc, err := time.LoadLocation("Europe/Berlin") 97 | checkFatal(t, err) 98 | sig := &Signature{ 99 | Name: "Rand Om Hacker", 100 | Email: "random@hacker.com", 101 | When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), 102 | } 103 | 104 | idx, err := repo.Index() 105 | checkFatal(t, err) 106 | err = idx.AddByPath("README") 107 | checkFatal(t, err) 108 | err = idx.Write() 109 | checkFatal(t, err) 110 | treeId, err := idx.WriteTree() 111 | checkFatal(t, err) 112 | 113 | message := "This is a commit\n" 114 | tree, err := repo.LookupTree(treeId) 115 | checkFatal(t, err) 116 | commitId, err := repo.CreateCommit("HEAD", sig, sig, message, tree) 117 | checkFatal(t, err) 118 | 119 | if opts.CommitSigningCallback != nil { 120 | commit, err := repo.LookupCommit(commitId) 121 | checkFatal(t, err) 122 | 123 | signature, signatureField, err := opts.CommitSigningCallback(commit.ContentToSign()) 124 | checkFatal(t, err) 125 | 126 | oid, err := commit.WithSignature(signature, signatureField) 127 | checkFatal(t, err) 128 | newCommit, err := repo.LookupCommit(oid) 129 | checkFatal(t, err) 130 | head, err := repo.Head() 131 | checkFatal(t, err) 132 | _, err = repo.References.Create( 133 | head.Name(), 134 | newCommit.Id(), 135 | true, 136 | "repoint to signed commit", 137 | ) 138 | checkFatal(t, err) 139 | } 140 | 141 | return commitId, treeId 142 | } 143 | 144 | func pathInRepo(repo *Repository, name string) string { 145 | return path.Join(path.Dir(path.Dir(repo.Path())), name) 146 | } 147 | 148 | func updateReadme(t *testing.T, repo *Repository, content string) (*Oid, *Oid) { 149 | loc, err := time.LoadLocation("Europe/Berlin") 150 | checkFatal(t, err) 151 | sig := &Signature{ 152 | Name: "Rand Om Hacker", 153 | Email: "random@hacker.com", 154 | When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), 155 | } 156 | 157 | tmpfile := "README" 158 | err = ioutil.WriteFile(pathInRepo(repo, tmpfile), []byte(content), 0644) 159 | checkFatal(t, err) 160 | 161 | idx, err := repo.Index() 162 | checkFatal(t, err) 163 | err = idx.AddByPath("README") 164 | checkFatal(t, err) 165 | err = idx.Write() 166 | checkFatal(t, err) 167 | treeId, err := idx.WriteTree() 168 | checkFatal(t, err) 169 | 170 | currentBranch, err := repo.Head() 171 | checkFatal(t, err) 172 | currentTip, err := repo.LookupCommit(currentBranch.Target()) 173 | checkFatal(t, err) 174 | 175 | message := "This is a commit\n" 176 | tree, err := repo.LookupTree(treeId) 177 | checkFatal(t, err) 178 | commitId, err := repo.CreateCommit("HEAD", sig, sig, message, tree, currentTip) 179 | checkFatal(t, err) 180 | 181 | return commitId, treeId 182 | } 183 | 184 | func TestOidZero(t *testing.T) { 185 | t.Parallel() 186 | var zeroId Oid 187 | 188 | if !zeroId.IsZero() { 189 | t.Error("Zero Oid is not zero") 190 | } 191 | } 192 | 193 | func TestEmptyOid(t *testing.T) { 194 | t.Parallel() 195 | _, err := NewOid("") 196 | if err == nil || !IsErrorCode(err, ErrorCodeGeneric) { 197 | t.Fatal("Should have returned invalid error") 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /stash_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "reflect" 9 | "runtime" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestStash(t *testing.T) { 15 | repo := createTestRepo(t) 16 | defer cleanupTestRepo(t, repo) 17 | 18 | prepareStashRepo(t, repo) 19 | 20 | sig := &Signature{ 21 | Name: "Rand Om Hacker", 22 | Email: "random@hacker.com", 23 | When: time.Now(), 24 | } 25 | 26 | stash1, err := repo.Stashes.Save(sig, "First stash", StashDefault) 27 | checkFatal(t, err) 28 | 29 | _, err = repo.LookupCommit(stash1) 30 | checkFatal(t, err) 31 | 32 | b, err := ioutil.ReadFile(pathInRepo(repo, "README")) 33 | checkFatal(t, err) 34 | if string(b) == "Update README goes to stash\n" { 35 | t.Errorf("README still contains the uncommitted changes") 36 | } 37 | 38 | if !fileExistsInRepo(repo, "untracked.txt") { 39 | t.Errorf("untracked.txt doesn't exist in the repo; should be untracked") 40 | } 41 | 42 | // Apply: default 43 | 44 | opts, err := DefaultStashApplyOptions() 45 | checkFatal(t, err) 46 | 47 | err = repo.Stashes.Apply(0, opts) 48 | checkFatal(t, err) 49 | 50 | b, err = ioutil.ReadFile(pathInRepo(repo, "README")) 51 | checkFatal(t, err) 52 | if string(b) != "Update README goes to stash\n" { 53 | t.Errorf("README changes aren't here") 54 | } 55 | 56 | // Apply: no stash for the given index 57 | 58 | err = repo.Stashes.Apply(1, opts) 59 | if !IsErrorCode(err, ErrorCodeNotFound) { 60 | t.Errorf("expecting GIT_ENOTFOUND error code %d, got %v", ErrorCodeNotFound, err) 61 | } 62 | 63 | // Apply: callback stopped 64 | 65 | opts.ProgressCallback = func(progress StashApplyProgress) error { 66 | if progress == StashApplyProgressCheckoutModified { 67 | return fmt.Errorf("Stop") 68 | } 69 | return nil 70 | } 71 | 72 | err = repo.Stashes.Apply(0, opts) 73 | if err.Error() != "Stop" { 74 | t.Errorf("expecting error 'Stop', got %v", err) 75 | } 76 | 77 | // Create second stash with ignored files 78 | 79 | os.MkdirAll(pathInRepo(repo, "tmp"), os.ModeDir|os.ModePerm) 80 | err = ioutil.WriteFile(pathInRepo(repo, "tmp/ignored.txt"), []byte("Ignore me\n"), 0644) 81 | checkFatal(t, err) 82 | 83 | stash2, err := repo.Stashes.Save(sig, "Second stash", StashIncludeIgnored) 84 | checkFatal(t, err) 85 | 86 | if fileExistsInRepo(repo, "tmp/ignored.txt") { 87 | t.Errorf("tmp/ignored.txt should not exist anymore in the work dir") 88 | } 89 | 90 | // Stash foreach 91 | 92 | expected := []stash{ 93 | {0, "On master: Second stash", stash2.String()}, 94 | {1, "On master: First stash", stash1.String()}, 95 | } 96 | checkStashes(t, repo, expected) 97 | 98 | // Stash pop 99 | 100 | opts, _ = DefaultStashApplyOptions() 101 | err = repo.Stashes.Pop(1, opts) 102 | checkFatal(t, err) 103 | 104 | b, err = ioutil.ReadFile(pathInRepo(repo, "README")) 105 | checkFatal(t, err) 106 | if string(b) != "Update README goes to stash\n" { 107 | t.Errorf("README changes aren't here") 108 | } 109 | 110 | expected = []stash{ 111 | {0, "On master: Second stash", stash2.String()}, 112 | } 113 | checkStashes(t, repo, expected) 114 | 115 | // Stash drop 116 | 117 | err = repo.Stashes.Drop(0) 118 | checkFatal(t, err) 119 | 120 | expected = []stash{} 121 | checkStashes(t, repo, expected) 122 | } 123 | 124 | type stash struct { 125 | index int 126 | msg string 127 | id string 128 | } 129 | 130 | func checkStashes(t *testing.T, repo *Repository, expected []stash) { 131 | var actual []stash 132 | 133 | repo.Stashes.Foreach(func(index int, msg string, id *Oid) error { 134 | stash := stash{index, msg, id.String()} 135 | if len(expected) > len(actual) { 136 | if s := expected[len(actual)]; s.id == "" { 137 | stash.id = "" // don't check id 138 | } 139 | } 140 | actual = append(actual, stash) 141 | return nil 142 | }) 143 | 144 | if len(expected) > 0 && !reflect.DeepEqual(expected, actual) { 145 | // The failure happens at wherever we were called, not here 146 | _, file, line, ok := runtime.Caller(1) 147 | if !ok { 148 | t.Fatalf("Unable to get caller") 149 | } 150 | t.Errorf("%v:%v: expecting %#v\ngot %#v", path.Base(file), line, expected, actual) 151 | } 152 | } 153 | 154 | func prepareStashRepo(t *testing.T, repo *Repository) { 155 | seedTestRepo(t, repo) 156 | 157 | err := ioutil.WriteFile(pathInRepo(repo, ".gitignore"), []byte("tmp\n"), 0644) 158 | checkFatal(t, err) 159 | 160 | sig := &Signature{ 161 | Name: "Rand Om Hacker", 162 | Email: "random@hacker.com", 163 | When: time.Now(), 164 | } 165 | 166 | idx, err := repo.Index() 167 | checkFatal(t, err) 168 | err = idx.AddByPath(".gitignore") 169 | checkFatal(t, err) 170 | treeID, err := idx.WriteTree() 171 | checkFatal(t, err) 172 | err = idx.Write() 173 | checkFatal(t, err) 174 | 175 | currentBranch, err := repo.Head() 176 | checkFatal(t, err) 177 | currentTip, err := repo.LookupCommit(currentBranch.Target()) 178 | checkFatal(t, err) 179 | 180 | message := "Add .gitignore\n" 181 | tree, err := repo.LookupTree(treeID) 182 | checkFatal(t, err) 183 | _, err = repo.CreateCommit("HEAD", sig, sig, message, tree, currentTip) 184 | checkFatal(t, err) 185 | 186 | err = ioutil.WriteFile(pathInRepo(repo, "README"), []byte("Update README goes to stash\n"), 0644) 187 | checkFatal(t, err) 188 | 189 | err = ioutil.WriteFile(pathInRepo(repo, "untracked.txt"), []byte("Hello, World\n"), 0644) 190 | checkFatal(t, err) 191 | } 192 | 193 | func fileExistsInRepo(repo *Repository, name string) bool { 194 | if _, err := os.Stat(pathInRepo(repo, name)); err != nil { 195 | return false 196 | } 197 | return true 198 | } 199 | -------------------------------------------------------------------------------- /tag_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestCreateTag(t *testing.T) { 10 | t.Parallel() 11 | repo := createTestRepo(t) 12 | defer cleanupTestRepo(t, repo) 13 | 14 | commitId, _ := seedTestRepo(t, repo) 15 | 16 | commit, err := repo.LookupCommit(commitId) 17 | checkFatal(t, err) 18 | 19 | tagId := createTestTag(t, repo, commit) 20 | 21 | tag, err := repo.LookupTag(tagId) 22 | checkFatal(t, err) 23 | 24 | compareStrings(t, "v0.0.0", tag.Name()) 25 | compareStrings(t, "This is a tag", tag.Message()) 26 | compareStrings(t, commitId.String(), tag.TargetId().String()) 27 | } 28 | 29 | func TestCreateTagLightweight(t *testing.T) { 30 | t.Parallel() 31 | repo := createTestRepo(t) 32 | defer cleanupTestRepo(t, repo) 33 | 34 | commitID, _ := seedTestRepo(t, repo) 35 | 36 | commit, err := repo.LookupCommit(commitID) 37 | checkFatal(t, err) 38 | 39 | tagID, err := repo.Tags.CreateLightweight("v0.1.0", commit, false) 40 | checkFatal(t, err) 41 | 42 | _, err = repo.Tags.CreateLightweight("v0.1.0", commit, true) 43 | checkFatal(t, err) 44 | 45 | ref, err := repo.References.Lookup("refs/tags/v0.1.0") 46 | checkFatal(t, err) 47 | 48 | compareStrings(t, "refs/tags/v0.1.0", ref.Name()) 49 | compareStrings(t, "v0.1.0", ref.Shorthand()) 50 | compareStrings(t, tagID.String(), commitID.String()) 51 | compareStrings(t, commitID.String(), ref.Target().String()) 52 | } 53 | 54 | func TestListTags(t *testing.T) { 55 | t.Parallel() 56 | repo := createTestRepo(t) 57 | defer cleanupTestRepo(t, repo) 58 | 59 | commitID, _ := seedTestRepo(t, repo) 60 | 61 | commit, err := repo.LookupCommit(commitID) 62 | checkFatal(t, err) 63 | 64 | createTag(t, repo, commit, "v1.0.1", "Release v1.0.1") 65 | 66 | commitID, _ = updateReadme(t, repo, "Release version 2") 67 | 68 | commit, err = repo.LookupCommit(commitID) 69 | checkFatal(t, err) 70 | 71 | createTag(t, repo, commit, "v2.0.0", "Release v2.0.0") 72 | 73 | expected := []string{ 74 | "v1.0.1", 75 | "v2.0.0", 76 | } 77 | 78 | actual, err := repo.Tags.List() 79 | checkFatal(t, err) 80 | 81 | compareStringList(t, expected, actual) 82 | } 83 | 84 | func TestListTagsWithMatch(t *testing.T) { 85 | t.Parallel() 86 | repo := createTestRepo(t) 87 | defer cleanupTestRepo(t, repo) 88 | 89 | commitID, _ := seedTestRepo(t, repo) 90 | 91 | commit, err := repo.LookupCommit(commitID) 92 | checkFatal(t, err) 93 | 94 | createTag(t, repo, commit, "v1.0.1", "Release v1.0.1") 95 | 96 | commitID, _ = updateReadme(t, repo, "Release version 2") 97 | 98 | commit, err = repo.LookupCommit(commitID) 99 | checkFatal(t, err) 100 | 101 | createTag(t, repo, commit, "v2.0.0", "Release v2.0.0") 102 | 103 | expected := []string{ 104 | "v2.0.0", 105 | } 106 | 107 | actual, err := repo.Tags.ListWithMatch("v2*") 108 | checkFatal(t, err) 109 | 110 | compareStringList(t, expected, actual) 111 | 112 | expected = []string{ 113 | "v1.0.1", 114 | } 115 | 116 | actual, err = repo.Tags.ListWithMatch("v1*") 117 | checkFatal(t, err) 118 | 119 | compareStringList(t, expected, actual) 120 | } 121 | 122 | func TestTagForeach(t *testing.T) { 123 | t.Parallel() 124 | repo := createTestRepo(t) 125 | defer cleanupTestRepo(t, repo) 126 | 127 | commitID, _ := seedTestRepo(t, repo) 128 | 129 | commit, err := repo.LookupCommit(commitID) 130 | checkFatal(t, err) 131 | 132 | tag1 := createTag(t, repo, commit, "v1.0.1", "Release v1.0.1") 133 | 134 | commitID, _ = updateReadme(t, repo, "Release version 2") 135 | 136 | commit, err = repo.LookupCommit(commitID) 137 | checkFatal(t, err) 138 | 139 | tag2 := createTag(t, repo, commit, "v2.0.0", "Release v2.0.0") 140 | 141 | expectedNames := []string{ 142 | "refs/tags/v1.0.1", 143 | "refs/tags/v2.0.0", 144 | } 145 | actualNames := []string{} 146 | expectedOids := []string{ 147 | tag1.String(), 148 | tag2.String(), 149 | } 150 | actualOids := []string{} 151 | 152 | err = repo.Tags.Foreach(func(name string, id *Oid) error { 153 | actualNames = append(actualNames, name) 154 | actualOids = append(actualOids, id.String()) 155 | return nil 156 | }) 157 | checkFatal(t, err) 158 | 159 | compareStringList(t, expectedNames, actualNames) 160 | compareStringList(t, expectedOids, actualOids) 161 | 162 | fakeErr := errors.New("fake error") 163 | 164 | err = repo.Tags.Foreach(func(name string, id *Oid) error { 165 | return fakeErr 166 | }) 167 | 168 | if err != fakeErr { 169 | t.Fatalf("Tags.Foreach() did not return the expected error, got %v", err) 170 | } 171 | } 172 | 173 | func compareStrings(t *testing.T, expected, value string) { 174 | if value != expected { 175 | t.Fatalf("expected '%v', actual '%v'", expected, value) 176 | } 177 | } 178 | 179 | func createTestTag(t *testing.T, repo *Repository, commit *Commit) *Oid { 180 | loc, err := time.LoadLocation("Europe/Berlin") 181 | checkFatal(t, err) 182 | sig := &Signature{ 183 | Name: "Rand Om Hacker", 184 | Email: "random@hacker.com", 185 | When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), 186 | } 187 | 188 | tagId, err := repo.Tags.Create("v0.0.0", commit, sig, "This is a tag") 189 | checkFatal(t, err) 190 | return tagId 191 | } 192 | 193 | func createTag(t *testing.T, repo *Repository, commit *Commit, name, message string) *Oid { 194 | loc, err := time.LoadLocation("Europe/Bucharest") 195 | checkFatal(t, err) 196 | sig := &Signature{ 197 | Name: "Rand Om Hacker", 198 | Email: "random@hacker.com", 199 | When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), 200 | } 201 | 202 | tagId, err := repo.Tags.Create(name, commit, sig, message) 203 | checkFatal(t, err) 204 | return tagId 205 | } 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | git2go 2 | ====== 3 | [![GoDoc](https://godoc.org/github.com/libgit2/git2go?status.svg)](http://godoc.org/github.com/libgit2/git2go/v34) [![Build Status](https://travis-ci.org/libgit2/git2go.svg?branch=main)](https://travis-ci.org/libgit2/git2go) 4 | 5 | Go bindings for [libgit2](http://libgit2.github.com/). 6 | 7 | ### Which Go version to use 8 | 9 | Due to the fact that Go 1.11 module versions have semantic meaning and don't necessarily align with libgit2's release schedule, please consult the following table for a mapping between libgit2 and git2go module versions: 10 | 11 | | libgit2 | git2go | 12 | |---------|---------------| 13 | | main | (will be v35) | 14 | | 1.5 | v34 | 15 | | 1.3 | v33 | 16 | | 1.2 | v32 | 17 | | 1.1 | v31 | 18 | | 1.0 | v30 | 19 | | 0.99 | v29 | 20 | | 0.28 | v28 | 21 | | 0.27 | v27 | 22 | 23 | You can import them in your project with the version's major number as a suffix. For example, if you have libgit2 v1.2 installed, you'd import git2go v34 with: 24 | 25 | ```sh 26 | go get github.com/libgit2/git2go/v34 27 | ``` 28 | ```go 29 | import "github.com/libgit2/git2go/v34" 30 | ``` 31 | 32 | which will ensure there are no sudden changes to the API. 33 | 34 | The `main` branch follows the tip of libgit2 itself (with some lag) and as such has no guarantees on the stability of libgit2's API. Thus this only supports statically linking against libgit2. 35 | 36 | ### Which branch to send Pull requests to 37 | 38 | If there's something version-specific that you'd want to contribute to, you can send them to the `release-${MAJOR}.${MINOR}` branches, which follow libgit2's releases. 39 | 40 | Installing 41 | ---------- 42 | 43 | This project wraps the functionality provided by libgit2. It thus needs it in order to perform the work. 44 | 45 | This project wraps the functionality provided by libgit2. If you're using a versioned branch, install it to your system via your system's package manager and then install git2go. 46 | 47 | 48 | ### Versioned branch, dynamic linking 49 | 50 | When linking dynamically against a released version of libgit2, install it via your system's package manager. CGo will take care of finding its pkg-config file and set up the linking. Import via Go modules, e.g. to work against libgit2 v1.2 51 | 52 | ```go 53 | import "github.com/libgit2/git2go/v34" 54 | ``` 55 | 56 | ### Versioned branch, static linking 57 | 58 | Follow the instructions for [Versioned branch, dynamic linking](#versioned-branch-dynamic-linking), but pass the `-tags static,system_libgit2` flag to all `go` commands that build any binaries. For instance: 59 | 60 | go build -tags static,system_libgit2 github.com/my/project/... 61 | go test -tags static,system_libgit2 github.com/my/project/... 62 | go install -tags static,system_libgit2 github.com/my/project/... 63 | 64 | ### `main` branch, or vendored static linking 65 | 66 | If using `main` or building a branch with the vendored libgit2 statically, we need to build libgit2 first. In order to build it, you need `cmake`, `pkg-config` and a C compiler. You will also need the development packages for OpenSSL (outside of Windows or macOS) and LibSSH2 installed if you want libgit2 to support HTTPS and SSH respectively. Note that even if libgit2 is included in the resulting binary, its dependencies will not be. 67 | 68 | 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. 69 | 70 | git submodule update --init # get libgit2 71 | make install-static 72 | 73 | will compile libgit2, link it into git2go and install it. The `main` branch is set up to follow the specific libgit2 version that is vendored, so trying dynamic linking may or may not work depending on the exact versions involved. 74 | 75 | In order to let Go pass the correct flags to `pkg-config`, `-tags static` needs to be passed to all `go` commands that build any binaries. For instance: 76 | 77 | go build -tags static github.com/my/project/... 78 | go test -tags static github.com/my/project/... 79 | go install -tags static github.com/my/project/... 80 | 81 | One thing to take into account is that since Go expects the `pkg-config` file to be within the same directory where `make install-static` was called, so the `go.mod` file may need to have a [`replace` directive](https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive) so that the correct setup is achieved. So if `git2go` is checked out at `$GOPATH/src/github.com/libgit2/git2go` and your project at `$GOPATH/src/github.com/my/project`, the `go.mod` file of `github.com/my/project` might need to have a line like 82 | 83 | replace github.com/libgit2/git2go/v34 => ../../libgit2/git2go 84 | 85 | Parallelism and network operations 86 | ---------------------------------- 87 | 88 | libgit2 may use 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. 89 | 90 | Running the tests 91 | ----------------- 92 | 93 | For the stable version, `go test` will work as usual. For the `main` branch, similarly to installing, running the tests requires building a local libgit2 library, so the Makefile provides a wrapper that makes sure it's built 94 | 95 | make test-static 96 | 97 | Alternatively, you can build the library manually first and then run the tests 98 | 99 | make install-static 100 | go test -v -tags static ./... 101 | 102 | License 103 | ------- 104 | 105 | M to the I to the T. See the LICENSE file if you've never seen an MIT license before. 106 | 107 | Authors 108 | ------- 109 | 110 | - Carlos Martín (@carlosmn) 111 | - Vicent Martí (@vmg) 112 | 113 | -------------------------------------------------------------------------------- /object.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "errors" 9 | "fmt" 10 | "runtime" 11 | ) 12 | 13 | type ObjectType int 14 | 15 | const ( 16 | ObjectAny ObjectType = C.GIT_OBJECT_ANY 17 | ObjectInvalid ObjectType = C.GIT_OBJECT_INVALID 18 | ObjectCommit ObjectType = C.GIT_OBJECT_COMMIT 19 | ObjectTree ObjectType = C.GIT_OBJECT_TREE 20 | ObjectBlob ObjectType = C.GIT_OBJECT_BLOB 21 | ObjectTag ObjectType = C.GIT_OBJECT_TAG 22 | ) 23 | 24 | type Object struct { 25 | doNotCompare 26 | ptr *C.git_object 27 | repo *Repository 28 | } 29 | 30 | // Objecter lets us accept any kind of Git object in functions. 31 | type Objecter interface { 32 | AsObject() *Object 33 | } 34 | 35 | func (t ObjectType) String() string { 36 | switch t { 37 | case ObjectAny: 38 | return "Any" 39 | case ObjectInvalid: 40 | return "Invalid" 41 | case ObjectCommit: 42 | return "Commit" 43 | case ObjectTree: 44 | return "Tree" 45 | case ObjectBlob: 46 | return "Blob" 47 | case ObjectTag: 48 | return "Tag" 49 | } 50 | // Never reached 51 | return "" 52 | } 53 | 54 | func (o *Object) Id() *Oid { 55 | ret := newOidFromC(C.git_object_id(o.ptr)) 56 | runtime.KeepAlive(o) 57 | return ret 58 | } 59 | 60 | func (o *Object) ShortId() (string, error) { 61 | resultBuf := C.git_buf{} 62 | 63 | runtime.LockOSThread() 64 | defer runtime.UnlockOSThread() 65 | 66 | ecode := C.git_object_short_id(&resultBuf, o.ptr) 67 | runtime.KeepAlive(o) 68 | if ecode < 0 { 69 | return "", MakeGitError(ecode) 70 | } 71 | defer C.git_buf_dispose(&resultBuf) 72 | return C.GoString(resultBuf.ptr), nil 73 | } 74 | 75 | func (o *Object) Type() ObjectType { 76 | ret := ObjectType(C.git_object_type(o.ptr)) 77 | runtime.KeepAlive(o) 78 | return ret 79 | } 80 | 81 | // Owner returns a weak reference to the repository which owns this object. 82 | // This won't keep the underlying repository alive, but it should still be 83 | // Freed. 84 | func (o *Object) Owner() *Repository { 85 | repo := newRepositoryFromC(C.git_object_owner(o.ptr)) 86 | runtime.KeepAlive(o) 87 | repo.weak = true 88 | return repo 89 | } 90 | 91 | func dupObject(obj *Object, kind ObjectType) (*C.git_object, error) { 92 | if obj.Type() != kind { 93 | return nil, errors.New(fmt.Sprintf("object is not a %v", kind)) 94 | } 95 | 96 | var cobj *C.git_object 97 | 98 | runtime.LockOSThread() 99 | defer runtime.UnlockOSThread() 100 | 101 | err := C.git_object_dup(&cobj, obj.ptr) 102 | runtime.KeepAlive(obj) 103 | if err < 0 { 104 | return nil, MakeGitError(err) 105 | } 106 | 107 | return cobj, nil 108 | } 109 | 110 | func allocTree(ptr *C.git_tree, repo *Repository) *Tree { 111 | tree := &Tree{ 112 | Object: Object{ 113 | ptr: (*C.git_object)(ptr), 114 | repo: repo, 115 | }, 116 | cast_ptr: ptr, 117 | } 118 | runtime.SetFinalizer(tree, (*Tree).Free) 119 | 120 | return tree 121 | } 122 | 123 | func (o *Object) AsTree() (*Tree, error) { 124 | cobj, err := dupObject(o, ObjectTree) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | return allocTree((*C.git_tree)(cobj), o.repo), nil 130 | } 131 | 132 | func allocCommit(ptr *C.git_commit, repo *Repository) *Commit { 133 | commit := &Commit{ 134 | Object: Object{ 135 | ptr: (*C.git_object)(ptr), 136 | repo: repo, 137 | }, 138 | cast_ptr: ptr, 139 | } 140 | runtime.SetFinalizer(commit, (*Commit).Free) 141 | 142 | return commit 143 | } 144 | 145 | func (o *Object) AsCommit() (*Commit, error) { 146 | cobj, err := dupObject(o, ObjectCommit) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | return allocCommit((*C.git_commit)(cobj), o.repo), nil 152 | } 153 | 154 | func allocBlob(ptr *C.git_blob, repo *Repository) *Blob { 155 | blob := &Blob{ 156 | Object: Object{ 157 | ptr: (*C.git_object)(ptr), 158 | repo: repo, 159 | }, 160 | cast_ptr: ptr, 161 | } 162 | runtime.SetFinalizer(blob, (*Blob).Free) 163 | 164 | return blob 165 | } 166 | 167 | func (o *Object) AsBlob() (*Blob, error) { 168 | cobj, err := dupObject(o, ObjectBlob) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | return allocBlob((*C.git_blob)(cobj), o.repo), nil 174 | } 175 | 176 | func allocTag(ptr *C.git_tag, repo *Repository) *Tag { 177 | tag := &Tag{ 178 | Object: Object{ 179 | ptr: (*C.git_object)(ptr), 180 | repo: repo, 181 | }, 182 | cast_ptr: ptr, 183 | } 184 | runtime.SetFinalizer(tag, (*Tag).Free) 185 | 186 | return tag 187 | } 188 | 189 | func (o *Object) AsTag() (*Tag, error) { 190 | cobj, err := dupObject(o, ObjectTag) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | return allocTag((*C.git_tag)(cobj), o.repo), nil 196 | } 197 | 198 | func (o *Object) Free() { 199 | runtime.SetFinalizer(o, nil) 200 | C.git_object_free(o.ptr) 201 | } 202 | 203 | // Peel recursively peels an object until an object of the specified type is met. 204 | // 205 | // If the query cannot be satisfied due to the object model, ErrorCodeInvalidSpec 206 | // will be returned (e.g. trying to peel a blob to a tree). 207 | // 208 | // If you pass ObjectAny as the target type, then the object will be peeled 209 | // until the type changes. A tag will be peeled until the referenced object 210 | // is no longer a tag, and a commit will be peeled to a tree. Any other object 211 | // type will return ErrorCodeInvalidSpec. 212 | // 213 | // If peeling a tag we discover an object which cannot be peeled to the target 214 | // type due to the object model, an error will be returned. 215 | func (o *Object) Peel(t ObjectType) (*Object, error) { 216 | var cobj *C.git_object 217 | 218 | runtime.LockOSThread() 219 | defer runtime.UnlockOSThread() 220 | 221 | err := C.git_object_peel(&cobj, o.ptr, C.git_object_t(t)) 222 | runtime.KeepAlive(o) 223 | if err < 0 { 224 | return nil, MakeGitError(err) 225 | } 226 | 227 | return allocObject(cobj, o.repo), nil 228 | } 229 | 230 | func allocObject(cobj *C.git_object, repo *Repository) *Object { 231 | obj := &Object{ 232 | ptr: cobj, 233 | repo: repo, 234 | } 235 | runtime.SetFinalizer(obj, (*Object).Free) 236 | 237 | return obj 238 | } 239 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | 8 | import ( 9 | "errors" 10 | "runtime" 11 | "unsafe" 12 | ) 13 | 14 | type Status int 15 | 16 | const ( 17 | StatusCurrent Status = C.GIT_STATUS_CURRENT 18 | StatusIndexNew Status = C.GIT_STATUS_INDEX_NEW 19 | StatusIndexModified Status = C.GIT_STATUS_INDEX_MODIFIED 20 | StatusIndexDeleted Status = C.GIT_STATUS_INDEX_DELETED 21 | StatusIndexRenamed Status = C.GIT_STATUS_INDEX_RENAMED 22 | StatusIndexTypeChange Status = C.GIT_STATUS_INDEX_TYPECHANGE 23 | StatusWtNew Status = C.GIT_STATUS_WT_NEW 24 | StatusWtModified Status = C.GIT_STATUS_WT_MODIFIED 25 | StatusWtDeleted Status = C.GIT_STATUS_WT_DELETED 26 | StatusWtTypeChange Status = C.GIT_STATUS_WT_TYPECHANGE 27 | StatusWtRenamed Status = C.GIT_STATUS_WT_RENAMED 28 | StatusIgnored Status = C.GIT_STATUS_IGNORED 29 | StatusConflicted Status = C.GIT_STATUS_CONFLICTED 30 | ) 31 | 32 | type StatusEntry struct { 33 | Status Status 34 | HeadToIndex DiffDelta 35 | IndexToWorkdir DiffDelta 36 | } 37 | 38 | func statusEntryFromC(statusEntry *C.git_status_entry) StatusEntry { 39 | var headToIndex DiffDelta = DiffDelta{} 40 | var indexToWorkdir DiffDelta = DiffDelta{} 41 | 42 | // Based on the libgit2 status example, head_to_index can be null in some cases 43 | if statusEntry.head_to_index != nil { 44 | headToIndex = diffDeltaFromC(statusEntry.head_to_index) 45 | } 46 | if statusEntry.index_to_workdir != nil { 47 | indexToWorkdir = diffDeltaFromC(statusEntry.index_to_workdir) 48 | } 49 | 50 | return StatusEntry{ 51 | Status: Status(statusEntry.status), 52 | HeadToIndex: headToIndex, 53 | IndexToWorkdir: indexToWorkdir, 54 | } 55 | } 56 | 57 | type StatusList struct { 58 | doNotCompare 59 | ptr *C.git_status_list 60 | r *Repository 61 | } 62 | 63 | func newStatusListFromC(ptr *C.git_status_list, r *Repository) *StatusList { 64 | if ptr == nil { 65 | return nil 66 | } 67 | 68 | statusList := &StatusList{ 69 | ptr: ptr, 70 | r: r, 71 | } 72 | 73 | runtime.SetFinalizer(statusList, (*StatusList).Free) 74 | return statusList 75 | } 76 | 77 | func (statusList *StatusList) Free() { 78 | if statusList.ptr == nil { 79 | return 80 | } 81 | runtime.SetFinalizer(statusList, nil) 82 | C.git_status_list_free(statusList.ptr) 83 | statusList.ptr = nil 84 | } 85 | 86 | func (statusList *StatusList) ByIndex(index int) (StatusEntry, error) { 87 | if statusList.ptr == nil { 88 | return StatusEntry{}, ErrInvalid 89 | } 90 | ptr := C.git_status_byindex(statusList.ptr, C.size_t(index)) 91 | if ptr == nil { 92 | return StatusEntry{}, errors.New("index out of Bounds") 93 | } 94 | entry := statusEntryFromC(ptr) 95 | runtime.KeepAlive(statusList) 96 | 97 | return entry, nil 98 | } 99 | 100 | func (statusList *StatusList) EntryCount() (int, error) { 101 | if statusList.ptr == nil { 102 | return -1, ErrInvalid 103 | } 104 | ret := int(C.git_status_list_entrycount(statusList.ptr)) 105 | runtime.KeepAlive(statusList) 106 | 107 | return ret, nil 108 | } 109 | 110 | type StatusOpt int 111 | 112 | const ( 113 | StatusOptIncludeUntracked StatusOpt = C.GIT_STATUS_OPT_INCLUDE_UNTRACKED 114 | StatusOptIncludeIgnored StatusOpt = C.GIT_STATUS_OPT_INCLUDE_IGNORED 115 | StatusOptIncludeUnmodified StatusOpt = C.GIT_STATUS_OPT_INCLUDE_UNMODIFIED 116 | StatusOptExcludeSubmodules StatusOpt = C.GIT_STATUS_OPT_EXCLUDE_SUBMODULES 117 | StatusOptRecurseUntrackedDirs StatusOpt = C.GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS 118 | StatusOptDisablePathspecMatch StatusOpt = C.GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH 119 | StatusOptRecurseIgnoredDirs StatusOpt = C.GIT_STATUS_OPT_RECURSE_IGNORED_DIRS 120 | StatusOptRenamesHeadToIndex StatusOpt = C.GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX 121 | StatusOptRenamesIndexToWorkdir StatusOpt = C.GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR 122 | StatusOptSortCaseSensitively StatusOpt = C.GIT_STATUS_OPT_SORT_CASE_SENSITIVELY 123 | StatusOptSortCaseInsensitively StatusOpt = C.GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY 124 | StatusOptRenamesFromRewrites StatusOpt = C.GIT_STATUS_OPT_RENAMES_FROM_REWRITES 125 | StatusOptNoRefresh StatusOpt = C.GIT_STATUS_OPT_NO_REFRESH 126 | StatusOptUpdateIndex StatusOpt = C.GIT_STATUS_OPT_UPDATE_INDEX 127 | ) 128 | 129 | type StatusShow int 130 | 131 | const ( 132 | StatusShowIndexAndWorkdir StatusShow = C.GIT_STATUS_SHOW_INDEX_AND_WORKDIR 133 | StatusShowIndexOnly StatusShow = C.GIT_STATUS_SHOW_INDEX_ONLY 134 | StatusShowWorkdirOnly StatusShow = C.GIT_STATUS_SHOW_WORKDIR_ONLY 135 | ) 136 | 137 | type StatusOptions struct { 138 | Show StatusShow 139 | Flags StatusOpt 140 | Pathspec []string 141 | } 142 | 143 | func (v *Repository) StatusList(opts *StatusOptions) (*StatusList, error) { 144 | var ptr *C.git_status_list 145 | var copts *C.git_status_options 146 | 147 | if opts != nil { 148 | cpathspec := C.git_strarray{} 149 | if opts.Pathspec != nil { 150 | cpathspec.count = C.size_t(len(opts.Pathspec)) 151 | cpathspec.strings = makeCStringsFromStrings(opts.Pathspec) 152 | defer freeStrarray(&cpathspec) 153 | } 154 | 155 | copts = &C.git_status_options{ 156 | version: C.GIT_STATUS_OPTIONS_VERSION, 157 | show: C.git_status_show_t(opts.Show), 158 | flags: C.uint(opts.Flags), 159 | pathspec: cpathspec, 160 | } 161 | } else { 162 | copts = &C.git_status_options{} 163 | ret := C.git_status_options_init(copts, C.GIT_STATUS_OPTIONS_VERSION) 164 | if ret < 0 { 165 | return nil, MakeGitError(ret) 166 | } 167 | } 168 | 169 | runtime.LockOSThread() 170 | defer runtime.UnlockOSThread() 171 | 172 | ret := C.git_status_list_new(&ptr, v.ptr, copts) 173 | if ret < 0 { 174 | return nil, MakeGitError(ret) 175 | } 176 | 177 | return newStatusListFromC(ptr, v), nil 178 | } 179 | 180 | func (v *Repository) StatusFile(path string) (Status, error) { 181 | var statusFlags C.uint 182 | cPath := C.CString(path) 183 | defer C.free(unsafe.Pointer(cPath)) 184 | 185 | runtime.LockOSThread() 186 | defer runtime.UnlockOSThread() 187 | 188 | ret := C.git_status_file(&statusFlags, v.ptr, cPath) 189 | runtime.KeepAlive(v) 190 | if ret < 0 { 191 | return 0, MakeGitError(ret) 192 | } 193 | return Status(statusFlags), nil 194 | } 195 | -------------------------------------------------------------------------------- /note.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | 8 | import ( 9 | "runtime" 10 | "unsafe" 11 | ) 12 | 13 | // This object represents the possible operations which can be 14 | // performed on the collection of notes for a repository. 15 | type NoteCollection struct { 16 | doNotCompare 17 | repo *Repository 18 | } 19 | 20 | // Create adds a note for an object 21 | func (c *NoteCollection) Create( 22 | ref string, author, committer *Signature, id *Oid, 23 | note string, force bool) (*Oid, error) { 24 | 25 | oid := new(Oid) 26 | 27 | var cref *C.char 28 | if ref == "" { 29 | cref = nil 30 | } else { 31 | cref = C.CString(ref) 32 | defer C.free(unsafe.Pointer(cref)) 33 | } 34 | 35 | authorSig, err := author.toC() 36 | if err != nil { 37 | return nil, err 38 | } 39 | defer C.git_signature_free(authorSig) 40 | 41 | committerSig, err := committer.toC() 42 | if err != nil { 43 | return nil, err 44 | } 45 | defer C.git_signature_free(committerSig) 46 | 47 | cnote := C.CString(note) 48 | defer C.free(unsafe.Pointer(cnote)) 49 | 50 | runtime.LockOSThread() 51 | defer runtime.UnlockOSThread() 52 | 53 | ret := C.git_note_create( 54 | oid.toC(), c.repo.ptr, cref, authorSig, 55 | committerSig, id.toC(), cnote, cbool(force)) 56 | runtime.KeepAlive(c) 57 | runtime.KeepAlive(id) 58 | if ret < 0 { 59 | return nil, MakeGitError(ret) 60 | } 61 | return oid, nil 62 | } 63 | 64 | // Read reads the note for an object 65 | func (c *NoteCollection) Read(ref string, id *Oid) (*Note, error) { 66 | var cref *C.char 67 | if ref == "" { 68 | cref = nil 69 | } else { 70 | cref = C.CString(ref) 71 | defer C.free(unsafe.Pointer(cref)) 72 | } 73 | 74 | runtime.LockOSThread() 75 | defer runtime.UnlockOSThread() 76 | 77 | var ptr *C.git_note 78 | ret := C.git_note_read(&ptr, c.repo.ptr, cref, id.toC()) 79 | runtime.KeepAlive(c) 80 | runtime.KeepAlive(id) 81 | if ret < 0 { 82 | return nil, MakeGitError(ret) 83 | } 84 | 85 | return newNoteFromC(ptr, c.repo), nil 86 | } 87 | 88 | // Remove removes the note for an object 89 | func (c *NoteCollection) Remove(ref string, author, committer *Signature, id *Oid) error { 90 | var cref *C.char 91 | if ref == "" { 92 | cref = nil 93 | } else { 94 | cref = C.CString(ref) 95 | defer C.free(unsafe.Pointer(cref)) 96 | } 97 | 98 | authorSig, err := author.toC() 99 | if err != nil { 100 | return err 101 | } 102 | defer C.git_signature_free(authorSig) 103 | 104 | committerSig, err := committer.toC() 105 | if err != nil { 106 | return err 107 | } 108 | defer C.git_signature_free(committerSig) 109 | 110 | runtime.LockOSThread() 111 | defer runtime.UnlockOSThread() 112 | 113 | ret := C.git_note_remove(c.repo.ptr, cref, authorSig, committerSig, id.toC()) 114 | runtime.KeepAlive(c) 115 | runtime.KeepAlive(id) 116 | if ret < 0 { 117 | return MakeGitError(ret) 118 | } 119 | return nil 120 | } 121 | 122 | // DefaultRef returns the default notes reference for a repository 123 | func (c *NoteCollection) DefaultRef() (string, error) { 124 | buf := C.git_buf{} 125 | 126 | runtime.LockOSThread() 127 | defer runtime.UnlockOSThread() 128 | 129 | ecode := C.git_note_default_ref(&buf, c.repo.ptr) 130 | runtime.KeepAlive(c) 131 | if ecode < 0 { 132 | return "", MakeGitError(ecode) 133 | } 134 | 135 | ret := C.GoString(buf.ptr) 136 | C.git_buf_dispose(&buf) 137 | 138 | return ret, nil 139 | } 140 | 141 | // Note 142 | type Note struct { 143 | doNotCompare 144 | ptr *C.git_note 145 | r *Repository 146 | } 147 | 148 | func newNoteFromC(ptr *C.git_note, r *Repository) *Note { 149 | note := &Note{ptr: ptr, r: r} 150 | runtime.SetFinalizer(note, (*Note).Free) 151 | return note 152 | } 153 | 154 | // Free frees a git_note object 155 | func (n *Note) Free() error { 156 | if n.ptr == nil { 157 | return ErrInvalid 158 | } 159 | runtime.SetFinalizer(n, nil) 160 | C.git_note_free(n.ptr) 161 | n.ptr = nil 162 | return nil 163 | } 164 | 165 | // Author returns the signature of the note author 166 | func (n *Note) Author() *Signature { 167 | ptr := C.git_note_author(n.ptr) 168 | return newSignatureFromC(ptr) 169 | } 170 | 171 | // Id returns the note object's id 172 | func (n *Note) Id() *Oid { 173 | ptr := C.git_note_id(n.ptr) 174 | runtime.KeepAlive(n) 175 | return newOidFromC(ptr) 176 | } 177 | 178 | // Committer returns the signature of the note committer 179 | func (n *Note) Committer() *Signature { 180 | ptr := C.git_note_committer(n.ptr) 181 | runtime.KeepAlive(n) 182 | return newSignatureFromC(ptr) 183 | } 184 | 185 | // Message returns the note message 186 | func (n *Note) Message() string { 187 | ret := C.GoString(C.git_note_message(n.ptr)) 188 | runtime.KeepAlive(n) 189 | return ret 190 | } 191 | 192 | // NoteIterator 193 | type NoteIterator struct { 194 | doNotCompare 195 | ptr *C.git_note_iterator 196 | r *Repository 197 | } 198 | 199 | // NewNoteIterator creates a new iterator for notes 200 | func (repo *Repository) NewNoteIterator(ref string) (*NoteIterator, error) { 201 | var cref *C.char 202 | if ref == "" { 203 | cref = nil 204 | } else { 205 | cref = C.CString(ref) 206 | defer C.free(unsafe.Pointer(cref)) 207 | } 208 | 209 | var ptr *C.git_note_iterator 210 | 211 | runtime.LockOSThread() 212 | defer runtime.UnlockOSThread() 213 | 214 | ret := C.git_note_iterator_new(&ptr, repo.ptr, cref) 215 | runtime.KeepAlive(repo) 216 | if ret < 0 { 217 | return nil, MakeGitError(ret) 218 | } 219 | 220 | iter := &NoteIterator{ptr: ptr, r: repo} 221 | runtime.SetFinalizer(iter, (*NoteIterator).Free) 222 | return iter, nil 223 | } 224 | 225 | // Free frees the note interator 226 | func (v *NoteIterator) Free() { 227 | runtime.SetFinalizer(v, nil) 228 | C.git_note_iterator_free(v.ptr) 229 | } 230 | 231 | // Next returns the current item (note id & annotated id) and advances the 232 | // iterator internally to the next item 233 | func (it *NoteIterator) Next() (noteId, annotatedId *Oid, err error) { 234 | noteId, annotatedId = new(Oid), new(Oid) 235 | 236 | runtime.LockOSThread() 237 | defer runtime.UnlockOSThread() 238 | 239 | ret := C.git_note_next(noteId.toC(), annotatedId.toC(), it.ptr) 240 | runtime.KeepAlive(noteId) 241 | runtime.KeepAlive(annotatedId) 242 | runtime.KeepAlive(it) 243 | if ret < 0 { 244 | err = MakeGitError(ret) 245 | } 246 | return 247 | } 248 | -------------------------------------------------------------------------------- /odb_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | "testing" 12 | ) 13 | 14 | func TestOdbRead(t *testing.T) { 15 | t.Parallel() 16 | repo := createTestRepo(t) 17 | defer cleanupTestRepo(t, repo) 18 | 19 | _, _ = seedTestRepo(t, repo) 20 | odb, err := repo.Odb() 21 | if err != nil { 22 | t.Fatalf("Odb: %v", err) 23 | } 24 | data := []byte("hello") 25 | id, err := odb.Write(data, ObjectBlob) 26 | if err != nil { 27 | t.Fatalf("odb.Write: %v", err) 28 | } 29 | 30 | sz, typ, err := odb.ReadHeader(id) 31 | if err != nil { 32 | t.Fatalf("ReadHeader: %v", err) 33 | } 34 | 35 | if sz != uint64(len(data)) { 36 | t.Errorf("ReadHeader got size %d, want %d", sz, len(data)) 37 | } 38 | if typ != ObjectBlob { 39 | t.Errorf("ReadHeader got object type %s", typ) 40 | } 41 | 42 | obj, err := odb.Read(id) 43 | if err != nil { 44 | t.Fatalf("Read: %v", err) 45 | } 46 | if !bytes.Equal(obj.Data(), data) { 47 | t.Errorf("Read got wrong data") 48 | } 49 | if sz := obj.Len(); sz != uint64(len(data)) { 50 | t.Errorf("Read got size %d, want %d", sz, len(data)) 51 | } 52 | if typ := obj.Type(); typ != ObjectBlob { 53 | t.Errorf("Read got object type %s", typ) 54 | } 55 | } 56 | 57 | func TestOdbStream(t *testing.T) { 58 | t.Parallel() 59 | repo := createTestRepo(t) 60 | defer cleanupTestRepo(t, repo) 61 | 62 | _, _ = seedTestRepo(t, repo) 63 | 64 | odb, err := repo.Odb() 65 | checkFatal(t, err) 66 | 67 | str := "hello, world!" 68 | 69 | writeStream, err := odb.NewWriteStream(int64(len(str)), ObjectBlob) 70 | checkFatal(t, err) 71 | n, err := io.WriteString(writeStream, str) 72 | checkFatal(t, err) 73 | if n != len(str) { 74 | t.Fatalf("Bad write length %v != %v", n, len(str)) 75 | } 76 | 77 | err = writeStream.Close() 78 | checkFatal(t, err) 79 | 80 | expectedId, err := NewOid("30f51a3fba5274d53522d0f19748456974647b4f") 81 | checkFatal(t, err) 82 | if writeStream.Id.Cmp(expectedId) != 0 { 83 | t.Fatal("Wrong data written") 84 | } 85 | 86 | readStream, err := odb.NewReadStream(&writeStream.Id) 87 | checkFatal(t, err) 88 | data, err := ioutil.ReadAll(readStream) 89 | if str != string(data) { 90 | t.Fatalf("Wrong data read %v != %v", str, string(data)) 91 | } 92 | } 93 | 94 | func TestOdbHash(t *testing.T) { 95 | t.Parallel() 96 | repo := createTestRepo(t) 97 | defer cleanupTestRepo(t, repo) 98 | 99 | _, _ = seedTestRepo(t, repo) 100 | 101 | odb, err := repo.Odb() 102 | checkFatal(t, err) 103 | 104 | str := `tree 115fcae49287c82eb55bb275cbbd4556fbed72b7 105 | parent 66e1c476199ebcd3e304659992233132c5a52c6c 106 | author John Doe 1390682018 +0000 107 | committer John Doe 1390682018 +0000 108 | 109 | Initial commit.` 110 | 111 | for _, data := range [][]byte{[]byte(str), doublePointerBytes()} { 112 | oid, err := odb.Hash(data, ObjectCommit) 113 | checkFatal(t, err) 114 | 115 | coid, err := odb.Write(data, ObjectCommit) 116 | checkFatal(t, err) 117 | 118 | if oid.Cmp(coid) != 0 { 119 | t.Fatal("Hash and write Oids are different") 120 | } 121 | } 122 | } 123 | 124 | func TestOdbForeach(t *testing.T) { 125 | t.Parallel() 126 | repo := createTestRepo(t) 127 | defer cleanupTestRepo(t, repo) 128 | 129 | _, _ = seedTestRepo(t, repo) 130 | 131 | odb, err := repo.Odb() 132 | checkFatal(t, err) 133 | 134 | expect := 3 135 | count := 0 136 | err = odb.ForEach(func(id *Oid) error { 137 | count++ 138 | return nil 139 | }) 140 | 141 | checkFatal(t, err) 142 | if count != expect { 143 | t.Fatalf("Expected %v objects, got %v", expect, count) 144 | } 145 | 146 | expect = 1 147 | count = 0 148 | to_return := errors.New("not really an error") 149 | err = odb.ForEach(func(id *Oid) error { 150 | count++ 151 | return to_return 152 | }) 153 | 154 | if err != to_return { 155 | t.Fatalf("Odb.ForEach() did not return the expected error, got %v", err) 156 | } 157 | } 158 | 159 | func TestOdbWritepack(t *testing.T) { 160 | t.Parallel() 161 | repo := createTestRepo(t) 162 | defer cleanupTestRepo(t, repo) 163 | 164 | _, _ = seedTestRepo(t, repo) 165 | 166 | odb, err := repo.Odb() 167 | checkFatal(t, err) 168 | 169 | var finalStats TransferProgress 170 | writepack, err := odb.NewWritePack(func(stats TransferProgress) error { 171 | finalStats = stats 172 | return nil 173 | }) 174 | checkFatal(t, err) 175 | defer writepack.Free() 176 | 177 | _, err = writepack.Write(outOfOrderPack) 178 | checkFatal(t, err) 179 | err = writepack.Commit() 180 | checkFatal(t, err) 181 | 182 | if finalStats.TotalObjects != 3 { 183 | t.Errorf("mismatched transferred objects, expected 3, got %v", finalStats.TotalObjects) 184 | } 185 | if finalStats.ReceivedObjects != 3 { 186 | t.Errorf("mismatched received objects, expected 3, got %v", finalStats.ReceivedObjects) 187 | } 188 | if finalStats.IndexedObjects != 3 { 189 | t.Errorf("mismatched indexed objects, expected 3, got %v", finalStats.IndexedObjects) 190 | } 191 | } 192 | 193 | func TestOdbBackendLoose(t *testing.T) { 194 | t.Parallel() 195 | repo := createTestRepo(t) 196 | defer cleanupTestRepo(t, repo) 197 | 198 | _, _ = seedTestRepo(t, repo) 199 | 200 | odb, err := repo.Odb() 201 | checkFatal(t, err) 202 | 203 | looseObjectsDir, err := ioutil.TempDir("", fmt.Sprintf("loose_objects_%s", path.Base(repo.Path()))) 204 | checkFatal(t, err) 205 | defer os.RemoveAll(looseObjectsDir) 206 | 207 | looseObjectsBackend, err := NewOdbBackendLoose(looseObjectsDir, -1, false, 0, 0) 208 | checkFatal(t, err) 209 | if err := odb.AddBackend(looseObjectsBackend, 999); err != nil { 210 | looseObjectsBackend.Free() 211 | checkFatal(t, err) 212 | } 213 | 214 | str := "hello, world!" 215 | 216 | writeStream, err := odb.NewWriteStream(int64(len(str)), ObjectBlob) 217 | checkFatal(t, err) 218 | n, err := io.WriteString(writeStream, str) 219 | checkFatal(t, err) 220 | if n != len(str) { 221 | t.Fatalf("Bad write length %v != %v", n, len(str)) 222 | } 223 | 224 | err = writeStream.Close() 225 | checkFatal(t, err) 226 | 227 | expectedId, err := NewOid("30f51a3fba5274d53522d0f19748456974647b4f") 228 | checkFatal(t, err) 229 | if !writeStream.Id.Equal(expectedId) { 230 | t.Fatalf("writeStream.id = %v; want %v", writeStream.Id, expectedId) 231 | } 232 | 233 | _, err = os.Stat(path.Join(looseObjectsDir, expectedId.String()[:2], expectedId.String()[2:])) 234 | checkFatal(t, err) 235 | } 236 | -------------------------------------------------------------------------------- /commit.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | 6 | extern int _go_git_treewalk(git_tree *tree, git_treewalk_mode mode, void *ptr); 7 | */ 8 | import "C" 9 | 10 | import ( 11 | "runtime" 12 | "unsafe" 13 | ) 14 | 15 | // MessageEncoding is the encoding of commit messages. 16 | type MessageEncoding string 17 | 18 | const ( 19 | // MessageEncodingUTF8 is the default message encoding. 20 | MessageEncodingUTF8 MessageEncoding = "UTF-8" 21 | ) 22 | 23 | // Commit 24 | type Commit struct { 25 | doNotCompare 26 | Object 27 | cast_ptr *C.git_commit 28 | } 29 | 30 | func (c *Commit) AsObject() *Object { 31 | return &c.Object 32 | } 33 | 34 | func (c *Commit) Message() string { 35 | ret := C.GoString(C.git_commit_message(c.cast_ptr)) 36 | runtime.KeepAlive(c) 37 | return ret 38 | } 39 | 40 | func (c *Commit) MessageEncoding() MessageEncoding { 41 | ptr := C.git_commit_message_encoding(c.cast_ptr) 42 | if ptr == nil { 43 | return MessageEncodingUTF8 44 | } 45 | ret := C.GoString(ptr) 46 | runtime.KeepAlive(c) 47 | return MessageEncoding(ret) 48 | } 49 | 50 | func (c *Commit) RawMessage() string { 51 | ret := C.GoString(C.git_commit_message_raw(c.cast_ptr)) 52 | runtime.KeepAlive(c) 53 | return ret 54 | } 55 | 56 | // RawHeader gets the full raw text of the commit header. 57 | func (c *Commit) RawHeader() string { 58 | ret := C.GoString(C.git_commit_raw_header(c.cast_ptr)) 59 | runtime.KeepAlive(c) 60 | return ret 61 | } 62 | 63 | // ContentToSign returns the content that will be passed to a signing function for this commit 64 | func (c *Commit) ContentToSign() string { 65 | return c.RawHeader() + "\n" + c.RawMessage() 66 | } 67 | 68 | // CommitSigningCallback defines a function type that takes some data to sign and returns (signature, signature_field, error) 69 | type CommitSigningCallback func(string) (signature, signatureField string, err error) 70 | 71 | // CommitCreateCallback defines a function type that is called when another 72 | // function is going to create commits (for example, Rebase) to allow callers 73 | // to override the commit creation behavior. For example, users may wish to 74 | // sign commits by providing this information to Repository.CreateCommitBuffer, 75 | // signing that buffer, then calling Repository.CreateCommitWithSignature. 76 | type CommitCreateCallback func( 77 | author, committer *Signature, 78 | messageEncoding MessageEncoding, 79 | message string, 80 | tree *Tree, 81 | parents ...*Commit, 82 | ) (oid *Oid, err error) 83 | 84 | // WithSignatureUsing creates a new signed commit from this one using the given signing callback 85 | func (c *Commit) WithSignatureUsing(f CommitSigningCallback) (*Oid, error) { 86 | signature, signatureField, err := f(c.ContentToSign()) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | return c.WithSignature(signature, signatureField) 92 | } 93 | 94 | // WithSignature creates a new signed commit from the given signature and signature field 95 | func (c *Commit) WithSignature(signature string, signatureField string) (*Oid, error) { 96 | return c.Owner().CreateCommitWithSignature( 97 | c.ContentToSign(), 98 | signature, 99 | signatureField, 100 | ) 101 | } 102 | 103 | func (c *Commit) ExtractSignature() (string, string, error) { 104 | 105 | var c_signed C.git_buf 106 | defer C.git_buf_dispose(&c_signed) 107 | 108 | var c_signature C.git_buf 109 | defer C.git_buf_dispose(&c_signature) 110 | 111 | oid := c.Id() 112 | repo := C.git_commit_owner(c.cast_ptr) 113 | 114 | runtime.LockOSThread() 115 | defer runtime.UnlockOSThread() 116 | ret := C.git_commit_extract_signature(&c_signature, &c_signed, repo, oid.toC(), nil) 117 | runtime.KeepAlive(oid) 118 | if ret < 0 { 119 | return "", "", MakeGitError(ret) 120 | } else { 121 | return C.GoString(c_signature.ptr), C.GoString(c_signed.ptr), nil 122 | } 123 | 124 | } 125 | 126 | func (c *Commit) Summary() string { 127 | ret := C.GoString(C.git_commit_summary(c.cast_ptr)) 128 | runtime.KeepAlive(c) 129 | return ret 130 | } 131 | 132 | func (c *Commit) Tree() (*Tree, error) { 133 | var ptr *C.git_tree 134 | 135 | runtime.LockOSThread() 136 | defer runtime.UnlockOSThread() 137 | 138 | err := C.git_commit_tree(&ptr, c.cast_ptr) 139 | runtime.KeepAlive(c) 140 | if err < 0 { 141 | return nil, MakeGitError(err) 142 | } 143 | 144 | return allocTree(ptr, c.repo), nil 145 | } 146 | 147 | func (c *Commit) TreeId() *Oid { 148 | ret := newOidFromC(C.git_commit_tree_id(c.cast_ptr)) 149 | runtime.KeepAlive(c) 150 | return ret 151 | } 152 | 153 | func (c *Commit) Author() *Signature { 154 | cast_ptr := C.git_commit_author(c.cast_ptr) 155 | ret := newSignatureFromC(cast_ptr) 156 | runtime.KeepAlive(c) 157 | return ret 158 | } 159 | 160 | func (c *Commit) Committer() *Signature { 161 | cast_ptr := C.git_commit_committer(c.cast_ptr) 162 | ret := newSignatureFromC(cast_ptr) 163 | runtime.KeepAlive(c) 164 | return ret 165 | } 166 | 167 | func (c *Commit) Parent(n uint) *Commit { 168 | var cobj *C.git_commit 169 | ret := C.git_commit_parent(&cobj, c.cast_ptr, C.uint(n)) 170 | if ret != 0 { 171 | return nil 172 | } 173 | 174 | parent := allocCommit(cobj, c.repo) 175 | runtime.KeepAlive(c) 176 | return parent 177 | } 178 | 179 | func (c *Commit) ParentId(n uint) *Oid { 180 | ret := newOidFromC(C.git_commit_parent_id(c.cast_ptr, C.uint(n))) 181 | runtime.KeepAlive(c) 182 | return ret 183 | } 184 | 185 | func (c *Commit) ParentCount() uint { 186 | ret := uint(C.git_commit_parentcount(c.cast_ptr)) 187 | runtime.KeepAlive(c) 188 | return ret 189 | } 190 | 191 | func (c *Commit) Amend(refname string, author, committer *Signature, message string, tree *Tree) (*Oid, error) { 192 | var cref *C.char 193 | if refname == "" { 194 | cref = nil 195 | } else { 196 | cref = C.CString(refname) 197 | defer C.free(unsafe.Pointer(cref)) 198 | } 199 | 200 | cmsg := C.CString(message) 201 | defer C.free(unsafe.Pointer(cmsg)) 202 | 203 | runtime.LockOSThread() 204 | defer runtime.UnlockOSThread() 205 | 206 | authorSig, err := author.toC() 207 | if err != nil { 208 | return nil, err 209 | } 210 | defer C.git_signature_free(authorSig) 211 | 212 | committerSig, err := committer.toC() 213 | if err != nil { 214 | return nil, err 215 | } 216 | defer C.git_signature_free(committerSig) 217 | 218 | oid := new(Oid) 219 | 220 | cerr := C.git_commit_amend(oid.toC(), c.cast_ptr, cref, authorSig, committerSig, nil, cmsg, tree.cast_ptr) 221 | runtime.KeepAlive(oid) 222 | runtime.KeepAlive(c) 223 | runtime.KeepAlive(tree) 224 | if cerr < 0 { 225 | return nil, MakeGitError(cerr) 226 | } 227 | 228 | return oid, nil 229 | } 230 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "sync" 10 | ) 11 | 12 | // RegisterManagedHTTPTransport registers a Go-native implementation of an 13 | // HTTP/S transport that doesn't rely on any system libraries (e.g. 14 | // libopenssl/libmbedtls). 15 | // 16 | // If Shutdown or ReInit are called, make sure that the smart transports are 17 | // freed before it. 18 | func RegisterManagedHTTPTransport(protocol string) (*RegisteredSmartTransport, error) { 19 | return NewRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory) 20 | } 21 | 22 | func registerManagedHTTP() error { 23 | globalRegisteredSmartTransports.Lock() 24 | defer globalRegisteredSmartTransports.Unlock() 25 | 26 | for _, protocol := range []string{"http", "https"} { 27 | if _, ok := globalRegisteredSmartTransports.transports[protocol]; ok { 28 | continue 29 | } 30 | managed, err := newRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory, true) 31 | if err != nil { 32 | return fmt.Errorf("failed to register transport for %q: %v", protocol, err) 33 | } 34 | globalRegisteredSmartTransports.transports[protocol] = managed 35 | } 36 | return nil 37 | } 38 | 39 | func httpSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSubtransport, error) { 40 | var proxyFn func(*http.Request) (*url.URL, error) 41 | remoteConnectOpts, err := transport.SmartRemoteConnectOptions() 42 | if err != nil { 43 | return nil, err 44 | } 45 | switch remoteConnectOpts.ProxyOptions.Type { 46 | case ProxyTypeNone: 47 | proxyFn = nil 48 | case ProxyTypeAuto: 49 | proxyFn = http.ProxyFromEnvironment 50 | case ProxyTypeSpecified: 51 | parsedUrl, err := url.Parse(remoteConnectOpts.ProxyOptions.Url) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | proxyFn = http.ProxyURL(parsedUrl) 57 | } 58 | 59 | return &httpSmartSubtransport{ 60 | transport: transport, 61 | client: &http.Client{ 62 | Transport: &http.Transport{ 63 | Proxy: proxyFn, 64 | }, 65 | }, 66 | }, nil 67 | } 68 | 69 | type httpSmartSubtransport struct { 70 | transport *Transport 71 | client *http.Client 72 | } 73 | 74 | func (t *httpSmartSubtransport) Action(url string, action SmartServiceAction) (SmartSubtransportStream, error) { 75 | var req *http.Request 76 | var err error 77 | switch action { 78 | case SmartServiceActionUploadpackLs: 79 | req, err = http.NewRequest("GET", url+"/info/refs?service=git-upload-pack", nil) 80 | 81 | case SmartServiceActionUploadpack: 82 | req, err = http.NewRequest("POST", url+"/git-upload-pack", nil) 83 | if err != nil { 84 | break 85 | } 86 | req.Header.Set("Content-Type", "application/x-git-upload-pack-request") 87 | 88 | case SmartServiceActionReceivepackLs: 89 | req, err = http.NewRequest("GET", url+"/info/refs?service=git-receive-pack", nil) 90 | 91 | case SmartServiceActionReceivepack: 92 | req, err = http.NewRequest("POST", url+"/info/refs?service=git-upload-pack", nil) 93 | if err != nil { 94 | break 95 | } 96 | req.Header.Set("Content-Type", "application/x-git-receive-pack-request") 97 | 98 | default: 99 | err = errors.New("unknown action") 100 | } 101 | 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | req.Header.Set("User-Agent", "git/2.0 (git2go)") 107 | 108 | stream := newManagedHttpStream(t, req) 109 | if req.Method == "POST" { 110 | stream.recvReply.Add(1) 111 | stream.sendRequestBackground() 112 | } 113 | 114 | return stream, nil 115 | } 116 | 117 | func (t *httpSmartSubtransport) Close() error { 118 | return nil 119 | } 120 | 121 | func (t *httpSmartSubtransport) Free() { 122 | t.client = nil 123 | } 124 | 125 | type httpSmartSubtransportStream struct { 126 | owner *httpSmartSubtransport 127 | req *http.Request 128 | resp *http.Response 129 | reader *io.PipeReader 130 | writer *io.PipeWriter 131 | sentRequest bool 132 | recvReply sync.WaitGroup 133 | httpError error 134 | } 135 | 136 | func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request) *httpSmartSubtransportStream { 137 | r, w := io.Pipe() 138 | return &httpSmartSubtransportStream{ 139 | owner: owner, 140 | req: req, 141 | reader: r, 142 | writer: w, 143 | } 144 | } 145 | 146 | func (self *httpSmartSubtransportStream) Read(buf []byte) (int, error) { 147 | if !self.sentRequest { 148 | self.recvReply.Add(1) 149 | if err := self.sendRequest(); err != nil { 150 | return 0, err 151 | } 152 | } 153 | 154 | if err := self.writer.Close(); err != nil { 155 | return 0, err 156 | } 157 | 158 | self.recvReply.Wait() 159 | 160 | if self.httpError != nil { 161 | return 0, self.httpError 162 | } 163 | 164 | return self.resp.Body.Read(buf) 165 | } 166 | 167 | func (self *httpSmartSubtransportStream) Write(buf []byte) (int, error) { 168 | if self.httpError != nil { 169 | return 0, self.httpError 170 | } 171 | return self.writer.Write(buf) 172 | } 173 | 174 | func (self *httpSmartSubtransportStream) Free() { 175 | if self.resp != nil { 176 | self.resp.Body.Close() 177 | } 178 | } 179 | 180 | func (self *httpSmartSubtransportStream) sendRequestBackground() { 181 | go func() { 182 | self.httpError = self.sendRequest() 183 | }() 184 | self.sentRequest = true 185 | } 186 | 187 | func (self *httpSmartSubtransportStream) sendRequest() error { 188 | defer self.recvReply.Done() 189 | self.resp = nil 190 | 191 | var resp *http.Response 192 | var err error 193 | var userName string 194 | var password string 195 | for { 196 | req := &http.Request{ 197 | Method: self.req.Method, 198 | URL: self.req.URL, 199 | Header: self.req.Header, 200 | } 201 | if req.Method == "POST" { 202 | req.Body = self.reader 203 | req.ContentLength = -1 204 | } 205 | 206 | if userName != "" && password != "" { 207 | req.SetBasicAuth(userName, password) 208 | } 209 | resp, err = http.DefaultClient.Do(req) 210 | if err != nil { 211 | return err 212 | } 213 | 214 | if resp.StatusCode == http.StatusOK { 215 | break 216 | } 217 | 218 | if resp.StatusCode == http.StatusUnauthorized { 219 | resp.Body.Close() 220 | 221 | cred, err := self.owner.transport.SmartCredentials("", CredentialTypeUserpassPlaintext) 222 | if err != nil { 223 | return err 224 | } 225 | defer cred.Free() 226 | 227 | userName, password, err = cred.GetUserpassPlaintext() 228 | if err != nil { 229 | return err 230 | } 231 | 232 | continue 233 | } 234 | 235 | // Any other error we treat as a hard error and punt back to the caller 236 | resp.Body.Close() 237 | return fmt.Errorf("Unhandled HTTP error %s", resp.Status) 238 | } 239 | 240 | self.sentRequest = true 241 | self.resp = resp 242 | return nil 243 | } 244 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | /* 4 | #include 5 | 6 | extern int _go_git_treewalk(git_tree *tree, git_treewalk_mode mode, void *ptr); 7 | */ 8 | import "C" 9 | 10 | import ( 11 | "errors" 12 | "runtime" 13 | "unsafe" 14 | ) 15 | 16 | type Filemode int 17 | 18 | const ( 19 | FilemodeTree Filemode = C.GIT_FILEMODE_TREE 20 | FilemodeBlob Filemode = C.GIT_FILEMODE_BLOB 21 | FilemodeBlobExecutable Filemode = C.GIT_FILEMODE_BLOB_EXECUTABLE 22 | FilemodeLink Filemode = C.GIT_FILEMODE_LINK 23 | FilemodeCommit Filemode = C.GIT_FILEMODE_COMMIT 24 | ) 25 | 26 | type Tree struct { 27 | doNotCompare 28 | Object 29 | cast_ptr *C.git_tree 30 | } 31 | 32 | func (t *Tree) AsObject() *Object { 33 | return &t.Object 34 | } 35 | 36 | type TreeEntry struct { 37 | Name string 38 | Id *Oid 39 | Type ObjectType 40 | Filemode Filemode 41 | } 42 | 43 | func newTreeEntry(entry *C.git_tree_entry) *TreeEntry { 44 | return &TreeEntry{ 45 | C.GoString(C.git_tree_entry_name(entry)), 46 | newOidFromC(C.git_tree_entry_id(entry)), 47 | ObjectType(C.git_tree_entry_type(entry)), 48 | Filemode(C.git_tree_entry_filemode(entry)), 49 | } 50 | } 51 | 52 | func (t *Tree) EntryByName(filename string) *TreeEntry { 53 | cname := C.CString(filename) 54 | defer C.free(unsafe.Pointer(cname)) 55 | 56 | entry := C.git_tree_entry_byname(t.cast_ptr, cname) 57 | if entry == nil { 58 | return nil 59 | } 60 | 61 | goEntry := newTreeEntry(entry) 62 | runtime.KeepAlive(t) 63 | return goEntry 64 | } 65 | 66 | // EntryById performs a lookup for a tree entry with the given SHA value. 67 | // 68 | // It returns a *TreeEntry that is owned by the Tree. You don't have to 69 | // free it, but you must not use it after the Tree is freed. 70 | // 71 | // Warning: this must examine every entry in the tree, so it is not fast. 72 | func (t *Tree) EntryById(id *Oid) *TreeEntry { 73 | runtime.LockOSThread() 74 | defer runtime.UnlockOSThread() 75 | 76 | entry := C.git_tree_entry_byid(t.cast_ptr, id.toC()) 77 | runtime.KeepAlive(id) 78 | if entry == nil { 79 | return nil 80 | } 81 | 82 | goEntry := newTreeEntry(entry) 83 | runtime.KeepAlive(t) 84 | return goEntry 85 | } 86 | 87 | // EntryByPath looks up an entry by its full path, recursing into 88 | // deeper trees if necessary (i.e. if there are slashes in the path) 89 | func (t *Tree) EntryByPath(path string) (*TreeEntry, error) { 90 | cpath := C.CString(path) 91 | defer C.free(unsafe.Pointer(cpath)) 92 | var entry *C.git_tree_entry 93 | 94 | runtime.LockOSThread() 95 | defer runtime.UnlockOSThread() 96 | 97 | ret := C.git_tree_entry_bypath(&entry, t.cast_ptr, cpath) 98 | runtime.KeepAlive(t) 99 | if ret < 0 { 100 | return nil, MakeGitError(ret) 101 | } 102 | defer C.git_tree_entry_free(entry) 103 | 104 | return newTreeEntry(entry), nil 105 | } 106 | 107 | func (t *Tree) EntryByIndex(index uint64) *TreeEntry { 108 | entry := C.git_tree_entry_byindex(t.cast_ptr, C.size_t(index)) 109 | if entry == nil { 110 | return nil 111 | } 112 | 113 | goEntry := newTreeEntry(entry) 114 | runtime.KeepAlive(t) 115 | return goEntry 116 | } 117 | 118 | func (t *Tree) EntryCount() uint64 { 119 | num := C.git_tree_entrycount(t.cast_ptr) 120 | runtime.KeepAlive(t) 121 | return uint64(num) 122 | } 123 | 124 | type TreeWalkCallback func(string, *TreeEntry) error 125 | type treeWalkCallbackData struct { 126 | callback TreeWalkCallback 127 | errorTarget *error 128 | } 129 | 130 | //export treeWalkCallback 131 | func treeWalkCallback(_root *C.char, entry *C.git_tree_entry, ptr unsafe.Pointer) C.int { 132 | data, ok := pointerHandles.Get(ptr).(*treeWalkCallbackData) 133 | if !ok { 134 | panic("invalid treewalk callback") 135 | } 136 | 137 | err := data.callback(C.GoString(_root), newTreeEntry(entry)) 138 | if err == TreeWalkSkip { 139 | return C.int(1) 140 | } else if err != nil { 141 | *data.errorTarget = err 142 | return C.int(ErrorCodeUser) 143 | } 144 | 145 | return C.int(ErrorCodeOK) 146 | } 147 | 148 | // TreeWalkSkip is an error that can be returned form TreeWalkCallback to skip 149 | // a subtree from being expanded. 150 | var TreeWalkSkip = errors.New("skip") 151 | 152 | // Walk traverses the entries in a tree and its subtrees in pre order. 153 | // 154 | // The entries will be traversed in the pre order, children subtrees will be 155 | // automatically loaded as required, and the callback will be called once per 156 | // entry with the current (relative) root for the entry and the entry data 157 | // itself. 158 | // 159 | // If the callback returns TreeWalkSkip, the passed entry will be skipped on 160 | // the traversal. Any other non-nil error stops the walk. 161 | func (t *Tree) Walk(callback TreeWalkCallback) error { 162 | var err error 163 | data := treeWalkCallbackData{ 164 | callback: callback, 165 | errorTarget: &err, 166 | } 167 | runtime.LockOSThread() 168 | defer runtime.UnlockOSThread() 169 | 170 | handle := pointerHandles.Track(&data) 171 | defer pointerHandles.Untrack(handle) 172 | 173 | ret := C._go_git_treewalk(t.cast_ptr, C.GIT_TREEWALK_PRE, handle) 174 | runtime.KeepAlive(t) 175 | if ret == C.int(ErrorCodeUser) && err != nil { 176 | return err 177 | } 178 | if ret < 0 { 179 | return MakeGitError(ret) 180 | } 181 | 182 | return nil 183 | } 184 | 185 | type TreeBuilder struct { 186 | doNotCompare 187 | ptr *C.git_treebuilder 188 | repo *Repository 189 | } 190 | 191 | func (v *TreeBuilder) Free() { 192 | runtime.SetFinalizer(v, nil) 193 | C.git_treebuilder_free(v.ptr) 194 | } 195 | 196 | func (v *TreeBuilder) Insert(filename string, id *Oid, filemode Filemode) error { 197 | cfilename := C.CString(filename) 198 | defer C.free(unsafe.Pointer(cfilename)) 199 | 200 | runtime.LockOSThread() 201 | defer runtime.UnlockOSThread() 202 | 203 | err := C.git_treebuilder_insert(nil, v.ptr, cfilename, id.toC(), C.git_filemode_t(filemode)) 204 | runtime.KeepAlive(v) 205 | runtime.KeepAlive(id) 206 | if err < 0 { 207 | return MakeGitError(err) 208 | } 209 | 210 | return nil 211 | } 212 | 213 | func (v *TreeBuilder) Remove(filename string) error { 214 | cfilename := C.CString(filename) 215 | defer C.free(unsafe.Pointer(cfilename)) 216 | 217 | runtime.LockOSThread() 218 | defer runtime.UnlockOSThread() 219 | 220 | err := C.git_treebuilder_remove(v.ptr, cfilename) 221 | runtime.KeepAlive(v) 222 | if err < 0 { 223 | return MakeGitError(err) 224 | } 225 | 226 | return nil 227 | } 228 | 229 | func (v *TreeBuilder) Write() (*Oid, error) { 230 | oid := new(Oid) 231 | 232 | runtime.LockOSThread() 233 | defer runtime.UnlockOSThread() 234 | 235 | err := C.git_treebuilder_write(oid.toC(), v.ptr) 236 | runtime.KeepAlive(v) 237 | if err < 0 { 238 | return nil, MakeGitError(err) 239 | } 240 | 241 | return oid, nil 242 | } 243 | -------------------------------------------------------------------------------- /index_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | func TestCreateRepoAndStage(t *testing.T) { 12 | t.Parallel() 13 | repo := createTestRepo(t) 14 | defer cleanupTestRepo(t, repo) 15 | 16 | idx, err := repo.Index() 17 | checkFatal(t, err) 18 | err = idx.AddByPath("README") 19 | checkFatal(t, err) 20 | treeId, err := idx.WriteTree() 21 | checkFatal(t, err) 22 | 23 | if treeId.String() != "b7119b11e8ef7a1a5a34d3ac87f5b075228ac81e" { 24 | t.Fatalf("%v", treeId.String()) 25 | } 26 | } 27 | 28 | func TestIndexReadTree(t *testing.T) { 29 | t.Parallel() 30 | repo := createTestRepo(t) 31 | defer cleanupTestRepo(t, repo) 32 | 33 | _, _ = seedTestRepo(t, repo) 34 | 35 | ref, err := repo.Head() 36 | checkFatal(t, err) 37 | 38 | obj, err := ref.Peel(ObjectTree) 39 | checkFatal(t, err) 40 | 41 | tree, err := obj.AsTree() 42 | checkFatal(t, err) 43 | 44 | idx, err := NewIndex() 45 | checkFatal(t, err) 46 | 47 | err = idx.ReadTree(tree) 48 | checkFatal(t, err) 49 | 50 | id, err := idx.WriteTreeTo(repo) 51 | checkFatal(t, err) 52 | 53 | if tree.Id().Cmp(id) != 0 { 54 | t.Fatalf("Read and written trees are not the same") 55 | } 56 | } 57 | 58 | func TestIndexWriteTreeTo(t *testing.T) { 59 | t.Parallel() 60 | repo := createTestRepo(t) 61 | defer cleanupTestRepo(t, repo) 62 | 63 | idx, err := NewIndex() 64 | checkFatal(t, err) 65 | 66 | odb, err := repo.Odb() 67 | checkFatal(t, err) 68 | 69 | content, err := ioutil.ReadFile(path.Join(repo.Workdir(), "README")) 70 | checkFatal(t, err) 71 | 72 | id, err := odb.Write(content, ObjectBlob) 73 | checkFatal(t, err) 74 | 75 | err = idx.Add(&IndexEntry{ 76 | Mode: FilemodeBlob, 77 | Uid: 0, 78 | Gid: 0, 79 | Size: uint32(len(content)), 80 | Id: id, 81 | Path: "README", 82 | }) 83 | checkFatal(t, err) 84 | 85 | treeId, err := idx.WriteTreeTo(repo) 86 | checkFatal(t, err) 87 | 88 | if treeId.String() != "b7119b11e8ef7a1a5a34d3ac87f5b075228ac81e" { 89 | t.Fatalf("%v", treeId.String()) 90 | } 91 | } 92 | 93 | func TestIndexAddAndWriteTreeTo(t *testing.T) { 94 | t.Parallel() 95 | repo := createTestRepo(t) 96 | defer cleanupTestRepo(t, repo) 97 | 98 | odb, err := repo.Odb() 99 | checkFatal(t, err) 100 | 101 | blobID, err := odb.Write([]byte("foo\n"), ObjectBlob) 102 | checkFatal(t, err) 103 | 104 | idx, err := NewIndex() 105 | checkFatal(t, err) 106 | 107 | if idx.Path() != "" { 108 | t.Fatal("in-memory repo has a path") 109 | } 110 | 111 | entry := IndexEntry{ 112 | Path: "README", 113 | Id: blobID, 114 | Mode: FilemodeBlob, 115 | } 116 | 117 | err = idx.Add(&entry) 118 | checkFatal(t, err) 119 | 120 | treeId, err := idx.WriteTreeTo(repo) 121 | checkFatal(t, err) 122 | 123 | if treeId.String() != "b7119b11e8ef7a1a5a34d3ac87f5b075228ac81e" { 124 | t.Fatalf("%v", treeId.String()) 125 | } 126 | } 127 | 128 | func TestIndexRemoveDirectory(t *testing.T) { 129 | repo := createTestRepo(t) 130 | defer cleanupTestRepo(t, repo) 131 | 132 | odb, err := repo.Odb() 133 | checkFatal(t, err) 134 | 135 | blobID, err := odb.Write([]byte("fou\n"), ObjectBlob) 136 | checkFatal(t, err) 137 | 138 | idx, err := NewIndex() 139 | checkFatal(t, err) 140 | 141 | entryCount := idx.EntryCount() 142 | if entryCount != 0 { 143 | t.Fatal("Index should count 0 entry") 144 | } 145 | 146 | entry := IndexEntry{ 147 | Path: "path/to/LISEZ_MOI", 148 | Id: blobID, 149 | Mode: FilemodeBlob, 150 | } 151 | 152 | err = idx.Add(&entry) 153 | checkFatal(t, err) 154 | 155 | entryCount = idx.EntryCount() 156 | if entryCount != 1 { 157 | t.Fatal("Index should count 1 entry") 158 | } 159 | 160 | err = idx.RemoveDirectory("path", 0) 161 | 162 | entryCount = idx.EntryCount() 163 | if entryCount != 0 { 164 | t.Fatal("Index should count 0 entry") 165 | } 166 | } 167 | 168 | func TestIndexAddFromBuffer(t *testing.T) { 169 | t.Parallel() 170 | repo := createTestRepo(t) 171 | defer cleanupTestRepo(t, repo) 172 | 173 | idx, err := repo.Index() 174 | checkFatal(t, err) 175 | 176 | entry := IndexEntry{ 177 | Path: "README", 178 | Mode: FilemodeBlob, 179 | } 180 | 181 | err = idx.AddFromBuffer(&entry, []byte("foo\n")) 182 | checkFatal(t, err) 183 | 184 | treeId, err := idx.WriteTreeTo(repo) 185 | checkFatal(t, err) 186 | 187 | if treeId.String() != "b7119b11e8ef7a1a5a34d3ac87f5b075228ac81e" { 188 | t.Fatalf("%v", treeId.String()) 189 | } 190 | } 191 | 192 | func TestIndexAddAllNoCallback(t *testing.T) { 193 | t.Parallel() 194 | repo := createTestRepo(t) 195 | defer cleanupTestRepo(t, repo) 196 | 197 | err := ioutil.WriteFile(repo.Workdir()+"/README", []byte("foo\n"), 0644) 198 | checkFatal(t, err) 199 | 200 | idx, err := repo.Index() 201 | checkFatal(t, err) 202 | 203 | err = idx.AddAll([]string{}, IndexAddDefault, nil) 204 | checkFatal(t, err) 205 | 206 | treeId, err := idx.WriteTreeTo(repo) 207 | checkFatal(t, err) 208 | 209 | if treeId.String() != "b7119b11e8ef7a1a5a34d3ac87f5b075228ac81e" { 210 | t.Fatalf("%v", treeId.String()) 211 | } 212 | } 213 | 214 | func TestIndexAddAllCallback(t *testing.T) { 215 | t.Parallel() 216 | repo := createTestRepo(t) 217 | defer cleanupTestRepo(t, repo) 218 | 219 | err := ioutil.WriteFile(repo.Workdir()+"/README", []byte("foo\n"), 0644) 220 | checkFatal(t, err) 221 | 222 | idx, err := repo.Index() 223 | checkFatal(t, err) 224 | 225 | cbPath := "" 226 | err = idx.AddAll([]string{}, IndexAddDefault, func(p, mP string) error { 227 | cbPath = p 228 | return nil 229 | }) 230 | checkFatal(t, err) 231 | if cbPath != "README" { 232 | t.Fatalf("%v", cbPath) 233 | } 234 | 235 | treeId, err := idx.WriteTreeTo(repo) 236 | checkFatal(t, err) 237 | 238 | if treeId.String() != "b7119b11e8ef7a1a5a34d3ac87f5b075228ac81e" { 239 | t.Fatalf("%v", treeId.String()) 240 | } 241 | } 242 | 243 | func TestIndexOpen(t *testing.T) { 244 | t.Parallel() 245 | repo := createTestRepo(t) 246 | defer cleanupTestRepo(t, repo) 247 | 248 | path := repo.Workdir() + "/heyindex" 249 | 250 | _, err := os.Stat(path) 251 | if !os.IsNotExist(err) { 252 | t.Fatal("new index file already exists") 253 | } 254 | 255 | idx, err := OpenIndex(path) 256 | checkFatal(t, err) 257 | 258 | if path != idx.Path() { 259 | t.Fatalf("mismatched index paths, expected %v, got %v", path, idx.Path()) 260 | } 261 | 262 | err = idx.Write() 263 | checkFatal(t, err) 264 | 265 | _, err = os.Stat(path) 266 | if os.IsNotExist(err) { 267 | t.Fatal("new index file did not get written") 268 | } 269 | } 270 | 271 | func checkFatal(t *testing.T, err error) { 272 | if err == nil { 273 | return 274 | } 275 | 276 | // The failure happens at wherever we were called, not here 277 | _, file, line, ok := runtime.Caller(1) 278 | if !ok { 279 | t.Fatalf("Unable to get caller") 280 | } 281 | t.Fatalf("Fail at %v:%v; %v", file, line, err) 282 | } 283 | --------------------------------------------------------------------------------