Merge pull request #362 from libgit2/cmn/master-tip-static

Update master to latest libgit2 and build statically
This commit is contained in:
Carlos Martín Nieto 2017-01-20 22:55:25 +00:00 committed by GitHub
commit 4a14260153
20 changed files with 804 additions and 84 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "vendor/libgit2"]
path = vendor/libgit2
url = https://github.com/libgit2/libgit2

View File

@ -5,10 +5,6 @@ sudo: required
install: ./script/install-libgit2.sh
go:
- 1.1
- 1.2
- 1.3
- 1.4
- 1.5
- 1.6
- 1.7

View File

@ -1,8 +1,11 @@
default: test
test:
build-libgit2:
./script/build-libgit2-static.sh
test: build-libgit2
go run script/check-MakeGitError-thread-lock.go
go test ./...
install:
install: build-libgit2
go install ./...

View File

@ -2,57 +2,59 @@ git2go
======
[![GoDoc](https://godoc.org/github.com/libgit2/git2go?status.svg)](http://godoc.org/github.com/libgit2/git2go) [![Build Status](https://travis-ci.org/libgit2/git2go.svg?branch=master)](https://travis-ci.org/libgit2/git2go)
Go bindings for [libgit2](http://libgit2.github.com/).
Go bindings for [libgit2](http://libgit2.github.com/). The `master` branch follows the latest libgit2 release. The versioned branches indicate which libgit2 version they work against.
### Which branch to use
The numbered branches work against the version of libgit2 as specified by their number. You can import them in your project via gopkg.in, e.g. if you have libgit2 v0.25 installed you'd import with
import "gopkg.in/libgit2/git2go.v25"
which will ensure there are no sudden changes to the API.
The `master` branch follows the tip of libgit2 itself (with some lag) and as such has no guarantees on its own API nor does it have expectations the stability of libgit2's. Thus this only supports statically linking against libgit2.
Installing
----------
This project wraps the functionality provided by libgit2. If you're using a stable version, install it to your system via your system's package manager and then install git2go as usual.
This project wraps the functionality provided by libgit2. It thus needs it in order to perform the work.
Otherwise (`next` which tracks an unstable version), we need to build libgit2 as well. In order to build it, you need `cmake`, `pkg-config` and a C compiler. You will also need the development packages for OpenSSL and LibSSH2 installed if you want libgit2 to support HTTPS and SSH respectively.
This project wraps the functionality provided by libgit2. If you're using a versioned branch, install it to your system via your system's package manager and then install git2go.
### Stable version
git2go has `master` which tracks the latest release of libgit2, and versioned branches which indicate which version of libgit2 they work against. Install the development package on your system via your favourite package manager or from source and you can use a service like gopkg.in to use the appropriate version. For the libgit2 v0.22 case, you can use
### Versioned branch, dynamic linking
import "gopkg.in/libgit2/git2go.v22"
When linking dynamically against a released version of libgit2, install it via your system's package manager. CGo will take care of finding its pkg-config file and set up the linking. Import via gopkg.in, e.g. to work against libgit2 v0.25
to use a version of git2go which will work against libgit2 v0.22 and dynamically link to the library. You can use
import "gopkg.in/libgit2/git2go.v25"
import "github.com/libgit2/git2go"
### Master branch, or static linking
to use the version which works against the latest release.
### From `next`
The `next` branch follows libgit2's master branch, which means there is no stable API or ABI to link against. git2go can statically link against a vendored version of libgit2.
If using `master` or building a branch statically, we need to build libgit2 first. In order to build it, you need `cmake`, `pkg-config` and a C compiler. You will also need the development packages for OpenSSL (outside of Windows or macOS) and LibSSH2 installed if you want libgit2 to support HTTPS and SSH respectively. Note that even if libgit2 is included in the resulting binary, its dependencies will not be.
Run `go get -d github.com/libgit2/git2go` to download the code and go to your `$GOPATH/src/github.com/libgit2/git2go` directory. From there, we need to build the C code and put it into the resulting go binary.
git checkout next
git submodule update --init # get libgit2
make install
will compile libgit2. Run `go install` so that it's statically linked to the git2go package.
will compile libgit2, link it into git2go and install it.
Parallelism and network operations
----------------------------------
libgit2 uses OpenSSL and LibSSH2 for performing encrypted network connections. For now, git2go asks libgit2 to set locking for OpenSSL. This makes HTTPS connections thread-safe, but it is fragile and will likely stop doing it soon. This may also make SSH connections thread-safe if your copy of libssh2 is linked against OpenSSL. Check libgit2's `THREADSAFE.md` for more information.
libgit2 may use OpenSSL and LibSSH2 for performing encrypted network connections. For now, git2go asks libgit2 to set locking for OpenSSL. This makes HTTPS connections thread-safe, but it is fragile and will likely stop doing it soon. This may also make SSH connections thread-safe if your copy of libssh2 is linked against OpenSSL. Check libgit2's `THREADSAFE.md` for more information.
Running the tests
-----------------
For the stable version, `go test` will work as usual. For the `next` branch, similarly to installing, running the tests requires linking against the local libgit2 library, so the Makefile provides a wrapper
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
make test
Alternatively, if you want to pass arguments to `go test`, you can use the script that sets it all up
Alternatively, you can build the library manually first and then run the tests
./script/with-static.sh go test -v
which will run the specified arguments with the correct environment variables.
./script/build-libgit2-static.sh
go test -v
License
-------

76
blob.go
View File

@ -4,15 +4,13 @@ package git
#include <git2.h>
#include <string.h>
extern int _go_git_blob_create_fromchunks(git_oid *id,
git_repository *repo,
const char *hintpath,
void *payload);
int _go_git_writestream_write(git_writestream *stream, const char *buffer, size_t len);
void _go_git_writestream_free(git_writestream *stream);
*/
import "C"
import (
"io"
"reflect"
"runtime"
"unsafe"
)
@ -87,27 +85,71 @@ func blobChunkCb(buffer *C.char, maxLen C.size_t, handle unsafe.Pointer) int {
return len(goBuf)
}
func (repo *Repository) CreateBlobFromChunks(hintPath string, callback BlobChunkCallback) (*Oid, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
func (repo *Repository) CreateFromStream(hintPath string) (*BlobWriteStream, error) {
var chintPath *C.char = nil
var stream *C.git_writestream
if len(hintPath) > 0 {
chintPath = C.CString(hintPath)
defer C.free(unsafe.Pointer(chintPath))
}
oid := C.git_oid{}
payload := &BlobCallbackData{Callback: callback}
handle := pointerHandles.Track(payload)
defer pointerHandles.Untrack(handle)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ecode := C._go_git_blob_create_fromchunks(&oid, repo.ptr, chintPath, handle)
if payload.Error != nil {
return nil, payload.Error
}
ecode := C.git_blob_create_fromstream(&stream, repo.ptr, chintPath)
if ecode < 0 {
return nil, MakeGitError(ecode)
}
return newBlobWriteStreamFromC(stream), nil
}
type BlobWriteStream struct {
ptr *C.git_writestream
}
func newBlobWriteStreamFromC(ptr *C.git_writestream) *BlobWriteStream {
stream := &BlobWriteStream{
ptr: ptr,
}
runtime.SetFinalizer(stream, (*BlobWriteStream).Free)
return stream
}
// Implement io.Writer
func (stream *BlobWriteStream) Write(p []byte) (int, error) {
header := (*reflect.SliceHeader)(unsafe.Pointer(&p))
ptr := (*C.char)(unsafe.Pointer(header.Data))
size := C.size_t(header.Len)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ecode := C._go_git_writestream_write(stream.ptr, ptr, size)
if ecode < 0 {
return 0, MakeGitError(ecode)
}
return len(p), nil
}
func (stream *BlobWriteStream) Free() {
runtime.SetFinalizer(stream, nil)
C._go_git_writestream_free(stream.ptr)
}
func (stream *BlobWriteStream) Commit() (*Oid, error) {
oid := C.git_oid{}
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ecode := C.git_blob_create_fromstream_commit(&oid, stream.ptr)
if ecode < 0 {
return nil, MakeGitError(ecode)
}
return newOidFromC(&oid), nil
}

View File

@ -22,6 +22,10 @@ func (c Commit) Message() string {
return C.GoString(C.git_commit_message(c.cast_ptr))
}
func (c Commit) RawMessage() string {
return C.GoString(C.git_commit_message_raw(c.cast_ptr))
}
func (c Commit) Summary() string {
return C.GoString(C.git_commit_summary(c.cast_ptr))
}

View File

@ -93,14 +93,16 @@ func TestDescribeCommit(t *testing.T) {
func checkDescribeNoRefsFound(t *testing.T, err error) {
// The failure happens at wherever we were called, not here
_, file, line, ok := runtime.Caller(1)
expectedString := "no reference found, cannot describe anything"
if !ok {
t.Fatalf("Unable to get caller")
}
if err == nil || !strings.Contains(err.Error(), "No reference found, cannot describe anything") {
if err == nil || !strings.Contains(err.Error(), expectedString) {
t.Fatalf(
"%s:%v: was expecting error 'No reference found, cannot describe anything', got %v",
"%s:%v: was expecting error %v, got %v",
path.Base(file),
line,
expectedString,
err,
)
}

View File

@ -20,6 +20,7 @@ const (
DiffFlagBinary DiffFlag = C.GIT_DIFF_FLAG_BINARY
DiffFlagNotBinary DiffFlag = C.GIT_DIFF_FLAG_NOT_BINARY
DiffFlagValidOid DiffFlag = C.GIT_DIFF_FLAG_VALID_ID
DiffFlagExists DiffFlag = C.GIT_DIFF_FLAG_EXISTS
)
type Delta int
@ -34,6 +35,8 @@ const (
DeltaIgnored Delta = C.GIT_DELTA_IGNORED
DeltaUntracked Delta = C.GIT_DELTA_UNTRACKED
DeltaTypeChange Delta = C.GIT_DELTA_TYPECHANGE
DeltaUnreadable Delta = C.GIT_DELTA_UNREADABLE
DeltaConflicted Delta = C.GIT_DELTA_CONFLICTED
)
type DiffLineType int
@ -399,6 +402,7 @@ const (
DiffIgnoreFilemode DiffOptionsFlag = C.GIT_DIFF_IGNORE_FILEMODE
DiffIgnoreSubmodules DiffOptionsFlag = C.GIT_DIFF_IGNORE_SUBMODULES
DiffIgnoreCase DiffOptionsFlag = C.GIT_DIFF_IGNORE_CASE
DiffIncludeCaseChange DiffOptionsFlag = C.GIT_DIFF_INCLUDE_CASECHANGE
DiffDisablePathspecMatch DiffOptionsFlag = C.GIT_DIFF_DISABLE_PATHSPEC_MATCH
DiffSkipBinaryCheck DiffOptionsFlag = C.GIT_DIFF_SKIP_BINARY_CHECK

30
features.go Normal file
View File

@ -0,0 +1,30 @@
package git
/*
#include <git2.h>
*/
import "C"
type Feature int
const (
// libgit2 was built with threading support
FeatureThreads Feature = C.GIT_FEATURE_THREADS
// libgit2 was built with HTTPS support built-in
FeatureHttps Feature = C.GIT_FEATURE_HTTPS
// libgit2 was build with SSH support built-in
FeatureSsh Feature = C.GIT_FEATURE_SSH
// libgit2 was built with nanosecond support for files
FeatureNSec Feature = C.GIT_FEATURE_NSEC
)
// Features returns a bit-flag of Feature values indicating which features the
// loaded libgit2 library has.
func Features() Feature {
features := C.git_libgit2_features()
return Feature(features)
}

18
git.go
View File

@ -1,12 +1,15 @@
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
#include <git2.h>
#include <git2/sys/openssl.h>
#cgo pkg-config: libgit2
#if LIBGIT2_VER_MAJOR != 0 || LIBGIT2_VER_MINOR != 24
# error "Invalid libgit2 version; this git2go supports libgit2 v0.24"
#if LIBGIT2_VER_MAJOR != 0 || LIBGIT2_VER_MINOR != 25
# error "Invalid libgit2 version; this git2go supports libgit2 v0.25"
#endif
*/
@ -125,6 +128,15 @@ func init() {
C.git_libgit2_init()
// Due to the multithreaded nature of Go and its interaction with
// calling C functions, we cannot work with a library that was not built
// with multi-threading support. The most likely outcome is a segfault
// or panic at an incomprehensible time, so let's make it easy by
// panicking right here.
if Features()&FeatureThreads == 0 {
panic("libgit2 was not built with threading support")
}
// This is not something we should be doing, as we may be
// stomping all over someone else's setup. The user should do
// this themselves or use some binding/wrapper which does it

View File

@ -93,6 +93,8 @@ func updateReadme(t *testing.T, repo *Repository, content string) (*Oid, *Oid) {
checkFatal(t, err)
err = idx.AddByPath("README")
checkFatal(t, err)
err = idx.Write()
checkFatal(t, err)
treeId, err := idx.WriteTree()
checkFatal(t, err)

4
odb.go
View File

@ -226,6 +226,10 @@ func (object *OdbObject) Len() (len uint64) {
return uint64(C.git_odb_object_size(object.ptr))
}
func (object *OdbObject) Type() ObjectType {
return ObjectType(C.git_odb_object_type(object.ptr))
}
func (object *OdbObject) Data() (data []byte) {
var c_blob unsafe.Pointer = C.git_odb_object_data(object.ptr)
var blob []byte

View File

@ -117,6 +117,30 @@ type FetchOptions struct {
Headers []string
}
type ProxyType uint
const (
// Do not attempt to connect through a proxy
//
// If built against lbicurl, it itself may attempt to connect
// to a proxy if the environment variables specify it.
ProxyTypeNone ProxyType = C.GIT_PROXY_NONE
// Try to auto-detect the proxy from the git configuration.
ProxyTypeAuto ProxyType = C.GIT_PROXY_AUTO
// Connect via the URL given in the options
ProxyTypeSpecified ProxyType = C.GIT_PROXY_SPECIFIED
)
type ProxyOptions struct {
// The type of proxy to use (or none)
Type ProxyType
// The proxy's URL
Url string
}
type Remote struct {
ptr *C.git_remote
callbacks RemoteCallbacks
@ -320,6 +344,20 @@ func pushUpdateReferenceCallback(refname, status *C.char, data unsafe.Pointer) i
return int(callbacks.PushUpdateReferenceCallback(C.GoString(refname), C.GoString(status)))
}
func populateProxyOptions(ptr *C.git_proxy_options, opts *ProxyOptions) {
C.git_proxy_init_options(ptr, C.GIT_PROXY_OPTIONS_VERSION)
if opts == nil {
return
}
ptr._type = C.git_proxy_t(opts.Type)
ptr.url = C.CString(opts.Url)
}
func freeProxyOptions(ptr *C.git_proxy_options) {
C.free(unsafe.Pointer(ptr.url))
}
func RemoteIsValidName(name string) bool {
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
@ -674,12 +712,12 @@ func (o *Remote) Fetch(refspecs []string, opts *FetchOptions, msg string) error
return nil
}
func (o *Remote) ConnectFetch(callbacks *RemoteCallbacks, headers []string) error {
return o.Connect(ConnectDirectionFetch, callbacks, headers)
func (o *Remote) ConnectFetch(callbacks *RemoteCallbacks, proxyOpts *ProxyOptions, headers []string) error {
return o.Connect(ConnectDirectionFetch, callbacks, proxyOpts, headers)
}
func (o *Remote) ConnectPush(callbacks *RemoteCallbacks, headers []string) error {
return o.Connect(ConnectDirectionPush, callbacks, headers)
func (o *Remote) ConnectPush(callbacks *RemoteCallbacks, proxyOpts *ProxyOptions, headers []string) error {
return o.Connect(ConnectDirectionPush, callbacks, proxyOpts, headers)
}
// Connect opens a connection to a remote.
@ -689,19 +727,24 @@ func (o *Remote) ConnectPush(callbacks *RemoteCallbacks, headers []string) error
// starts up a specific binary which can only do the one or the other.
//
// 'headers' are extra HTTP headers to use in this connection.
func (o *Remote) Connect(direction ConnectDirection, callbacks *RemoteCallbacks, headers []string) error {
func (o *Remote) Connect(direction ConnectDirection, callbacks *RemoteCallbacks, proxyOpts *ProxyOptions, headers []string) error {
var ccallbacks C.git_remote_callbacks
populateRemoteCallbacks(&ccallbacks, callbacks)
var cproxy C.git_proxy_options
populateProxyOptions(&cproxy, proxyOpts)
defer freeProxyOptions(&cproxy)
cheaders := C.git_strarray{}
cheaders.count = C.size_t(len(headers))
cheaders.strings = makeCStringsFromStrings(headers)
defer freeStrarray(&cheaders)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if ret := C.git_remote_connect(o.ptr, C.git_direction(direction), &ccallbacks, &cheaders); ret != 0 {
if ret := C.git_remote_connect(o.ptr, C.git_direction(direction), &ccallbacks, &cproxy, &cheaders); ret != 0 {
return MakeGitError(ret)
}
return nil

View File

@ -61,7 +61,7 @@ func TestRemoteConnect(t *testing.T) {
remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository")
checkFatal(t, err)
err = remote.ConnectFetch(nil, nil)
err = remote.ConnectFetch(nil, nil, nil)
checkFatal(t, err)
}
@ -73,7 +73,7 @@ func TestRemoteLs(t *testing.T) {
remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository")
checkFatal(t, err)
err = remote.ConnectFetch(nil, nil)
err = remote.ConnectFetch(nil, nil, nil)
checkFatal(t, err)
heads, err := remote.Ls()
@ -92,7 +92,7 @@ func TestRemoteLsFiltering(t *testing.T) {
remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository")
checkFatal(t, err)
err = remote.ConnectFetch(nil, nil)
err = remote.ConnectFetch(nil, nil, nil)
checkFatal(t, err)
heads, err := remote.Ls("master")
@ -173,7 +173,7 @@ func TestRemotePrune(t *testing.T) {
rr, err := repo.Remotes.Lookup("origin")
checkFatal(t, err)
err = rr.ConnectFetch(nil, nil)
err = rr.ConnectFetch(nil, nil, nil)
checkFatal(t, err)
err = rr.Prune(nil)

View File

@ -30,6 +30,9 @@ type Repository struct {
// Tags represents the collection of tags and can be used to create,
// list, iterate and remove tags in this repository.
Tags TagsCollection
// Stashes represents the collection of stashes and can be used to
// save, apply and iterate over stash states in this repository.
Stashes StashCollection
}
func newRepositoryFromC(ptr *C.git_repository) *Repository {
@ -40,6 +43,7 @@ func newRepositoryFromC(ptr *C.git_repository) *Repository {
repo.References.repo = repo
repo.Notes.repo = repo
repo.Tags.repo = repo
repo.Stashes.repo = repo
runtime.SetFinalizer(repo, (*Repository).Free)
@ -264,6 +268,40 @@ func (v *Repository) IsHeadDetached() (bool, error) {
return ret != 0, nil
}
func (v *Repository) IsHeadUnborn() (bool, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_repository_head_unborn(v.ptr)
if ret < 0 {
return false, MakeGitError(ret)
}
return ret != 0, nil
}
func (v *Repository) IsEmpty() (bool, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_repository_is_empty(v.ptr)
if ret < 0 {
return false, MakeGitError(ret)
}
return ret != 0, nil
}
func (v *Repository) IsShallow() (bool, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_repository_is_shallow(v.ptr)
if ret < 0 {
return false, MakeGitError(ret)
}
return ret != 0, nil
}
func (v *Repository) Walk() (*RevWalk, error) {
var walkPtr *C.git_revwalk

View File

@ -1,12 +0,0 @@
#!/bin/sh
set -ex
export BUILD="$PWD/vendor/libgit2/build"
export PCFILE="$BUILD/libgit2.pc"
FLAGS=$(pkg-config --static --libs $PCFILE) || exit 1
export CGO_LDFLAGS="$BUILD/libgit2.a -L$BUILD ${FLAGS}"
export CGO_CFLAGS="-I$PWD/vendor/libgit2/include"
$@

338
stash.go Normal file
View File

@ -0,0 +1,338 @@
package git
/*
#include <git2.h>
extern void _go_git_setup_stash_apply_progress_callbacks(git_stash_apply_options *opts);
extern int _go_git_stash_foreach(git_repository *repo, void *payload);
*/
import "C"
import (
"runtime"
"unsafe"
)
// StashFlag are flags that affect the stash save operation.
type StashFlag int
const (
// StashDefault represents no option, default.
StashDefault StashFlag = C.GIT_STASH_DEFAULT
// StashKeepIndex leaves all changes already added to the
// index intact in the working directory.
StashKeepIndex StashFlag = C.GIT_STASH_KEEP_INDEX
// StashIncludeUntracked means all untracked files are also
// stashed and then cleaned up from the working directory.
StashIncludeUntracked StashFlag = C.GIT_STASH_INCLUDE_UNTRACKED
// StashIncludeIgnored means all ignored files are also
// stashed and then cleaned up from the working directory.
StashIncludeIgnored StashFlag = C.GIT_STASH_INCLUDE_IGNORED
)
// StashCollection represents the possible operations that can be
// performed on the collection of stashes for a repository.
type StashCollection struct {
repo *Repository
}
// Save saves the local modifications to a new stash.
//
// Stasher is the identity of the person performing the stashing.
// Message is the optional description along with the stashed state.
// Flags control the stashing process and are given as bitwise OR.
func (c *StashCollection) Save(
stasher *Signature, message string, flags StashFlag) (*Oid, error) {
oid := new(Oid)
stasherC, err := stasher.toC()
if err != nil {
return nil, err
}
defer C.git_signature_free(stasherC)
messageC := C.CString(message)
defer C.free(unsafe.Pointer(messageC))
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_stash_save(
oid.toC(), c.repo.ptr,
stasherC, messageC, C.uint32_t(flags))
if ret < 0 {
return nil, MakeGitError(ret)
}
return oid, nil
}
// StashApplyFlag are flags that affect the stash apply operation.
type StashApplyFlag int
const (
// StashApplyDefault is the default.
StashApplyDefault StashApplyFlag = C.GIT_STASH_APPLY_DEFAULT
// StashApplyReinstateIndex will try to reinstate not only the
// working tree's changes, but also the index's changes.
StashApplyReinstateIndex StashApplyFlag = C.GIT_STASH_APPLY_REINSTATE_INDEX
)
// StashApplyProgress are flags describing the progress of the apply operation.
type StashApplyProgress int
const (
// StashApplyProgressNone means loading the stashed data from the object store.
StashApplyProgressNone StashApplyProgress = C.GIT_STASH_APPLY_PROGRESS_NONE
// StashApplyProgressLoadingStash means the stored index is being analyzed.
StashApplyProgressLoadingStash StashApplyProgress = C.GIT_STASH_APPLY_PROGRESS_LOADING_STASH
// StashApplyProgressAnalyzeIndex means the stored index is being analyzed.
StashApplyProgressAnalyzeIndex StashApplyProgress = C.GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX
// StashApplyProgressAnalyzeModified means the modified files are being analyzed.
StashApplyProgressAnalyzeModified StashApplyProgress = C.GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED
// StashApplyProgressAnalyzeUntracked means the untracked and ignored files are being analyzed.
StashApplyProgressAnalyzeUntracked StashApplyProgress = C.GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED
// StashApplyProgressCheckoutUntracked means the untracked files are being written to disk.
StashApplyProgressCheckoutUntracked StashApplyProgress = C.GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED
// StashApplyProgressCheckoutModified means the modified files are being written to disk.
StashApplyProgressCheckoutModified StashApplyProgress = C.GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED
// StashApplyProgressDone means the stash was applied successfully.
StashApplyProgressDone StashApplyProgress = C.GIT_STASH_APPLY_PROGRESS_DONE
)
// StashApplyProgressCallback is the apply operation notification callback.
type StashApplyProgressCallback func(progress StashApplyProgress) error
type stashApplyProgressData struct {
Callback StashApplyProgressCallback
Error error
}
//export stashApplyProgressCb
func stashApplyProgressCb(progress C.git_stash_apply_progress_t, handle unsafe.Pointer) int {
payload := pointerHandles.Get(handle)
data, ok := payload.(*stashApplyProgressData)
if !ok {
panic("could not retrieve data for handle")
}
if data != nil {
err := data.Callback(StashApplyProgress(progress))
if err != nil {
data.Error = err
return C.GIT_EUSER
}
}
return 0
}
// StashApplyOptions represents options to control the apply operation.
type StashApplyOptions struct {
Flags StashApplyFlag
CheckoutOptions CheckoutOpts // options to use when writing files to the working directory
ProgressCallback StashApplyProgressCallback // optional callback to notify the consumer of application progress
}
// DefaultStashApplyOptions initializes the structure with default values.
func DefaultStashApplyOptions() (StashApplyOptions, error) {
optsC := C.git_stash_apply_options{}
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ecode := C.git_stash_apply_init_options(&optsC, C.GIT_STASH_APPLY_OPTIONS_VERSION)
if ecode < 0 {
return StashApplyOptions{}, MakeGitError(ecode)
}
return StashApplyOptions{
Flags: StashApplyFlag(optsC.flags),
CheckoutOptions: checkoutOptionsFromC(&optsC.checkout_options),
}, nil
}
func (opts *StashApplyOptions) toC() (
optsC *C.git_stash_apply_options, progressData *stashApplyProgressData) {
if opts != nil {
progressData = &stashApplyProgressData{
Callback: opts.ProgressCallback,
}
optsC = &C.git_stash_apply_options{
version: C.GIT_STASH_APPLY_OPTIONS_VERSION,
flags: C.git_stash_apply_flags(opts.Flags),
}
populateCheckoutOpts(&optsC.checkout_options, &opts.CheckoutOptions)
if opts.ProgressCallback != nil {
C._go_git_setup_stash_apply_progress_callbacks(optsC)
optsC.progress_payload = pointerHandles.Track(progressData)
}
}
return
}
// should be called after every call to toC() as deferred.
func untrackStashApplyOptionsCallback(optsC *C.git_stash_apply_options) {
if optsC != nil && optsC.progress_payload != nil {
pointerHandles.Untrack(optsC.progress_payload)
}
}
func freeStashApplyOptions(optsC *C.git_stash_apply_options) {
if optsC != nil {
freeCheckoutOpts(&optsC.checkout_options)
}
}
// Apply applies a single stashed state from the stash list.
//
// If local changes in the working directory conflict with changes in the
// stash then ErrConflict will be returned. In this case, the index
// will always remain unmodified and all files in the working directory will
// remain unmodified. However, if you are restoring untracked files or
// ignored files and there is a conflict when applying the modified files,
// then those files will remain in the working directory.
//
// If passing the StashApplyReinstateIndex flag and there would be conflicts
// when reinstating the index, the function will return ErrConflict
// and both the working directory and index will be left unmodified.
//
// Note that a minimum checkout strategy of 'CheckoutSafe' is implied.
//
// 'index' is the position within the stash list. 0 points to the most
// recent stashed state.
//
// Returns error code ErrNotFound if there's no stashed state for the given
// index, error code ErrConflict if local changes in the working directory
// conflict with changes in the stash, the user returned error from the
// StashApplyProgressCallback, if any, or other error code.
//
// Error codes can be interogated with IsErrorCode(err, ErrNotFound).
func (c *StashCollection) Apply(index int, opts StashApplyOptions) error {
optsC, progressData := opts.toC()
defer untrackStashApplyOptionsCallback(optsC)
defer freeStashApplyOptions(optsC)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_stash_apply(c.repo.ptr, C.size_t(index), optsC)
if ret == C.GIT_EUSER {
return progressData.Error
}
if ret < 0 {
return MakeGitError(ret)
}
return nil
}
// StashCallback is called per entry when interating over all
// the stashed states.
//
// 'index' is the position of the current stash in the stash list,
// 'message' is the message used when creating the stash and 'id'
// is the commit id of the stash.
type StashCallback func(index int, message string, id *Oid) error
type stashCallbackData struct {
Callback StashCallback
Error error
}
//export stashForeachCb
func stashForeachCb(index C.size_t, message *C.char, id *C.git_oid, handle unsafe.Pointer) int {
payload := pointerHandles.Get(handle)
data, ok := payload.(*stashCallbackData)
if !ok {
panic("could not retrieve data for handle")
}
err := data.Callback(int(index), C.GoString(message), newOidFromC(id))
if err != nil {
data.Error = err
return C.GIT_EUSER
}
return 0
}
// Foreach loops over all the stashed states and calls the callback
// for each one.
//
// If callback returns an error, this will stop looping.
func (c *StashCollection) Foreach(callback StashCallback) error {
data := stashCallbackData{
Callback: callback,
}
handle := pointerHandles.Track(&data)
defer pointerHandles.Untrack(handle)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C._go_git_stash_foreach(c.repo.ptr, handle)
if ret == C.GIT_EUSER {
return data.Error
}
if ret < 0 {
return MakeGitError(ret)
}
return nil
}
// Drop removes a single stashed state from the stash list.
//
// 'index' is the position within the stash list. 0 points
// to the most recent stashed state.
//
// Returns error code ErrNotFound if there's no stashed
// state for the given index.
func (c *StashCollection) Drop(index int) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_stash_drop(c.repo.ptr, C.size_t(index))
if ret < 0 {
return MakeGitError(ret)
}
return nil
}
// Pop applies a single stashed state from the stash list
// and removes it from the list if successful.
//
// 'index' is the position within the stash list. 0 points
// to the most recent stashed state.
//
// 'opts' controls how stashes are applied.
//
// Returns error code ErrNotFound if there's no stashed
// state for the given index.
func (c *StashCollection) Pop(index int, opts StashApplyOptions) error {
optsC, progressData := opts.toC()
defer untrackStashApplyOptionsCallback(optsC)
defer freeStashApplyOptions(optsC)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_stash_pop(c.repo.ptr, C.size_t(index), optsC)
if ret == C.GIT_EUSER {
return progressData.Error
}
if ret < 0 {
return MakeGitError(ret)
}
return nil
}

198
stash_test.go Normal file
View File

@ -0,0 +1,198 @@
package git
import (
"fmt"
"io/ioutil"
"os"
"path"
"reflect"
"runtime"
"testing"
"time"
)
func TestStash(t *testing.T) {
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)
prepareStashRepo(t, repo)
sig := &Signature{
Name: "Rand Om Hacker",
Email: "random@hacker.com",
When: time.Now(),
}
stash1, err := repo.Stashes.Save(sig, "First stash", StashDefault)
checkFatal(t, err)
_, err = repo.LookupCommit(stash1)
checkFatal(t, err)
b, err := ioutil.ReadFile(pathInRepo(repo, "README"))
checkFatal(t, err)
if string(b) == "Update README goes to stash\n" {
t.Errorf("README still contains the uncommitted changes")
}
if !fileExistsInRepo(repo, "untracked.txt") {
t.Errorf("untracked.txt doesn't exist in the repo; should be untracked")
}
// Apply: default
opts, err := DefaultStashApplyOptions()
checkFatal(t, err)
err = repo.Stashes.Apply(0, opts)
checkFatal(t, err)
b, err = ioutil.ReadFile(pathInRepo(repo, "README"))
checkFatal(t, err)
if string(b) != "Update README goes to stash\n" {
t.Errorf("README changes aren't here")
}
// Apply: no stash for the given index
err = repo.Stashes.Apply(1, opts)
if !IsErrorCode(err, ErrNotFound) {
t.Errorf("expecting GIT_ENOTFOUND error code %d, got %v", ErrNotFound, err)
}
// Apply: callback stopped
opts.ProgressCallback = func(progress StashApplyProgress) error {
if progress == StashApplyProgressCheckoutModified {
return fmt.Errorf("Stop")
}
return nil
}
err = repo.Stashes.Apply(0, opts)
if err.Error() != "Stop" {
t.Errorf("expecting error 'Stop', got %v", err)
}
// Create second stash with ignored files
os.MkdirAll(pathInRepo(repo, "tmp"), os.ModeDir|os.ModePerm)
err = ioutil.WriteFile(pathInRepo(repo, "tmp/ignored.txt"), []byte("Ignore me\n"), 0644)
checkFatal(t, err)
stash2, err := repo.Stashes.Save(sig, "Second stash", StashIncludeIgnored)
checkFatal(t, err)
if fileExistsInRepo(repo, "tmp/ignored.txt") {
t.Errorf("tmp/ignored.txt should not exist anymore in the work dir")
}
// Stash foreach
expected := []stash{
{0, "On master: Second stash", stash2.String()},
{1, "On master: First stash", stash1.String()},
}
checkStashes(t, repo, expected)
// Stash pop
opts, _ = DefaultStashApplyOptions()
err = repo.Stashes.Pop(1, opts)
checkFatal(t, err)
b, err = ioutil.ReadFile(pathInRepo(repo, "README"))
checkFatal(t, err)
if string(b) != "Update README goes to stash\n" {
t.Errorf("README changes aren't here")
}
expected = []stash{
{0, "On master: Second stash", stash2.String()},
}
checkStashes(t, repo, expected)
// Stash drop
err = repo.Stashes.Drop(0)
checkFatal(t, err)
expected = []stash{}
checkStashes(t, repo, expected)
}
type stash struct {
index int
msg string
id string
}
func checkStashes(t *testing.T, repo *Repository, expected []stash) {
var actual []stash
repo.Stashes.Foreach(func(index int, msg string, id *Oid) error {
stash := stash{index, msg, id.String()}
if len(expected) > len(actual) {
if s := expected[len(actual)]; s.id == "" {
stash.id = "" // don't check id
}
}
actual = append(actual, stash)
return nil
})
if len(expected) > 0 && !reflect.DeepEqual(expected, actual) {
// The failure happens at wherever we were called, not here
_, file, line, ok := runtime.Caller(1)
if !ok {
t.Fatalf("Unable to get caller")
}
t.Errorf("%v:%v: expecting %#v\ngot %#v", path.Base(file), line, expected, actual)
}
}
func prepareStashRepo(t *testing.T, repo *Repository) {
seedTestRepo(t, repo)
err := ioutil.WriteFile(pathInRepo(repo, ".gitignore"), []byte("tmp\n"), 0644)
checkFatal(t, err)
sig := &Signature{
Name: "Rand Om Hacker",
Email: "random@hacker.com",
When: time.Now(),
}
idx, err := repo.Index()
checkFatal(t, err)
err = idx.AddByPath(".gitignore")
checkFatal(t, err)
treeID, err := idx.WriteTree()
checkFatal(t, err)
err = idx.Write()
checkFatal(t, err)
currentBranch, err := repo.Head()
checkFatal(t, err)
currentTip, err := repo.LookupCommit(currentBranch.Target())
checkFatal(t, err)
message := "Add .gitignore\n"
tree, err := repo.LookupTree(treeID)
checkFatal(t, err)
_, err = repo.CreateCommit("HEAD", sig, sig, message, tree, currentTip)
checkFatal(t, err)
err = ioutil.WriteFile(pathInRepo(repo, "README"), []byte("Update README goes to stash\n"), 0644)
checkFatal(t, err)
err = ioutil.WriteFile(pathInRepo(repo, "untracked.txt"), []byte("Hello, World\n"), 0644)
checkFatal(t, err)
}
func fileExistsInRepo(repo *Repository, name string) bool {
if _, err := os.Stat(pathInRepo(repo, name)); err != nil {
return false
}
return true
}

1
vendor/libgit2 vendored Submodule

@ -0,0 +1 @@
Subproject commit df4dfaadcf709646ebab2e57e3589952cf1ac809

View File

@ -114,19 +114,6 @@ void _go_git_setup_callbacks(git_remote_callbacks *callbacks) {
callbacks->push_update_reference = (push_update_reference_cb) pushUpdateReferenceCallback;
}
int _go_blob_chunk_cb(char *buffer, size_t maxLen, void *payload)
{
return blobChunkCb(buffer, maxLen, payload);
}
int _go_git_blob_create_fromchunks(git_oid *id,
git_repository *repo,
const char *hintpath,
void *payload)
{
return git_blob_create_fromchunks(id, repo, hintpath, _go_blob_chunk_cb, payload);
}
int _go_git_index_add_all(git_index *index, const git_strarray *pathspec, unsigned int flags, void *callback) {
git_index_matched_path_cb cb = callback ? (git_index_matched_path_cb) &indexMatchedPathCallback : NULL;
return git_index_add_all(index, pathspec, flags, cb, callback);
@ -170,4 +157,27 @@ int _go_git_merge_file(git_merge_file_result* out, char* ancestorContents, size_
return git_merge_file(out, &ancestor, &ours, &theirs, copts);
}
void _go_git_setup_stash_apply_progress_callbacks(git_stash_apply_options *opts) {
opts->progress_cb = (git_stash_apply_progress_cb)stashApplyProgressCb;
}
int _go_git_stash_foreach(git_repository *repo, void *payload) {
return git_stash_foreach(repo, (git_stash_cb)&stashForeachCb, payload);
}
int _go_git_writestream_write(git_writestream *stream, const char *buffer, size_t len)
{
return stream->write(stream, buffer, len);
}
int _go_git_writestream_close(git_writestream *stream)
{
return stream->close(stream);
}
void _go_git_writestream_free(git_writestream *stream)
{
stream->free(stream);
}
/* EOF */