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 dd9f97a..b25a052 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,29 @@ language: go go: - - 1.7 - - 1.8 - - 1.9 + - "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: false - -before_install: - - git submodule update --init + 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/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 ab9af38..7260089 100644 --- a/config.go +++ b/config.go @@ -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 3088320..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 ( 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 00828a5..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 != 26 -# error "Invalid libgit2 version; this git2go supports libgit2 v0.26" +#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 b42d49f..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 != 26 -# error "Invalid libgit2 version; this git2go supports libgit2 v0.26" +#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 5106516..50538bd 100644 --- a/index.go +++ b/index.go @@ -90,7 +90,9 @@ func populateCIndexEntry(source *IndexEntry, dest *C.git_index_entry) { dest.uid = C.uint32_t(source.Uid) dest.gid = C.uint32_t(source.Gid) dest.file_size = C.uint32_t(source.Size) - dest.id = *source.Id.toC() + if source.Id != nil { + dest.id = *source.Id.toC() + } dest.path = C.CString(source.Path) } @@ -145,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 { @@ -181,6 +197,28 @@ func (v *Index) AddByPath(path string) error { return nil } +// AddFromBuffer adds or replaces an index entry from a buffer in memory +func (v *Index) AddFromBuffer(entry *IndexEntry, buffer []byte) error { + var centry C.git_index_entry + + populateCIndexEntry(entry, ¢ry) + defer freeCIndexEntry(¢ry) + + var cbuffer unsafe.Pointer + if len(buffer) > 0 { + cbuffer = unsafe.Pointer(&buffer[0]) + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if err := C.git_index_add_from_buffer(v.ptr, ¢ry, cbuffer, C.size_t(len(buffer))); err < 0 { + return MakeGitError(err) + } + + return nil +} + func (v *Index) AddAll(pathspecs []string, flags IndexAddOpts, callback IndexMatchedPathCallback) error { cpathspecs := C.git_strarray{} cpathspecs.count = C.size_t(len(pathspecs)) diff --git a/index_test.go b/index_test.go index f47dace..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" { @@ -149,6 +165,30 @@ func TestIndexRemoveDirectory(t *testing.T) { } } +func TestIndexAddFromBuffer(t *testing.T) { + t.Parallel() + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + idx, err := repo.Index() + checkFatal(t, err) + + entry := IndexEntry{ + Path: "README", + Mode: FilemodeBlob, + } + + err = idx.AddFromBuffer(&entry, []byte("foo\n")) + checkFatal(t, err) + + treeId, err := idx.WriteTreeTo(repo) + checkFatal(t, err) + + if treeId.String() != "b7119b11e8ef7a1a5a34d3ac87f5b075228ac81e" { + t.Fatalf("%v", treeId.String()) + } +} + func TestIndexAddAllNoCallback(t *testing.T) { t.Parallel() repo := createTestRepo(t) 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 cdf7f75..ab30fb2 100644 --- a/merge.go +++ b/merge.go @@ -157,7 +157,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), @@ -369,9 +369,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 { @@ -380,6 +400,7 @@ type MergeFileOptions struct { TheirLabel string Favor MergeFileFavor Flags MergeFileFlags + MarkerSize uint16 } func mergeFileOptionsFromC(c C.git_merge_file_options) MergeFileOptions { @@ -389,6 +410,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), } } @@ -397,7 +419,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/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 7e6f71d..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 } diff --git a/rebase.go b/rebase.go index 5206fca..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,6 +29,24 @@ 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) 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 d8de97a..07b2605 100644 --- a/repository.go +++ b/repository.go @@ -3,6 +3,8 @@ package git /* #include #include +#include +#include */ import "C" import ( @@ -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/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 f1323d9..ee3307a 160000 --- a/vendor/libgit2 +++ b/vendor/libgit2 @@ -1 +1 @@ -Subproject commit f1323d9c161aeeada190fd9615a8b5a9fb8a7f3e +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 */