diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/.travis.yml b/.travis.yml index 8a81f16..2758d33 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/Makefile b/Makefile index 9c42283..39fc558 100644 --- a/Makefile +++ b/Makefile @@ -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 ./... diff --git a/README.md b/README.md index 315032f..bd918d6 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ to use a version of git2go which will work against libgit2 v0.22 and dynamically import "github.com/libgit2/git2go" -to use the version which works against the latest release. +to use the 'master' branch, which works against the latest release of libgit2, whichever that one is at the time. ### From `next` @@ -44,15 +44,14 @@ libgit2 uses OpenSSL and LibSSH2 for performing encrypted network connections. F 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 ------- diff --git a/blob.go b/blob.go index 8b3e94f..73a4a19 100644 --- a/blob.go +++ b/blob.go @@ -4,15 +4,13 @@ package git #include #include -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 } diff --git a/commit.go b/commit.go index 6830da3..07b7c37 100644 --- a/commit.go +++ b/commit.go @@ -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)) } diff --git a/diff.go b/diff.go index 8ae0512..e8d5007 100644 --- a/diff.go +++ b/diff.go @@ -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 diff --git a/features.go b/features.go new file mode 100644 index 0000000..f6474a0 --- /dev/null +++ b/features.go @@ -0,0 +1,30 @@ +package git + +/* +#include +*/ +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) +} diff --git a/git.go b/git.go index ed891e6..411c11b 100644 --- a/git.go +++ b/git.go @@ -5,8 +5,8 @@ package git #include #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 +125,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 diff --git a/git_test.go b/git_test.go index 3385a72..807dcc2 100644 --- a/git_test.go +++ b/git_test.go @@ -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) diff --git a/odb.go b/odb.go index 9ba2ea2..a728cd3 100644 --- a/odb.go +++ b/odb.go @@ -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 diff --git a/remote.go b/remote.go index c537932..d3f437d 100644 --- a/remote.go +++ b/remote.go @@ -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 diff --git a/remote_test.go b/remote_test.go index 3d00640..8b20fd2 100644 --- a/remote_test.go +++ b/remote_test.go @@ -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) diff --git a/repository.go b/repository.go index efc506e..0612b5f 100644 --- a/repository.go +++ b/repository.go @@ -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 diff --git a/script/with-static.sh b/script/with-static.sh deleted file mode 100755 index 3f60e31..0000000 --- a/script/with-static.sh +++ /dev/null @@ -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" - -$@ diff --git a/stash.go b/stash.go new file mode 100644 index 0000000..809732e --- /dev/null +++ b/stash.go @@ -0,0 +1,338 @@ +package git + +/* +#include + +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 +} diff --git a/stash_test.go b/stash_test.go new file mode 100644 index 0000000..180a16b --- /dev/null +++ b/stash_test.go @@ -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 +} diff --git a/wrapper.c b/wrapper.c index f4b4ecc..11c2f32 100644 --- a/wrapper.c +++ b/wrapper.c @@ -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 */