diff options
| -rw-r--r-- | .gitmodules | 3 | ||||
| -rw-r--r-- | blob.go | 74 | ||||
| -rw-r--r-- | diff.go | 4 | ||||
| -rw-r--r-- | git_test.go | 4 | ||||
| -rw-r--r-- | odb.go | 4 | ||||
| -rw-r--r-- | remote.go | 55 | ||||
| -rw-r--r-- | remote_test.go | 8 | ||||
| -rw-r--r-- | repository.go | 38 | ||||
| -rw-r--r-- | stash.go | 338 | ||||
| -rw-r--r-- | stash_test.go | 198 | ||||
| m--------- | vendor/libgit2 | 0 | ||||
| -rw-r--r-- | wrapper.c | 36 |
12 files changed, 723 insertions, 39 deletions
diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8eb5872 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/libgit2"] + path = vendor/libgit2 + url = https://github.com/libgit2/libgit2 @@ -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.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) - ecode := C._go_git_blob_create_fromchunks(&oid, repo.ptr, chintPath, handle) - if payload.Error != nil { - return nil, payload.Error + 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 } @@ -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/git_test.go b/git_test.go index bdb6837..807dcc2 100644 --- a/git_test.go +++ b/git_test.go @@ -58,6 +58,8 @@ func seedTestRepo(t *testing.T, repo *Repository) (*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) @@ -91,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) @@ -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 @@ -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)) @@ -654,12 +692,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. @@ -669,19 +707,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/stash.go b/stash.go new file mode 100644 index 0000000..809732e --- /dev/null +++ b/stash.go @@ -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 +} 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: "[email protected]", + 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: "[email protected]", + 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/vendor/libgit2 b/vendor/libgit2 new file mode 160000 +Subproject 73dab7692e780e1df96093a54854795428eb66b @@ -108,19 +108,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); @@ -164,4 +151,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 */ |
