Add stash support
diff --git a/repository.go b/repository.go
index d8e398b..4bf8531 100644
--- a/repository.go
+++ b/repository.go
@@ -30,6 +30,9 @@
 	// 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 @@
 	repo.References.repo = repo
 	repo.Notes.repo = repo
 	repo.Tags.repo = repo
+	repo.Stashes.repo = repo
 
 	runtime.SetFinalizer(repo, (*Repository).Free)
 
diff --git a/stash.go b/stash.go
new file mode 100644
index 0000000..5142b82
--- /dev/null
+++ b/stash.go
@@ -0,0 +1,334 @@
+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.uint(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)
+	}
+	defer freeStashApplyOptions(&optsC)
+
+	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),
+			checkout_options: *opts.CheckoutOptions.toC(),
+		}
+		if opts.ProgressCallback != nil {
+			C._go_git_setup_stash_apply_progress_callbacks(optsC)
+			optsC.progress_payload = pointerHandles.Track(progressData)
+		}
+	}
+	return
+}
+
+func freeStashApplyOptions(optsC *C.git_stash_apply_options) {
+	if optsC != nil {
+		freeCheckoutOpts(&optsC.checkout_options)
+		if optsC.progress_payload != nil {
+			pointerHandles.Untrack(optsC.progress_payload)
+		}
+	}
+}
+
+// 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 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 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 2b1a180..a01867c 100644
--- a/wrapper.c
+++ b/wrapper.c
@@ -141,4 +141,12 @@
     return git_tag_foreach(repo, (git_tag_foreach_cb)&gitTagForeachCb, payload);
 }
 
+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 */