diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..931cc09 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,89 @@ +name: git2go CI +on: + pull_request: + push: + branches: + - master + - release-* + - v* + +jobs: + + build-legacy: + strategy: + fail-fast: false + matrix: + go: [ '1.9', '1.10' ] + name: Go ${{ matrix.go }} + + runs-on: ubuntu-18.04 + + steps: + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go }} + id: go + - name: Check out code into the GOPATH + uses: actions/checkout@v1 + with: + fetch-depth: 1 + path: src/github.com/${{ github.repository }} + - name: Build + env: + GOPATH: /home/runner/work/git2go + run: | + git submodule update --init + make build-libgit2-static + go get --tags "static" github.com/${{ github.repository }}/... + go build --tags "static" github.com/${{ github.repository }}/... + - name: Test + env: + GOPATH: /home/runner/work/git2go + run: make test-static + + build-static: + strategy: + fail-fast: false + matrix: + go: [ '1.11', '1.12', '1.13' ] + name: Go ${{ matrix.go }} + + runs-on: ubuntu-18.04 + + steps: + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go }} + id: go + - name: Check out code into the Go module directory + uses: actions/checkout@v1 + - name: Build + run: | + git submodule update --init + make build-libgit2-static + - name: Test + run: make test-static + + build-dynamic: + strategy: + fail-fast: false + name: Go (dynamic) + + runs-on: ubuntu-18.04 + + steps: + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: '1.13' + id: go + - name: Check out code into the Go module directory + uses: actions/checkout@v1 + - name: Build + run: | + git submodule update --init + make build-libgit2-dynamic + - name: Test + run: make test-dynamic diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..713781b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/static-build/ +/dynamic-build/ diff --git a/.travis.yml b/.travis.yml index 6131e6d..b25a052 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,29 @@ language: go go: - - 1.7 - - 1.8 + - "1.9" + - "1.10" + - "1.11" + - "1.12" + - "1.13" - tip -script: make test-static +install: + - make build-libgit2-static + - go get --tags "static" ./... + +script: + - make test-static matrix: allow_failures: - go: tip +git: + submodules: true + branches: only: - master - /v\d+/ - - next + - /release-.*/ diff --git a/Makefile b/Makefile index cf00cef..182c53e 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,53 @@ default: test -test: build-libgit2 +# System library +# ============== +# This uses whatever version of libgit2 can be found in the system. +test: go run script/check-MakeGitError-thread-lock.go - go test ./... + go test --count=1 ./... -install: build-libgit2 +install: go install ./... -build-libgit2: +# Bundled dynamic library +# ======================= +# In order to avoid having to manipulate `git_dynamic.go`, which would prevent +# the system-wide libgit2.so from being used in a sort of ergonomic way, this +# instead moves the complexity of overriding the paths so that the built +# libraries can be found by the build and tests. +.PHONY: build-libgit2-dynamic +build-libgit2-dynamic: + ./script/build-libgit2-dynamic.sh + +dynamic-build/install/lib/libgit2.so: + ./script/build-libgit2-dynamic.sh + +test-dynamic: dynamic-build/install/lib/libgit2.so + PKG_CONFIG_PATH=dynamic-build/install/lib/pkgconfig \ + go run script/check-MakeGitError-thread-lock.go + PKG_CONFIG_PATH=dynamic-build/install/lib/pkgconfig \ + LD_LIBRARY_PATH=dynamic-build/install/lib \ + go test --count=1 ./... + +install-dynamic: dynamic-build/install/lib/libgit2.so + PKG_CONFIG_PATH=dynamic-build/install/lib/pkgconfig \ + go install ./... + +# Bundled static library +# ====================== +# This is mostly used in tests, but can also be used to provide a +# statically-linked library with the bundled version of libgit2. +.PHONY: build-libgit2-static +build-libgit2-static: ./script/build-libgit2-static.sh -install-static: build-libgit2 - go install --tags "static" ./... +static-build/install/lib/libgit2.a: + ./script/build-libgit2-static.sh -test-static: build-libgit2 +test-static: static-build/install/lib/libgit2.a go run script/check-MakeGitError-thread-lock.go - go test --tags "static" ./... + go test --count=1 --tags "static" ./... + +install-static: static-build/install/lib/libgit2.a + go install --tags "static" ./... diff --git a/README.md b/README.md index 4ecfa34..4bd2e7e 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ If using `master` or building a branch statically, we need to build libgit2 firs Run `go get -d github.com/libgit2/git2go` to download the code and go to your `$GOPATH/src/github.com/libgit2/git2go` directory. From there, we need to build the C code and put it into the resulting go binary. git submodule update --init # get libgit2 - make install + make install-static -will compile libgit2, link it into git2go and install it. +will compile libgit2, link it into git2go and install it. The `master` 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. Parallelism and network operations ---------------------------------- @@ -47,19 +47,19 @@ libgit2 may use OpenSSL and LibSSH2 for performing encrypted network connections Running the tests ----------------- -For the stable version, `go test` will work as usual. For the `next` 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 +For the stable version, `go test` will work as usual. For the `master` 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 - make test + make test-static Alternatively, you can build the library manually first and then run the tests ./script/build-libgit2-static.sh - go test -v + go test -v --tags "static" ./... License ------- -M to the I to the T. See the LICENSE file if you've never seen a MIT license before. +M to the I to the T. See the LICENSE file if you've never seen an MIT license before. Authors ------- diff --git a/blob.go b/blob.go index 227e014..d895449 100644 --- a/blob.go +++ b/blob.go @@ -48,7 +48,7 @@ func (repo *Repository) CreateBlobFromBuffer(data []byte) (*Oid, error) { var size C.size_t // Go 1.6 added some increased checking of passing pointer to - // C, but its check depends on its expectations of waht we + // C, but its check depends on its expectations of what we // pass to the C function, so unless we take the address of // its contents at the call site itself, it can fail when // 'data' is a slice of a slice. diff --git a/branch.go b/branch.go index d6e7a53..6f79825 100644 --- a/branch.go +++ b/branch.go @@ -208,7 +208,7 @@ func (repo *Repository) RemoteName(canonicalBranchName string) (string, error) { if ret < 0 { return "", MakeGitError(ret) } - defer C.git_buf_free(&nameBuf) + defer C.git_buf_dispose(&nameBuf) return C.GoString(nameBuf.ptr), nil } @@ -256,7 +256,7 @@ func (repo *Repository) UpstreamName(canonicalBranchName string) (string, error) if ret < 0 { return "", MakeGitError(ret) } - defer C.git_buf_free(&nameBuf) + defer C.git_buf_dispose(&nameBuf) return C.GoString(nameBuf.ptr), nil } diff --git a/checkout.go b/checkout.go index db3118f..2b12058 100644 --- a/checkout.go +++ b/checkout.go @@ -35,7 +35,7 @@ const ( CheckoutDontUpdateIndex CheckoutStrategy = C.GIT_CHECKOUT_DONT_UPDATE_INDEX // Normally checkout updates index entries as it goes; this stops that CheckoutNoRefresh CheckoutStrategy = C.GIT_CHECKOUT_NO_REFRESH // Don't refresh index/config/etc before doing checkout CheckoutSkipUnmerged CheckoutStrategy = C.GIT_CHECKOUT_SKIP_UNMERGED // Allow checkout to skip unmerged files - CheckoutUserOurs CheckoutStrategy = C.GIT_CHECKOUT_USE_OURS // For unmerged files, checkout stage 2 from index + CheckoutUseOurs CheckoutStrategy = C.GIT_CHECKOUT_USE_OURS // For unmerged files, checkout stage 2 from index CheckoutUseTheirs CheckoutStrategy = C.GIT_CHECKOUT_USE_THEIRS // For unmerged files, checkout stage 3 from index CheckoutDisablePathspecMatch CheckoutStrategy = C.GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH // Treat pathspec as simple list of exact match file paths CheckoutSkipLockedDirectories CheckoutStrategy = C.GIT_CHECKOUT_SKIP_LOCKED_DIRECTORIES // Ignore directories in use, they will be left empty diff --git a/commit.go b/commit.go index 223b093..4262060 100644 --- a/commit.go +++ b/commit.go @@ -28,6 +28,12 @@ func (c *Commit) Message() string { return ret } +func (c *Commit) MessageEncoding() string { + ret := C.GoString(C.git_commit_message_encoding(c.cast_ptr)) + runtime.KeepAlive(c) + return ret +} + func (c *Commit) RawMessage() string { ret := C.GoString(C.git_commit_message_raw(c.cast_ptr)) runtime.KeepAlive(c) @@ -37,10 +43,10 @@ func (c *Commit) RawMessage() string { func (c *Commit) ExtractSignature() (string, string, error) { var c_signed C.git_buf - defer C.git_buf_free(&c_signed) + defer C.git_buf_dispose(&c_signed) var c_signature C.git_buf - defer C.git_buf_free(&c_signature) + defer C.git_buf_dispose(&c_signature) oid := c.Id() repo := C.git_commit_owner(c.cast_ptr) diff --git a/config.go b/config.go index c19ad32..7260089 100644 --- a/config.go +++ b/config.go @@ -77,7 +77,7 @@ func (c *Config) AddFile(path string, level ConfigLevel, force bool) error { runtime.LockOSThread() defer runtime.UnlockOSThread() - ret := C.git_config_add_file_ondisk(c.ptr, cpath, C.git_config_level_t(level), cbool(force)) + ret := C.git_config_add_file_ondisk(c.ptr, cpath, C.git_config_level_t(level), nil, cbool(force)) runtime.KeepAlive(c) if ret < 0 { return MakeGitError(ret) @@ -134,7 +134,7 @@ func (c *Config) LookupString(name string) (string, error) { if ret < 0 { return "", MakeGitError(ret) } - defer C.git_buf_free(&valBuf) + defer C.git_buf_dispose(&valBuf) return C.GoString(valBuf.ptr), nil } @@ -344,7 +344,7 @@ func (c *Config) OpenLevel(parent *Config, level ConfigLevel) (*Config, error) { } // OpenOndisk creates a new config instance containing a single on-disk file -func OpenOndisk(parent *Config, path string) (*Config, error) { +func OpenOndisk(path string) (*Config, error) { cpath := C.CString(path) defer C.free(unsafe.Pointer(cpath)) @@ -390,7 +390,7 @@ func (iter *ConfigIterator) Free() { func ConfigFindGlobal() (string, error) { var buf C.git_buf - defer C.git_buf_free(&buf) + defer C.git_buf_dispose(&buf) runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -405,7 +405,7 @@ func ConfigFindGlobal() (string, error) { func ConfigFindSystem() (string, error) { var buf C.git_buf - defer C.git_buf_free(&buf) + defer C.git_buf_dispose(&buf) runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -420,7 +420,7 @@ func ConfigFindSystem() (string, error) { func ConfigFindXDG() (string, error) { var buf C.git_buf - defer C.git_buf_free(&buf) + defer C.git_buf_dispose(&buf) runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -438,7 +438,7 @@ func ConfigFindXDG() (string, error) { // Look for the file in %PROGRAMDATA%\Git\config used by portable git. func ConfigFindProgramdata() (string, error) { var buf C.git_buf - defer C.git_buf_free(&buf) + defer C.git_buf_dispose(&buf) runtime.LockOSThread() defer runtime.UnlockOSThread() diff --git a/config_test.go b/config_test.go index 196d4ad..398236e 100644 --- a/config_test.go +++ b/config_test.go @@ -13,7 +13,7 @@ func setupConfig() (*Config, error) { err error ) - c, err = OpenOndisk(nil, tempConfig) + c, err = OpenOndisk(tempConfig) if err != nil { return nil, err } diff --git a/credentials.go b/credentials.go index 4e42b6e..038313b 100644 --- a/credentials.go +++ b/credentials.go @@ -2,6 +2,9 @@ package git /* #include +#include + +git_credtype_t _go_git_cred_credtype(git_cred *cred); */ import "C" import "unsafe" @@ -27,7 +30,7 @@ func (o *Cred) HasUsername() bool { } func (o *Cred) Type() CredType { - return (CredType)(o.ptr.credtype) + return (CredType)(C._go_git_cred_credtype(o.ptr)) } func credFromC(ptr *C.git_cred) *Cred { diff --git a/delta_string.go b/delta_string.go new file mode 100644 index 0000000..53e02bd --- /dev/null +++ b/delta_string.go @@ -0,0 +1,33 @@ +// Code generated by "stringer -type Delta -trimprefix Delta -tags static"; DO NOT EDIT. + +package git + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[DeltaUnmodified-0] + _ = x[DeltaAdded-1] + _ = x[DeltaDeleted-2] + _ = x[DeltaModified-3] + _ = x[DeltaRenamed-4] + _ = x[DeltaCopied-5] + _ = x[DeltaIgnored-6] + _ = x[DeltaUntracked-7] + _ = x[DeltaTypeChange-8] + _ = x[DeltaUnreadable-9] + _ = x[DeltaConflicted-10] +} + +const _Delta_name = "UnmodifiedAddedDeletedModifiedRenamedCopiedIgnoredUntrackedTypeChangeUnreadableConflicted" + +var _Delta_index = [...]uint8{0, 10, 15, 22, 30, 37, 43, 50, 59, 69, 79, 89} + +func (i Delta) String() string { + if i < 0 || i >= Delta(len(_Delta_index)-1) { + return "Delta(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Delta_name[_Delta_index[i]:_Delta_index[i+1]] +} diff --git a/describe.go b/describe.go index 0b75076..f7036df 100644 --- a/describe.go +++ b/describe.go @@ -212,7 +212,7 @@ func (result *DescribeResult) Format(opts *DescribeFormatOptions) (string, error if ecode < 0 { return "", MakeGitError(ecode) } - defer C.git_buf_free(&resultBuf) + defer C.git_buf_dispose(&resultBuf) return C.GoString(resultBuf.ptr), nil } diff --git a/diff.go b/diff.go index 3cc1dc2..ed2949c 100644 --- a/diff.go +++ b/diff.go @@ -39,6 +39,8 @@ const ( DeltaConflicted Delta = C.GIT_DELTA_CONFLICTED ) +//go:generate stringer -type Delta -trimprefix Delta -tags static + type DiffLineType int const ( @@ -54,6 +56,8 @@ const ( DiffLineBinary DiffLineType = C.GIT_DIFF_LINE_BINARY ) +//go:generate stringer -type DiffLineType -trimprefix DiffLine -tags static + type DiffFile struct { Path string Oid *Oid @@ -246,7 +250,7 @@ const ( func (stats *DiffStats) String(format DiffStatsFormat, width uint) (string, error) { buf := C.git_buf{} - defer C.git_buf_free(&buf) + defer C.git_buf_dispose(&buf) runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -284,7 +288,7 @@ type diffForEachData struct { Error error } -type DiffForEachFileCallback func(DiffDelta, float64) (DiffForEachHunkCallback, error) +type DiffForEachFileCallback func(delta DiffDelta, progress float64) (DiffForEachHunkCallback, error) type DiffDetail int @@ -405,6 +409,36 @@ func (diff *Diff) Patch(deltaIndex int) (*Patch, error) { return newPatchFromC(patchPtr), nil } +type DiffFormat int + +const ( + DiffFormatPatch DiffFormat = C.GIT_DIFF_FORMAT_PATCH + DiffFormatPatchHeader DiffFormat = C.GIT_DIFF_FORMAT_PATCH_HEADER + DiffFormatRaw DiffFormat = C.GIT_DIFF_FORMAT_RAW + DiffFormatNameOnly DiffFormat = C.GIT_DIFF_FORMAT_NAME_ONLY + DiffFormatNameStatus DiffFormat = C.GIT_DIFF_FORMAT_NAME_STATUS +) + +func (diff *Diff) ToBuf(format DiffFormat) ([]byte, error) { + if diff.ptr == nil { + return nil, ErrInvalid + } + + diffBuf := C.git_buf{} + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ecode := C.git_diff_to_buf(&diffBuf, diff.ptr, C.git_diff_format_t(format)) + runtime.KeepAlive(diff) + if ecode < 0 { + return nil, MakeGitError(ecode) + } + defer C.git_buf_free(&diffBuf) + + return C.GoBytes(unsafe.Pointer(diffBuf.ptr), C.int(diffBuf.size)), nil +} + type DiffOptionsFlag int const ( @@ -437,6 +471,8 @@ const ( DiffShowUnmodified DiffOptionsFlag = C.GIT_DIFF_SHOW_UNMODIFIED DiffPatience DiffOptionsFlag = C.GIT_DIFF_PATIENCE DiffMinimal DiffOptionsFlag = C.GIT_DIFF_MINIMAL + DiffShowBinary DiffOptionsFlag = C.GIT_DIFF_SHOW_BINARY + DiffIndentHeuristic DiffOptionsFlag = C.GIT_DIFF_INDENT_HEURISTIC ) type DiffNotifyCallback func(diffSoFar *Diff, deltaToAdd DiffDelta, matchedPathspec string) error diff --git a/difflinetype_string.go b/difflinetype_string.go new file mode 100644 index 0000000..3c1ad58 --- /dev/null +++ b/difflinetype_string.go @@ -0,0 +1,56 @@ +// Code generated by "stringer -type DiffLineType -trimprefix DiffLine -tags static"; DO NOT EDIT. + +package git + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[DiffLineContext-32] + _ = x[DiffLineAddition-43] + _ = x[DiffLineDeletion-45] + _ = x[DiffLineContextEOFNL-61] + _ = x[DiffLineAddEOFNL-62] + _ = x[DiffLineDelEOFNL-60] + _ = x[DiffLineFileHdr-70] + _ = x[DiffLineHunkHdr-72] + _ = x[DiffLineBinary-66] +} + +const ( + _DiffLineType_name_0 = "Context" + _DiffLineType_name_1 = "Addition" + _DiffLineType_name_2 = "Deletion" + _DiffLineType_name_3 = "DelEOFNLContextEOFNLAddEOFNL" + _DiffLineType_name_4 = "Binary" + _DiffLineType_name_5 = "FileHdr" + _DiffLineType_name_6 = "HunkHdr" +) + +var ( + _DiffLineType_index_3 = [...]uint8{0, 8, 20, 28} +) + +func (i DiffLineType) String() string { + switch { + case i == 32: + return _DiffLineType_name_0 + case i == 43: + return _DiffLineType_name_1 + case i == 45: + return _DiffLineType_name_2 + case 60 <= i && i <= 62: + i -= 60 + return _DiffLineType_name_3[_DiffLineType_index_3[i]:_DiffLineType_index_3[i+1]] + case i == 66: + return _DiffLineType_name_4 + case i == 70: + return _DiffLineType_name_5 + case i == 72: + return _DiffLineType_name_6 + default: + return "DiffLineType(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/git.go b/git.go index 0925e45..968d404 100644 --- a/git.go +++ b/git.go @@ -189,22 +189,16 @@ func (oid *Oid) Cmp(oid2 *Oid) int { } func (oid *Oid) Copy() *Oid { - ret := new(Oid) - copy(ret[:], oid[:]) - return ret + ret := *oid + return &ret } func (oid *Oid) Equal(oid2 *Oid) bool { - return bytes.Equal(oid[:], oid2[:]) + return *oid == *oid2 } func (oid *Oid) IsZero() bool { - for _, a := range oid { - if a != 0 { - return false - } - } - return true + return *oid == Oid{} } func (oid *Oid) NCmp(oid2 *Oid, n uint) int { @@ -309,7 +303,7 @@ func Discover(start string, across_fs bool, ceiling_dirs []string) (string, erro defer C.free(unsafe.Pointer(cstart)) var buf C.git_buf - defer C.git_buf_free(&buf) + defer C.git_buf_dispose(&buf) runtime.LockOSThread() defer runtime.UnlockOSThread() diff --git a/git_dynamic.go b/git_dynamic.go index f89d509..06deeeb 100644 --- a/git_dynamic.go +++ b/git_dynamic.go @@ -6,8 +6,8 @@ package git #include #cgo pkg-config: libgit2 -#if LIBGIT2_VER_MAJOR != 0 || LIBGIT2_VER_MINOR != 25 -# error "Invalid libgit2 version; this git2go supports libgit2 v0.25" +#if LIBGIT2_VER_MAJOR != 0 || LIBGIT2_VER_MINOR != 28 +# error "Invalid libgit2 version; this git2go supports libgit2 v0.28" #endif */ diff --git a/git_static.go b/git_static.go index d0acb3a..d7c2295 100644 --- a/git_static.go +++ b/git_static.go @@ -3,14 +3,13 @@ package git /* -#cgo CFLAGS: -I${SRCDIR}/vendor/libgit2/include -#cgo LDFLAGS: -L${SRCDIR}/vendor/libgit2/build/ -lgit2 -#cgo windows LDFLAGS: -lwinhttp -#cgo !windows pkg-config: --static ${SRCDIR}/vendor/libgit2/build/libgit2.pc +#cgo windows CFLAGS: -I${SRCDIR}/static-build/install/include/ +#cgo windows LDFLAGS: -L${SRCDIR}/static-build/install/lib/ -lgit2 -lwinhttp +#cgo !windows pkg-config: --static ${SRCDIR}/static-build/install/lib/pkgconfig/libgit2.pc #include -#if LIBGIT2_VER_MAJOR != 0 || LIBGIT2_VER_MINOR != 25 -# error "Invalid libgit2 version; this git2go supports libgit2 v0.25" +#if LIBGIT2_VER_MAJOR != 0 || LIBGIT2_VER_MINOR != 28 +# error "Invalid libgit2 version; this git2go supports libgit2 v0.28" #endif */ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7a68658 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/libgit2/git2go + +go 1.13 diff --git a/index.go b/index.go index d7a1e5f..32acd14 100644 --- a/index.go +++ b/index.go @@ -147,6 +147,20 @@ func (v *Index) Path() string { return ret } +// Clear clears the index object in memory; changes must be explicitly +// written to disk for them to take effect persistently +func (v *Index) Clear() error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := C.git_index_clear(v.ptr) + runtime.KeepAlive(v) + if err < 0 { + return MakeGitError(err) + } + return nil +} + // Add adds or replaces the given entry to the index, making a copy of // the data func (v *Index) Add(entry *IndexEntry) error { diff --git a/index_test.go b/index_test.go index d882809..5fa3f9f 100644 --- a/index_test.go +++ b/index_test.go @@ -3,6 +3,7 @@ package git import ( "io/ioutil" "os" + "path" "runtime" "testing" ) @@ -59,14 +60,29 @@ func TestIndexWriteTreeTo(t *testing.T) { repo := createTestRepo(t) defer cleanupTestRepo(t, repo) - repo2 := createTestRepo(t) - defer cleanupTestRepo(t, repo2) + idx, err := NewIndex() + checkFatal(t, err) - idx, err := repo.Index() + odb, err := repo.Odb() checkFatal(t, err) - err = idx.AddByPath("README") + + content, err := ioutil.ReadFile(path.Join(repo.Workdir(), "README")) checkFatal(t, err) - treeId, err := idx.WriteTreeTo(repo2) + + id, err := odb.Write(content, ObjectBlob) + checkFatal(t, err) + + err = idx.Add(&IndexEntry{ + Mode: FilemodeBlob, + Uid: 0, + Gid: 0, + Size: uint32(len(content)), + Id: id, + Path: "README", + }) + checkFatal(t, err) + + treeId, err := idx.WriteTreeTo(repo) checkFatal(t, err) if treeId.String() != "b7119b11e8ef7a1a5a34d3ac87f5b075228ac81e" { diff --git a/mempack.go b/mempack.go new file mode 100644 index 0000000..bdea224 --- /dev/null +++ b/mempack.go @@ -0,0 +1,91 @@ +package git + +/* +#include +#include + +extern int git_mempack_new(git_odb_backend **out); +extern int git_mempack_dump(git_buf *pack, git_repository *repo, git_odb_backend *backend); +extern int git_mempack_reset(git_odb_backend *backend); +extern void _go_git_odb_backend_free(git_odb_backend *backend); +*/ +import "C" + +import ( + "runtime" + "unsafe" +) + +// Mempack is a custom ODB backend that permits packing object in-memory. +type Mempack struct { + ptr *C.git_odb_backend +} + +// NewMempack creates a new mempack instance and registers it to the ODB. +func NewMempack(odb *Odb) (mempack *Mempack, err error) { + mempack = new(Mempack) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_mempack_new(&mempack.ptr) + if ret < 0 { + return nil, MakeGitError(ret) + } + + ret = C.git_odb_add_backend(odb.ptr, mempack.ptr, C.int(999)) + runtime.KeepAlive(odb) + if ret < 0 { + // Since git_odb_add_alternate() takes ownership of the ODB backend, the + // only case in which we free the mempack's memory is if it fails to be + // added to the ODB. + C._go_git_odb_backend_free(mempack.ptr) + return nil, MakeGitError(ret) + } + + return mempack, nil +} + +// Dump dumps all the queued in-memory writes to a packfile. +// +// It is the caller's responsibility to ensure that the generated packfile is +// available to the repository (e.g. by writing it to disk, or doing something +// crazy like distributing it across several copies of the repository over a +// network). +// +// Once the generated packfile is available to the repository, call +// Mempack.Reset to cleanup the memory store. +// +// Calling Mempack.Reset before the packfile has been written to disk will +// result in an inconsistent repository (the objects in the memory store won't +// be accessible). +func (mempack *Mempack) Dump(repository *Repository) ([]byte, error) { + buf := C.git_buf{} + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_mempack_dump(&buf, repository.ptr, mempack.ptr) + runtime.KeepAlive(repository) + if ret < 0 { + return nil, MakeGitError(ret) + } + defer C.git_buf_dispose(&buf) + + return C.GoBytes(unsafe.Pointer(buf.ptr), C.int(buf.size)), nil +} + +// Reset resets the memory packer by clearing all the queued objects. +// +// This assumes that Mempack.Dump has been called before to store all the +// queued objects into a single packfile. +func (mempack *Mempack) Reset() error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_mempack_reset(mempack.ptr) + if ret < 0 { + return MakeGitError(ret) + } + return nil +} diff --git a/mempack_test.go b/mempack_test.go new file mode 100644 index 0000000..3e31dcf --- /dev/null +++ b/mempack_test.go @@ -0,0 +1,60 @@ +package git + +import ( + "bytes" + "testing" +) + +func TestMempack(t *testing.T) { + t.Parallel() + + odb, err := NewOdb() + checkFatal(t, err) + + repo, err := NewRepositoryWrapOdb(odb) + checkFatal(t, err) + + mempack, err := NewMempack(odb) + checkFatal(t, err) + + id, err := odb.Write([]byte("hello, world!"), ObjectBlob) + checkFatal(t, err) + + expectedId, err := NewOid("30f51a3fba5274d53522d0f19748456974647b4f") + checkFatal(t, err) + if !expectedId.Equal(id) { + t.Errorf("mismatched id. expected %v, got %v", expectedId.String(), id.String()) + } + + // The object should be available from the odb. + { + obj, err := odb.Read(expectedId) + checkFatal(t, err) + defer obj.Free() + } + + data, err := mempack.Dump(repo) + checkFatal(t, err) + + expectedData := []byte{ + 0x50, 0x41, 0x43, 0x4b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x9d, 0x08, 0x82, 0x3b, 0xd8, 0xa8, 0xea, 0xb5, 0x10, 0xad, 0x6a, + 0xc7, 0x5c, 0x82, 0x3c, 0xfd, 0x3e, 0xd3, 0x1e, + } + if !bytes.Equal(expectedData, data) { + t.Errorf("mismatched mempack data. expected %v, got %v", expectedData, data) + } + + mempack.Reset() + + // After the reset, the object should now be unavailable. + { + obj, err := odb.Read(expectedId) + if err == nil { + t.Errorf("object %s unexpectedly found", obj.Id().String()) + obj.Free() + } else if !IsErrorCode(err, ErrNotFound) { + t.Errorf("unexpected error %v", err) + } + } +} diff --git a/merge.go b/merge.go index bfbf9a3..955289a 100644 --- a/merge.go +++ b/merge.go @@ -132,7 +132,7 @@ func (mo *MergeOptions) toC() *C.git_merge_options { } return &C.git_merge_options{ version: C.uint(mo.Version), - flags: C.git_merge_flag_t(mo.TreeFlags), + flags: C.uint32_t(mo.TreeFlags), rename_threshold: C.uint(mo.RenameThreshold), target_limit: C.uint(mo.TargetLimit), file_favor: C.git_merge_file_favor_t(mo.FileFavor), @@ -344,9 +344,29 @@ type MergeFileFlags int const ( MergeFileDefault MergeFileFlags = C.GIT_MERGE_FILE_DEFAULT - MergeFileStyleMerge MergeFileFlags = C.GIT_MERGE_FILE_STYLE_MERGE - MergeFileStyleDiff MergeFileFlags = C.GIT_MERGE_FILE_STYLE_DIFF3 + // Create standard conflicted merge files + MergeFileStyleMerge MergeFileFlags = C.GIT_MERGE_FILE_STYLE_MERGE + + // Create diff3-style files + MergeFileStyleDiff MergeFileFlags = C.GIT_MERGE_FILE_STYLE_DIFF3 + + // Condense non-alphanumeric regions for simplified diff file MergeFileStyleSimplifyAlnum MergeFileFlags = C.GIT_MERGE_FILE_SIMPLIFY_ALNUM + + // Ignore all whitespace + MergeFileIgnoreWhitespace MergeFileFlags = C.GIT_MERGE_FILE_IGNORE_WHITESPACE + + // Ignore changes in amount of whitespace + MergeFileIgnoreWhitespaceChange MergeFileFlags = C.GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE + + // Ignore whitespace at end of line + MergeFileIgnoreWhitespaceEOL MergeFileFlags = C.GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL + + // Use the "patience diff" algorithm + MergeFileDiffPatience MergeFileFlags = C.GIT_MERGE_FILE_DIFF_PATIENCE + + // Take extra time to find minimal diff + MergeFileDiffMinimal MergeFileFlags = C.GIT_MERGE_FILE_DIFF_MINIMAL ) type MergeFileOptions struct { @@ -355,6 +375,7 @@ type MergeFileOptions struct { TheirLabel string Favor MergeFileFavor Flags MergeFileFlags + MarkerSize uint16 } func mergeFileOptionsFromC(c C.git_merge_file_options) MergeFileOptions { @@ -364,6 +385,7 @@ func mergeFileOptionsFromC(c C.git_merge_file_options) MergeFileOptions { TheirLabel: C.GoString(c.their_label), Favor: MergeFileFavor(c.favor), Flags: MergeFileFlags(c.flags), + MarkerSize: uint16(c.marker_size), } } @@ -372,7 +394,8 @@ func populateCMergeFileOptions(c *C.git_merge_file_options, options MergeFileOpt c.our_label = C.CString(options.OurLabel) c.their_label = C.CString(options.TheirLabel) c.favor = C.git_merge_file_favor_t(options.Favor) - c.flags = C.git_merge_file_flag_t(options.Flags) + c.flags = C.uint32_t(options.Flags) + c.marker_size = C.ushort(options.MarkerSize) } func freeCMergeFileOptions(c *C.git_merge_file_options) { diff --git a/note.go b/note.go index 21bed57..9df1b9d 100644 --- a/note.go +++ b/note.go @@ -132,7 +132,7 @@ func (c *NoteCollection) DefaultRef() (string, error) { } ret := C.GoString(buf.ptr) - C.git_buf_free(&buf) + C.git_buf_dispose(&buf) return ret, nil } diff --git a/object.go b/object.go index 5505e35..2d75b06 100644 --- a/object.go +++ b/object.go @@ -13,12 +13,12 @@ import ( type ObjectType int const ( - ObjectAny ObjectType = C.GIT_OBJ_ANY - ObjectBad ObjectType = C.GIT_OBJ_BAD - ObjectCommit ObjectType = C.GIT_OBJ_COMMIT - ObjectTree ObjectType = C.GIT_OBJ_TREE - ObjectBlob ObjectType = C.GIT_OBJ_BLOB - ObjectTag ObjectType = C.GIT_OBJ_TAG + ObjectAny ObjectType = C.GIT_OBJECT_ANY + ObjectInvalid ObjectType = C.GIT_OBJECT_INVALID + ObjectCommit ObjectType = C.GIT_OBJECT_COMMIT + ObjectTree ObjectType = C.GIT_OBJECT_TREE + ObjectBlob ObjectType = C.GIT_OBJECT_BLOB + ObjectTag ObjectType = C.GIT_OBJECT_TAG ) type Object struct { @@ -35,8 +35,8 @@ func (t ObjectType) String() string { switch t { case ObjectAny: return "Any" - case ObjectBad: - return "Bad" + case ObjectInvalid: + return "Invalid" case ObjectCommit: return "Commit" case ObjectTree: @@ -67,7 +67,7 @@ func (o *Object) ShortId() (string, error) { if ecode < 0 { return "", MakeGitError(ecode) } - defer C.git_buf_free(&resultBuf) + defer C.git_buf_dispose(&resultBuf) return C.GoString(resultBuf.ptr), nil } @@ -217,7 +217,7 @@ func (o *Object) Peel(t ObjectType) (*Object, error) { runtime.LockOSThread() defer runtime.UnlockOSThread() - err := C.git_object_peel(&cobj, o.ptr, C.git_otype(t)) + err := C.git_object_peel(&cobj, o.ptr, C.git_object_t(t)) runtime.KeepAlive(o) if err < 0 { return nil, MakeGitError(err) diff --git a/object_test.go b/object_test.go index cc08210..4932dd2 100644 --- a/object_test.go +++ b/object_test.go @@ -108,7 +108,7 @@ func TestObjectOwner(t *testing.T) { func checkShortId(t *testing.T, Id, shortId string) { if len(shortId) < 7 || len(shortId) >= len(Id) { - t.Fatalf("bad shortId lenght %d", len(shortId)) + t.Fatalf("bad shortId length %d", len(shortId)) } if !strings.HasPrefix(Id, shortId) { diff --git a/odb.go b/odb.go index 64c5415..6489653 100644 --- a/odb.go +++ b/odb.go @@ -8,6 +8,7 @@ extern void _go_git_odb_backend_free(git_odb_backend *backend); */ import "C" import ( + "io" "reflect" "runtime" "unsafe" @@ -60,12 +61,12 @@ func (v *Odb) ReadHeader(oid *Oid) (uint64, ObjectType, error) { defer runtime.UnlockOSThread() var sz C.size_t - var cotype C.git_otype + var cotype C.git_object_t ret := C.git_odb_read_header(&sz, &cotype, v.ptr, oid.toC()) runtime.KeepAlive(v) if ret < 0 { - return 0, C.GIT_OBJ_BAD, MakeGitError(ret) + return 0, ObjectInvalid, MakeGitError(ret) } return uint64(sz), ObjectType(cotype), nil @@ -80,15 +81,19 @@ func (v *Odb) Exists(oid *Oid) bool { func (v *Odb) Write(data []byte, otype ObjectType) (oid *Oid, err error) { oid = new(Oid) - var cptr unsafe.Pointer - if len(data) > 0 { - cptr = unsafe.Pointer(&data[0]) - } runtime.LockOSThread() defer runtime.UnlockOSThread() - ret := C.git_odb_write(oid.toC(), v.ptr, cptr, C.size_t(len(data)), C.git_otype(otype)) + var size C.size_t + if len(data) > 0 { + size = C.size_t(len(data)) + } else { + data = []byte{0} + size = C.size_t(0) + } + + ret := C.git_odb_write(oid.toC(), v.ptr, unsafe.Pointer(&data[0]), size, C.git_object_t(otype)) runtime.KeepAlive(v) if ret < 0 { return nil, MakeGitError(ret) @@ -164,13 +169,19 @@ func (v *Odb) ForEach(callback OdbForEachCallback) error { // Hash determines the object-ID (sha1) of a data buffer. func (v *Odb) Hash(data []byte, otype ObjectType) (oid *Oid, err error) { oid = new(Oid) - header := (*reflect.SliceHeader)(unsafe.Pointer(&data)) - ptr := unsafe.Pointer(header.Data) runtime.LockOSThread() defer runtime.UnlockOSThread() - ret := C.git_odb_hash(oid.toC(), ptr, C.size_t(header.Len), C.git_otype(otype)) + var size C.size_t + if len(data) > 0 { + size = C.size_t(len(data)) + } else { + data = []byte{0} + size = C.size_t(0) + } + + ret := C.git_odb_hash(oid.toC(), unsafe.Pointer(&data[0]), size, C.git_object_t(otype)) runtime.KeepAlive(data) if ret < 0 { return nil, MakeGitError(ret) @@ -182,17 +193,21 @@ func (v *Odb) Hash(data []byte, otype ObjectType) (oid *Oid, err error) { // contents of the object. func (v *Odb) NewReadStream(id *Oid) (*OdbReadStream, error) { stream := new(OdbReadStream) + var ctype C.git_object_t + var csize C.size_t runtime.LockOSThread() defer runtime.UnlockOSThread() - ret := C.git_odb_open_rstream(&stream.ptr, v.ptr, id.toC()) + ret := C.git_odb_open_rstream(&stream.ptr, &csize, &ctype, v.ptr, id.toC()) runtime.KeepAlive(v) runtime.KeepAlive(id) if ret < 0 { return nil, MakeGitError(ret) } + stream.Size = uint64(csize) + stream.Type = ObjectType(ctype) runtime.SetFinalizer(stream, (*OdbReadStream).Free) return stream, nil } @@ -206,7 +221,7 @@ func (v *Odb) NewWriteStream(size int64, otype ObjectType) (*OdbWriteStream, err runtime.LockOSThread() defer runtime.UnlockOSThread() - ret := C.git_odb_open_wstream(&stream.ptr, v.ptr, C.git_off_t(size), C.git_otype(otype)) + ret := C.git_odb_open_wstream(&stream.ptr, v.ptr, C.git_object_size_t(size), C.git_object_t(otype)) runtime.KeepAlive(v) if ret < 0 { return nil, MakeGitError(ret) @@ -264,7 +279,9 @@ func (object *OdbObject) Data() (data []byte) { } type OdbReadStream struct { - ptr *C.git_odb_stream + ptr *C.git_odb_stream + Size uint64 + Type ObjectType } // Read reads from the stream @@ -281,6 +298,9 @@ func (stream *OdbReadStream) Read(data []byte) (int, error) { if ret < 0 { return 0, MakeGitError(ret) } + if ret == 0 { + return 0, io.EOF + } header.Len = int(ret) diff --git a/odb_test.go b/odb_test.go index 3d22fc9..46acdba 100644 --- a/odb_test.go +++ b/odb_test.go @@ -1,12 +1,14 @@ package git import ( + "bytes" "errors" "io" + "io/ioutil" "testing" ) -func TestOdbReadHeader(t *testing.T) { +func TestOdbRead(t *testing.T) { t.Parallel() repo := createTestRepo(t) defer cleanupTestRepo(t, repo) @@ -26,13 +28,27 @@ func TestOdbReadHeader(t *testing.T) { if err != nil { t.Fatalf("ReadHeader: %v", err) } - + if sz != uint64(len(data)) { t.Errorf("ReadHeader got size %d, want %d", sz, len(data)) } if typ != ObjectBlob { t.Errorf("ReadHeader got object type %s", typ) } + + obj, err := odb.Read(id) + if err != nil { + t.Fatalf("Read: %v", err) + } + if !bytes.Equal(obj.Data(), data) { + t.Errorf("Read got wrong data") + } + if sz := obj.Len(); sz != uint64(len(data)) { + t.Errorf("Read got size %d, want %d", sz, len(data)) + } + if typ := obj.Type(); typ != ObjectBlob { + t.Errorf("Read got object type %s", typ) + } } func TestOdbStream(t *testing.T) { @@ -47,22 +63,29 @@ func TestOdbStream(t *testing.T) { str := "hello, world!" - stream, error := odb.NewWriteStream(int64(len(str)), ObjectBlob) + writeStream, error := odb.NewWriteStream(int64(len(str)), ObjectBlob) checkFatal(t, error) - n, error := io.WriteString(stream, str) + n, error := io.WriteString(writeStream, str) checkFatal(t, error) if n != len(str) { t.Fatalf("Bad write length %v != %v", n, len(str)) } - error = stream.Close() + error = writeStream.Close() checkFatal(t, error) expectedId, error := NewOid("30f51a3fba5274d53522d0f19748456974647b4f") checkFatal(t, error) - if stream.Id.Cmp(expectedId) != 0 { + if writeStream.Id.Cmp(expectedId) != 0 { t.Fatal("Wrong data written") } + + readStream, error := odb.NewReadStream(&writeStream.Id) + checkFatal(t, error) + data, error := ioutil.ReadAll(readStream) + if str != string(data) { + t.Fatalf("Wrong data read %v != %v", str, string(data)) + } } func TestOdbHash(t *testing.T) { @@ -82,14 +105,16 @@ committer John Doe 1390682018 +0000 Initial commit.` - oid, error := odb.Hash([]byte(str), ObjectCommit) - checkFatal(t, error) + for _, data := range [][]byte{[]byte(str), doublePointerBytes()} { + oid, error := odb.Hash(data, ObjectCommit) + checkFatal(t, error) - coid, error := odb.Write([]byte(str), ObjectCommit) - checkFatal(t, error) + coid, error := odb.Write(data, ObjectCommit) + checkFatal(t, error) - if oid.Cmp(coid) != 0 { - t.Fatal("Hash and write Oids are different") + if oid.Cmp(coid) != 0 { + t.Fatal("Hash and write Oids are different") + } } } diff --git a/packbuilder.go b/packbuilder.go index 0e04bbf..576e5ca 100644 --- a/packbuilder.go +++ b/packbuilder.go @@ -85,6 +85,19 @@ func (pb *Packbuilder) InsertTree(id *Oid) error { return nil } +func (pb *Packbuilder) InsertWalk(walk *RevWalk) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_packbuilder_insert_walk(pb.ptr, walk.ptr) + runtime.KeepAlive(pb) + runtime.KeepAlive(walk) + if ret != 0 { + return MakeGitError(ret) + } + return nil +} + func (pb *Packbuilder) ObjectCount() uint32 { ret := uint32(C.git_packbuilder_object_count(pb.ptr)) runtime.KeepAlive(pb) diff --git a/patch.go b/patch.go index 75e843c..6a16b5f 100644 --- a/patch.go +++ b/patch.go @@ -51,7 +51,7 @@ func (patch *Patch) String() (string, error) { if ecode < 0 { return "", MakeGitError(ecode) } - defer C.git_buf_free(&buf) + defer C.git_buf_dispose(&buf) return C.GoString(buf.ptr), nil } @@ -69,7 +69,7 @@ func (v *Repository) PatchFromBuffers(oldPath, newPath string, oldBuf, newBuf [] var patchPtr *C.git_patch oldPtr := toPointer(oldBuf) - newPtr := (*C.char)(toPointer(newBuf)) + newPtr := toPointer(newBuf) cOldPath := C.CString(oldPath) defer C.free(unsafe.Pointer(cOldPath)) diff --git a/rebase.go b/rebase.go index 1f1324d..d29e183 100644 --- a/rebase.go +++ b/rebase.go @@ -6,6 +6,7 @@ package git import "C" import ( "errors" + "fmt" "runtime" "unsafe" ) @@ -16,6 +17,8 @@ type RebaseOperationType uint const ( // RebaseOperationPick The given commit is to be cherry-picked. The client should commit the changes and continue if there are no conflicts. RebaseOperationPick RebaseOperationType = C.GIT_REBASE_OPERATION_PICK + // RebaseOperationReword The given commit is to be cherry-picked, but the client should prompt the user to provide an updated commit message. + RebaseOperationReword RebaseOperationType = C.GIT_REBASE_OPERATION_REWORD // RebaseOperationEdit The given commit is to be cherry-picked, but the client should stop to allow the user to edit the changes before committing them. RebaseOperationEdit RebaseOperationType = C.GIT_REBASE_OPERATION_EDIT // RebaseOperationSquash The given commit is to be squashed into the previous commit. The commit message will be merged with the previous message. @@ -26,11 +29,29 @@ const ( RebaseOperationExec RebaseOperationType = C.GIT_REBASE_OPERATION_EXEC ) +func (t RebaseOperationType) String() string { + switch t { + case RebaseOperationPick: + return "pick" + case RebaseOperationReword: + return "reword" + case RebaseOperationEdit: + return "edit" + case RebaseOperationSquash: + return "squash" + case RebaseOperationFixup: + return "fixup" + case RebaseOperationExec: + return "exec" + } + return fmt.Sprintf("RebaseOperationType(%d)", t) +} + // Special value indicating that there is no currently active operation var RebaseNoOperation uint = ^uint(0) // Error returned if there is no current rebase operation -var ErrRebaseNoOperation = errors.New("o current rebase operation") +var ErrRebaseNoOperation = errors.New("no current rebase operation") // RebaseOperation describes a single instruction/operation to be performed during the rebase. type RebaseOperation struct { diff --git a/reference.go b/reference.go index e10c9b2..b5f5e47 100644 --- a/reference.go +++ b/reference.go @@ -284,7 +284,7 @@ func (v *Reference) Peel(t ObjectType) (*Object, error) { runtime.LockOSThread() defer runtime.UnlockOSThread() - err := C.git_reference_peel(&cobj, v.ptr, C.git_otype(t)) + err := C.git_reference_peel(&cobj, v.ptr, C.git_object_t(t)) runtime.KeepAlive(v) if err < 0 { return nil, MakeGitError(err) @@ -301,7 +301,7 @@ func (v *Reference) Owner() *Repository { } } -// Cmp compares both references, retursn 0 on equality, otherwise a +// Cmp compares v to ref2. It returns 0 on equality, otherwise a // stable sorting. func (v *Reference) Cmp(ref2 *Reference) int { ret := int(C.git_reference_cmp(v.ptr, ref2.ptr)) @@ -310,13 +310,14 @@ func (v *Reference) Cmp(ref2 *Reference) int { return ret } -// Shorthand ret :=s a "human-readable" short reference name +// Shorthand returns a "human-readable" short reference name. func (v *Reference) Shorthand() string { ret := C.GoString(C.git_reference_shorthand(v.ptr)) runtime.KeepAlive(v) return ret } +// Name returns the full name of v. func (v *Reference) Name() string { ret := C.GoString(C.git_reference_name(v.ptr)) runtime.KeepAlive(v) @@ -455,10 +456,12 @@ func (v *ReferenceIterator) Next() (*Reference, error) { } func newReferenceIteratorFromC(ptr *C.git_reference_iterator, r *Repository) *ReferenceIterator { - return &ReferenceIterator{ + iter := &ReferenceIterator{ ptr: ptr, repo: r, } + runtime.SetFinalizer(iter, (*ReferenceIterator).Free) + return iter } // Free the reference iterator diff --git a/remote.go b/remote.go index b4b1dd7..43ffd33 100644 --- a/remote.go +++ b/remote.go @@ -1,9 +1,11 @@ package git /* -#include #include +#include +#include + extern void _go_git_setup_callbacks(git_remote_callbacks *callbacks); */ diff --git a/repository.go b/repository.go index abf8b71..07b2605 100644 --- a/repository.go +++ b/repository.go @@ -3,6 +3,8 @@ package git /* #include #include +#include +#include */ import "C" import ( @@ -19,10 +21,10 @@ type Repository struct { Remotes RemoteCollection // Submodules represents the collection of submodules and can // be used to add, remove and configure submodules in this - // repostiory. + // repository. Submodules SubmoduleCollection // References represents the collection of references and can - // be used to create, remove or update refernces for this repository. + // be used to create, remove or update references for this repository. References ReferenceCollection // Notes represents the collection of notes and can be used to // read, write and delete notes from this repository. @@ -174,7 +176,7 @@ func (v *Repository) lookupType(id *Oid, t ObjectType) (*Object, error) { runtime.LockOSThread() defer runtime.UnlockOSThread() - ret := C.git_object_lookup(&ptr, v.ptr, id.toC(), C.git_otype(t)) + ret := C.git_object_lookup(&ptr, v.ptr, id.toC(), C.git_object_t(t)) runtime.KeepAlive(id) if ret < 0 { return nil, MakeGitError(ret) @@ -192,6 +194,7 @@ func (v *Repository) LookupTree(id *Oid) (*Tree, error) { if err != nil { return nil, err } + defer obj.Free() return obj.AsTree() } @@ -201,6 +204,7 @@ func (v *Repository) LookupCommit(id *Oid) (*Commit, error) { if err != nil { return nil, err } + defer obj.Free() return obj.AsCommit() } @@ -210,6 +214,7 @@ func (v *Repository) LookupBlob(id *Oid) (*Blob, error) { if err != nil { return nil, err } + defer obj.Free() return obj.AsBlob() } @@ -219,6 +224,7 @@ func (v *Repository) LookupTag(id *Oid) (*Tag, error) { if err != nil { return nil, err } + defer obj.Free() return obj.AsTag() } @@ -389,6 +395,74 @@ func (v *Repository) CreateCommit( return oid, nil } +func (v *Repository) CreateCommitFromIds( + refname string, author, committer *Signature, + message string, tree *Oid, parents ...*Oid) (*Oid, error) { + + oid := new(Oid) + + var cref *C.char + if refname == "" { + cref = nil + } else { + cref = C.CString(refname) + defer C.free(unsafe.Pointer(cref)) + } + + cmsg := C.CString(message) + defer C.free(unsafe.Pointer(cmsg)) + + var parentsarg **C.git_oid = nil + + nparents := len(parents) + if nparents > 0 { + // All this awful pointer arithmetic is needed to avoid passing a Go + // pointer to Go pointer into C. Other methods (like CreateCommits) are + // fine without this workaround because they are just passing Go pointers + // to C pointers, but arrays-of-pointers-to-git_oid are a bit special since + // both the array and the objects are allocated from Go. + var emptyOidPtr *C.git_oid + sizeofOidPtr := unsafe.Sizeof(emptyOidPtr) + parentsarg = (**C.git_oid)(C.calloc(C.size_t(uintptr(nparents)), C.size_t(sizeofOidPtr))) + defer C.free(unsafe.Pointer(parentsarg)) + parentsptr := uintptr(unsafe.Pointer(parentsarg)) + for _, v := range parents { + *(**C.git_oid)(unsafe.Pointer(parentsptr)) = v.toC() + parentsptr += sizeofOidPtr + } + } + + authorSig, err := author.toC() + if err != nil { + return nil, err + } + defer C.git_signature_free(authorSig) + + committerSig, err := committer.toC() + if err != nil { + return nil, err + } + defer C.git_signature_free(committerSig) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_commit_create_from_ids( + oid.toC(), v.ptr, cref, + authorSig, committerSig, + nil, cmsg, tree.toC(), C.size_t(nparents), parentsarg) + + runtime.KeepAlive(v) + runtime.KeepAlive(oid) + runtime.KeepAlive(tree) + runtime.KeepAlive(parents) + if ret < 0 { + return nil, MakeGitError(ret) + } + + return oid, nil +} + func (v *Odb) Free() { runtime.SetFinalizer(v, nil) C.git_odb_free(v.ptr) diff --git a/repository_test.go b/repository_test.go new file mode 100644 index 0000000..1950c69 --- /dev/null +++ b/repository_test.go @@ -0,0 +1,42 @@ +package git + +import ( + "testing" + "time" +) + +func TestCreateCommitFromIds(t *testing.T) { + t.Parallel() + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + loc, err := time.LoadLocation("Europe/Berlin") + checkFatal(t, err) + sig := &Signature{ + Name: "Rand Om Hacker", + Email: "random@hacker.com", + When: time.Date(2013, 03, 06, 14, 30, 0, 0, loc), + } + + idx, err := repo.Index() + checkFatal(t, err) + err = idx.AddByPath("README") + checkFatal(t, err) + err = idx.Write() + checkFatal(t, err) + treeId, err := idx.WriteTree() + checkFatal(t, err) + + message := "This is a commit\n" + tree, err := repo.LookupTree(treeId) + checkFatal(t, err) + expectedCommitId, err := repo.CreateCommit("HEAD", sig, sig, message, tree) + checkFatal(t, err) + + commitId, err := repo.CreateCommitFromIds("", sig, sig, message, treeId) + checkFatal(t, err) + + if !expectedCommitId.Equal(commitId) { + t.Errorf("mismatched commit ids, expected %v, got %v", expectedCommitId.String(), commitId.String()) + } +} diff --git a/reset_test.go b/reset_test.go index 45777e4..89ebc49 100644 --- a/reset_test.go +++ b/reset_test.go @@ -8,6 +8,8 @@ import ( func TestResetToCommit(t *testing.T) { t.Parallel() repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + seedTestRepo(t, repo) // create commit to reset to commitId, _ := updateReadme(t, repo, "testing reset") diff --git a/revert.go b/revert.go new file mode 100644 index 0000000..e745f11 --- /dev/null +++ b/revert.go @@ -0,0 +1,101 @@ +package git + +/* +#include +*/ +import "C" +import ( + "runtime" +) + +// RevertOptions contains options for performing a revert +type RevertOptions struct { + Mainline uint + MergeOpts MergeOptions + CheckoutOpts CheckoutOpts +} + +func (opts *RevertOptions) toC() *C.git_revert_options { + return &C.git_revert_options{ + version: C.GIT_REVERT_OPTIONS_VERSION, + mainline: C.uint(opts.Mainline), + merge_opts: *opts.MergeOpts.toC(), + checkout_opts: *opts.CheckoutOpts.toC(), + } +} + +func revertOptionsFromC(opts *C.git_revert_options) RevertOptions { + return RevertOptions{ + Mainline: uint(opts.mainline), + MergeOpts: mergeOptionsFromC(&opts.merge_opts), + CheckoutOpts: checkoutOptionsFromC(&opts.checkout_opts), + } +} + +func freeRevertOptions(opts *C.git_revert_options) { + freeCheckoutOpts(&opts.checkout_opts) +} + +// DefaultRevertOptions initialises a RevertOptions struct with default values +func DefaultRevertOptions() (RevertOptions, error) { + opts := C.git_revert_options{} + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ecode := C.git_revert_init_options(&opts, C.GIT_REVERT_OPTIONS_VERSION) + if ecode < 0 { + return RevertOptions{}, MakeGitError(ecode) + } + + defer freeRevertOptions(&opts) + return revertOptionsFromC(&opts), nil +} + +// Revert the provided commit leaving the index updated with the results of the revert +func (r *Repository) Revert(commit *Commit, revertOptions *RevertOptions) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + var cOpts *C.git_revert_options + + if revertOptions != nil { + cOpts = revertOptions.toC() + defer freeRevertOptions(cOpts) + } + + ecode := C.git_revert(r.ptr, commit.cast_ptr, cOpts) + runtime.KeepAlive(r) + runtime.KeepAlive(commit) + + if ecode < 0 { + return MakeGitError(ecode) + } + + return nil +} + +// RevertCommit reverts the provided commit against "ourCommit" +// The returned index contains the result of the revert and should be freed +func (r *Repository) RevertCommit(revertCommit *Commit, ourCommit *Commit, mainline uint, mergeOptions *MergeOptions) (*Index, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + var cOpts *C.git_merge_options + + if mergeOptions != nil { + cOpts = mergeOptions.toC() + } + + var index *C.git_index + + ecode := C.git_revert_commit(&index, r.ptr, revertCommit.cast_ptr, ourCommit.cast_ptr, C.uint(mainline), cOpts) + runtime.KeepAlive(revertCommit) + runtime.KeepAlive(ourCommit) + + if ecode < 0 { + return nil, MakeGitError(ecode) + } + + return newIndexFromC(index, r), nil +} diff --git a/revert_test.go b/revert_test.go new file mode 100644 index 0000000..fcf8e43 --- /dev/null +++ b/revert_test.go @@ -0,0 +1,76 @@ +package git + +import ( + "testing" +) + +const ( + expectedRevertedReadmeContents = "foo\n" +) + +func TestRevert(t *testing.T) { + t.Parallel() + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + seedTestRepo(t, repo) + commitID, _ := updateReadme(t, repo, content) + + commit, err := repo.LookupCommit(commitID) + checkFatal(t, err) + + revertOptions, err := DefaultRevertOptions() + checkFatal(t, err) + + err = repo.Revert(commit, &revertOptions) + checkFatal(t, err) + + actualReadmeContents := readReadme(t, repo) + + if actualReadmeContents != expectedRevertedReadmeContents { + t.Fatalf(`README has incorrect contents after revert. Expected: "%v", Actual: "%v"`, + expectedRevertedReadmeContents, actualReadmeContents) + } + + state := repo.State() + if state != RepositoryStateRevert { + t.Fatalf("Incorrect repository state. Expected: %v, Actual: %v", RepositoryStateRevert, state) + } + + err = repo.StateCleanup() + checkFatal(t, err) + + state = repo.State() + if state != RepositoryStateNone { + t.Fatalf("Incorrect repository state. Expected: %v, Actual: %v", RepositoryStateNone, state) + } +} + +func TestRevertCommit(t *testing.T) { + t.Parallel() + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + seedTestRepo(t, repo) + commitID, _ := updateReadme(t, repo, content) + + commit, err := repo.LookupCommit(commitID) + checkFatal(t, err) + + revertOptions, err := DefaultRevertOptions() + checkFatal(t, err) + + index, err := repo.RevertCommit(commit, commit, 0, &revertOptions.MergeOpts) + checkFatal(t, err) + defer index.Free() + + err = repo.CheckoutIndex(index, &revertOptions.CheckoutOpts) + checkFatal(t, err) + + actualReadmeContents := readReadme(t, repo) + + if actualReadmeContents != expectedRevertedReadmeContents { + t.Fatalf(`README has incorrect contents after revert. Expected: "%v", Actual: "%v"`, + expectedRevertedReadmeContents, actualReadmeContents) + } +} diff --git a/script/build-libgit2-dynamic.sh b/script/build-libgit2-dynamic.sh new file mode 100755 index 0000000..af037f3 --- /dev/null +++ b/script/build-libgit2-dynamic.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +exec "$(dirname "$0")/build-libgit2.sh" --dynamic diff --git a/script/build-libgit2-static.sh b/script/build-libgit2-static.sh index 5723721..1d28898 100755 --- a/script/build-libgit2-static.sh +++ b/script/build-libgit2-static.sh @@ -1,19 +1,5 @@ #!/bin/sh -set -ex +set -e -VENDORED_PATH=vendor/libgit2 - -cd $VENDORED_PATH && -mkdir -p install/lib && -mkdir -p build && -cd build && -cmake -DTHREADSAFE=ON \ - -DBUILD_CLAR=OFF \ - -DBUILD_SHARED_LIBS=OFF \ - -DCMAKE_C_FLAGS=-fPIC \ - -DCMAKE_BUILD_TYPE="RelWithDebInfo" \ - -DCMAKE_INSTALL_PREFIX=../install \ - .. && - -cmake --build . +exec "$(dirname "$0")/build-libgit2.sh" --static diff --git a/script/build-libgit2.sh b/script/build-libgit2.sh new file mode 100755 index 0000000..acbc84a --- /dev/null +++ b/script/build-libgit2.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +# Since CMake cannot build the static and dynamic libraries in the same +# directory, this script helps build both static and dynamic versions of it and +# have the common flags in one place instead of split between two places. + +set -e + +if [ "$#" -eq "0" ]; then + echo "Usage: $0 <--dynamic|--static>">&2 + exit 1 +fi + +ROOT="$(cd "$(dirname "$0")/.." && echo "${PWD}")" +VENDORED_PATH="${ROOT}/vendor/libgit2" + +case "$1" in + --static) + BUILD_PATH="${ROOT}/static-build" + BUILD_SHARED_LIBS=OFF + ;; + + --dynamic) + BUILD_PATH="${ROOT}/dynamic-build" + BUILD_SHARED_LIBS=ON + ;; + + *) + echo "Usage: $0 <--dynamic|--static>">&2 + exit 1 + ;; +esac + +mkdir -p "${BUILD_PATH}/build" "${BUILD_PATH}/install/lib" + +cd "${BUILD_PATH}/build" && +cmake -DTHREADSAFE=ON \ + -DBUILD_CLAR=OFF \ + -DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \ + -DREGEX_BACKEND=builtin \ + -DCMAKE_C_FLAGS=-fPIC \ + -DCMAKE_BUILD_TYPE="RelWithDebInfo" \ + -DCMAKE_INSTALL_PREFIX="${BUILD_PATH}/install" \ + "${VENDORED_PATH}" && + +exec cmake --build . --target install diff --git a/settings.go b/settings.go index c7f1850..b9bc216 100644 --- a/settings.go +++ b/settings.go @@ -31,7 +31,7 @@ import ( func SearchPath(level ConfigLevel) (string, error) { var buf C.git_buf - defer C.git_buf_free(&buf) + defer C.git_buf_dispose(&buf) runtime.LockOSThread() defer runtime.UnlockOSThread() diff --git a/signature.go b/signature.go index 16964d2..220fe57 100644 --- a/signature.go +++ b/signature.go @@ -26,7 +26,7 @@ func newSignatureFromC(sig *C.git_signature) *Signature { } } -// the offset in mintes, which is what git wants +// Offset returns the time zone offset of v.When in minutes, which is what git wants. func (v *Signature) Offset() int { _, offset := v.When.Zone() return offset / 60 diff --git a/stash.go b/stash.go index 8743da8..6624bce 100644 --- a/stash.go +++ b/stash.go @@ -171,7 +171,7 @@ func (opts *StashApplyOptions) toC() ( optsC = &C.git_stash_apply_options{ version: C.GIT_STASH_APPLY_OPTIONS_VERSION, - flags: C.git_stash_apply_flags(opts.Flags), + flags: C.uint32_t(opts.Flags), } populateCheckoutOpts(&optsC.checkout_options, &opts.CheckoutOptions) if opts.ProgressCallback != nil { diff --git a/submodule.go b/submodule.go index 406ed08..f4b4f15 100644 --- a/submodule.go +++ b/submodule.go @@ -15,7 +15,6 @@ import ( type SubmoduleUpdateOptions struct { *CheckoutOpts *FetchOptions - CloneCheckoutStrategy CheckoutStrategy } // Submodule @@ -369,7 +368,6 @@ func populateSubmoduleUpdateOptions(ptr *C.git_submodule_update_options, opts *S populateCheckoutOpts(&ptr.checkout_opts, opts.CheckoutOpts) populateFetchOptions(&ptr.fetch_opts, opts.FetchOptions) - ptr.clone_checkout_strategy = C.uint(opts.CloneCheckoutStrategy) return nil } diff --git a/tag.go b/tag.go index 4debdb7..1bea2b7 100644 --- a/tag.go +++ b/tag.go @@ -21,26 +21,26 @@ func (t *Tag) AsObject() *Object { return &t.Object } -func (t Tag) Message() string { +func (t *Tag) Message() string { ret := C.GoString(C.git_tag_message(t.cast_ptr)) runtime.KeepAlive(t) return ret } -func (t Tag) Name() string { +func (t *Tag) Name() string { ret := C.GoString(C.git_tag_name(t.cast_ptr)) runtime.KeepAlive(t) return ret } -func (t Tag) Tagger() *Signature { +func (t *Tag) Tagger() *Signature { cast_ptr := C.git_tag_tagger(t.cast_ptr) ret := newSignatureFromC(cast_ptr) runtime.KeepAlive(t) return ret } -func (t Tag) Target() *Object { +func (t *Tag) Target() *Object { var ptr *C.git_object ret := C.git_tag_target(&ptr, t.cast_ptr) runtime.KeepAlive(t) @@ -51,13 +51,13 @@ func (t Tag) Target() *Object { return allocObject(ptr, t.repo) } -func (t Tag) TargetId() *Oid { +func (t *Tag) TargetId() *Oid { ret := newOidFromC(C.git_tag_target_id(t.cast_ptr)) runtime.KeepAlive(t) return ret } -func (t Tag) TargetType() ObjectType { +func (t *Tag) TargetType() ObjectType { ret := ObjectType(C.git_tag_target_type(t.cast_ptr)) runtime.KeepAlive(t) return ret diff --git a/tree.go b/tree.go index ee14ec5..b309193 100644 --- a/tree.go +++ b/tree.go @@ -47,17 +47,18 @@ func newTreeEntry(entry *C.git_tree_entry) *TreeEntry { } } -func (t Tree) EntryByName(filename string) *TreeEntry { +func (t *Tree) EntryByName(filename string) *TreeEntry { cname := C.CString(filename) defer C.free(unsafe.Pointer(cname)) entry := C.git_tree_entry_byname(t.cast_ptr, cname) - runtime.KeepAlive(t) if entry == nil { return nil } - return newTreeEntry(entry) + goEntry := newTreeEntry(entry) + runtime.KeepAlive(t) + return goEntry } // EntryById performs a lookup for a tree entry with the given SHA value. @@ -66,23 +67,24 @@ func (t Tree) EntryByName(filename string) *TreeEntry { // free it, but you must not use it after the Tree is freed. // // Warning: this must examine every entry in the tree, so it is not fast. -func (t Tree) EntryById(id *Oid) *TreeEntry { +func (t *Tree) EntryById(id *Oid) *TreeEntry { runtime.LockOSThread() defer runtime.UnlockOSThread() entry := C.git_tree_entry_byid(t.cast_ptr, id.toC()) - runtime.KeepAlive(t) runtime.KeepAlive(id) if entry == nil { return nil } - return newTreeEntry(entry) + goEntry := newTreeEntry(entry) + runtime.KeepAlive(t) + return goEntry } // EntryByPath looks up an entry by its full path, recursing into // deeper trees if necessary (i.e. if there are slashes in the path) -func (t Tree) EntryByPath(path string) (*TreeEntry, error) { +func (t *Tree) EntryByPath(path string) (*TreeEntry, error) { cpath := C.CString(path) defer C.free(unsafe.Pointer(cpath)) var entry *C.git_tree_entry @@ -100,17 +102,18 @@ func (t Tree) EntryByPath(path string) (*TreeEntry, error) { return newTreeEntry(entry), nil } -func (t Tree) EntryByIndex(index uint64) *TreeEntry { +func (t *Tree) EntryByIndex(index uint64) *TreeEntry { entry := C.git_tree_entry_byindex(t.cast_ptr, C.size_t(index)) - runtime.KeepAlive(t) if entry == nil { return nil } - return newTreeEntry(entry) + goEntry := newTreeEntry(entry) + runtime.KeepAlive(t) + return goEntry } -func (t Tree) EntryCount() uint64 { +func (t *Tree) EntryCount() uint64 { num := C.git_tree_entrycount(t.cast_ptr) runtime.KeepAlive(t) return uint64(num) @@ -119,9 +122,8 @@ func (t Tree) EntryCount() uint64 { type TreeWalkCallback func(string, *TreeEntry) int //export CallbackGitTreeWalk -func CallbackGitTreeWalk(_root *C.char, _entry unsafe.Pointer, ptr unsafe.Pointer) C.int { +func CallbackGitTreeWalk(_root *C.char, entry *C.git_tree_entry, ptr unsafe.Pointer) C.int { root := C.GoString(_root) - entry := (*C.git_tree_entry)(_entry) if callback, ok := pointerHandles.Get(ptr).(TreeWalkCallback); ok { return C.int(callback(root, newTreeEntry(entry))) @@ -130,7 +132,7 @@ func CallbackGitTreeWalk(_root *C.char, _entry unsafe.Pointer, ptr unsafe.Pointe } } -func (t Tree) Walk(callback TreeWalkCallback) error { +func (t *Tree) Walk(callback TreeWalkCallback) error { runtime.LockOSThread() defer runtime.UnlockOSThread() diff --git a/vendor/libgit2 b/vendor/libgit2 index df4dfaa..ee3307a 160000 --- a/vendor/libgit2 +++ b/vendor/libgit2 @@ -1 +1 @@ -Subproject commit df4dfaadcf709646ebab2e57e3589952cf1ac809 +Subproject commit ee3307a183e39d602b25fa94831c6fc09e7c1b61 diff --git a/wrapper.c b/wrapper.c index 11c2f32..3656773 100644 --- a/wrapper.c +++ b/wrapper.c @@ -2,6 +2,7 @@ #include #include #include +#include typedef int (*gogit_submodule_cbk)(git_submodule *sm, const char *name, void *payload); @@ -180,4 +181,8 @@ void _go_git_writestream_free(git_writestream *stream) stream->free(stream); } +git_credtype_t _go_git_cred_credtype(git_cred *cred) { + return cred->credtype; +} + /* EOF */