Merge pull request #257 from clns/stash-support
[next] Add stash support
This commit is contained in:
commit
836b6c56be
|
@ -30,6 +30,9 @@ type Repository struct {
|
|||
// Tags represents the collection of tags and can be used to create,
|
||||
// list and iterate 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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -164,4 +164,12 @@ 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);
|
||||
}
|
||||
|
||||
/* EOF */
|
||||
|
|
Loading…
Reference in New Issue