Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,15 @@ func (c *Command) AddEnvs(envs ...string) *Command {
}

// WithContext returns a new Command with the given context.
func (c Command) WithContext(ctx context.Context) *Command {
func (c *Command) WithContext(ctx context.Context) *Command {
c.ctx = ctx
return &c
return c
}

// WithTimeout returns a new Command with given timeout.
func (c Command) WithTimeout(timeout time.Duration) *Command {
func (c *Command) WithTimeout(timeout time.Duration) *Command {
c.timeout = timeout
return &c
return c
}

// SetTimeout sets the timeout for the command.
Expand Down
122 changes: 122 additions & 0 deletions repo_stash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package git

import (
"bytes"
"io"
"regexp"
"strconv"
"strings"
)

// Stash represents a stash in the repository.
type Stash struct {
// Index is the index of the stash.
Index int
// Message is the message of the stash.
Message string
// Files is the list of files in the stash.
Files []string
}

// StashListOptions describes the options for the StashList function.
type StashListOptions struct {
// CommandOptions describes the options for the command.
CommandOptions
}

var stashLineRegexp = regexp.MustCompile(`^stash@\{(\d+)\}: (.*)$`)

// StashList returns a list of stashes in the repository.
// This must be run in a work tree.
func (r *Repository) StashList(opts ...StashListOptions) ([]*Stash, error) {
var opt StashListOptions
if len(opts) > 0 {
opt = opts[0]
}

stashes := make([]*Stash, 0)
cmd := NewCommand("stash", "list", "--name-only").AddOptions(opt.CommandOptions)
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
if err := cmd.RunInDirPipeline(stdout, stderr, r.path); err != nil {
return nil, concatenateError(err, stderr.String())
}

var stash *Stash
lines := strings.Split(stdout.String(), "\n")
for i := range lines {
line := strings.TrimSpace(lines[i])
// Init entry
if match := stashLineRegexp.FindStringSubmatch(line); len(match) == 3 {
// Append the previous stash
if stash != nil {
stashes = append(stashes, stash)
}

idx, err := strconv.Atoi(match[1])
if err != nil {
continue
}
stash = &Stash{
Index: idx,
Message: match[2],
Files: make([]string, 0),
}
} else if stash != nil && line != "" {
stash.Files = append(stash.Files, line)
}
}

// Append the last stash
if stash != nil {
stashes = append(stashes, stash)
}
return stashes, nil
}

// StashDiff returns a parsed diff object for the given stash index.
// This must be run in a work tree.
func (r *Repository) StashDiff(index int, maxFiles, maxFileLines, maxLineChars int, opts ...DiffOptions) (*Diff, error) {
var opt DiffOptions
if len(opts) > 0 {
opt = opts[0]
}

cmd := NewCommand("stash", "show", "-p", "--full-index", "-M", strconv.Itoa(index)).AddOptions(opt.CommandOptions)
stdout, w := io.Pipe()
done := make(chan SteamParseDiffResult)
go StreamParseDiff(stdout, done, maxFiles, maxFileLines, maxLineChars)

stderr := new(bytes.Buffer)
err := cmd.RunInDirPipeline(w, stderr, r.path)
_ = w.Close() // Close writer to exit parsing goroutine
if err != nil {
return nil, concatenateError(err, stderr.String())
}

result := <-done
return result.Diff, result.Err
}

// StashPushOptions describes the options for the StashPush function.
type StashPushOptions struct {
// CommandOptions describes the options for the command.
CommandOptions
}

// StashPush pushes the current worktree to the stash.
// This must be run in a work tree.
func (r *Repository) StashPush(msg string, opts ...StashPushOptions) error {
var opt StashPushOptions
if len(opts) > 0 {
opt = opts[0]
}

cmd := NewCommand("stash", "push")
if msg != "" {
cmd.AddArgs("-m", msg)
}
cmd.AddOptions(opt.CommandOptions)

_, err := cmd.RunInDir(r.path)
return err
}
186 changes: 186 additions & 0 deletions repo_stash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package git

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestStashWorktreeError(t *testing.T) {
_, err := testrepo.StashList()
assert.Errorf(t, err, "StashList() should return an error when not run in a work tree")
}

func TestStash(t *testing.T) {
tmp := t.TempDir()
path, err := filepath.Abs(repoPath)
require.NoError(t, err)

require.NoError(t, Clone("file://"+path, tmp))

repo, err := Open(tmp)
require.NoError(t, err)

err = os.WriteFile(tmp+"/resources/newfile", []byte("hello, world!"), 0o644)
require.NoError(t, err)

f, err := os.OpenFile(tmp+"/README.txt", os.O_APPEND|os.O_WRONLY, 0o644)
require.NoError(t, err)

_, err = f.WriteString("\n\ngit-module")
require.NoError(t, err)

f.Close()
err = repo.Add(AddOptions{All: true})
require.NoError(t, err)

err = repo.StashPush("")
require.NoError(t, err)

f, err = os.OpenFile(tmp+"/README.txt", os.O_APPEND|os.O_WRONLY, 0o644)
require.NoError(t, err)

_, err = f.WriteString("\n\nstash 1")
require.NoError(t, err)

f.Close()
err = repo.Add(AddOptions{All: true})
require.NoError(t, err)

err = repo.StashPush("custom message")
require.NoError(t, err)

want := []*Stash{
{
Index: 0,
Message: "On master: custom message",
Files: []string{"README.txt"},
},
{
Index: 1,
Message: "WIP on master: cfc3b29 Add files with same SHA",
Files: []string{"README.txt", "resources/newfile"},
},
}

stash, err := repo.StashList(StashListOptions{
CommandOptions: CommandOptions{
Envs: []string{"GIT_CONFIG_GLOBAL=/dev/null"},
},
})
require.NoError(t, err)
require.Equalf(t, want, stash, "StashList() got = %v, want %v", stash, want)

wantDiff := &Diff{
totalAdditions: 4,
totalDeletions: 0,
isIncomplete: false,
Files: []*DiffFile{
{
Name: "README.txt",
Type: DiffFileChange,
Index: "72e29aca01368bc0aca5d599c31fa8705b11787d",
OldIndex: "adfd6da3c0a3fb038393144becbf37f14f780087",
Sections: []*DiffSection{
{
Lines: []*DiffLine{
{
Type: DiffLineSection,
Content: `@@ -13,3 +13,6 @@ As a quick reminder, this came from one of three locations in either SSH, Git, o`,
},
{
Type: DiffLinePlain,
Content: " We can, as an example effort, even modify this README and change it as if it were source code for the purposes of the class.",
LeftLine: 13,
RightLine: 13,
},
{
Type: DiffLinePlain,
Content: " ",
LeftLine: 14,
RightLine: 14,
},
{
Type: DiffLinePlain,
Content: " This demo also includes an image with changes on a branch for examination of image diff on GitHub.",
LeftLine: 15,
RightLine: 15,
},
{
Type: DiffLineAdd,
Content: "+",
LeftLine: 0,
RightLine: 16,
},
{
Type: DiffLineAdd,
Content: "+",
LeftLine: 0,
RightLine: 17,
},
{
Type: DiffLineAdd,
Content: "+git-module",
LeftLine: 0,
RightLine: 18,
},
},
numAdditions: 3,
numDeletions: 0,
},
},
numAdditions: 3,
numDeletions: 0,
oldName: "README.txt",
mode: 0o100644,
oldMode: 0o100644,
isBinary: false,
isSubmodule: false,
isIncomplete: false,
},
{
Name: "resources/newfile",
Type: DiffFileAdd,
Index: "30f51a3fba5274d53522d0f19748456974647b4f",
OldIndex: "0000000000000000000000000000000000000000",
Sections: []*DiffSection{
{
Lines: []*DiffLine{
{
Type: DiffLineSection,
Content: "@@ -0,0 +1 @@",
},
{
Type: DiffLineAdd,
Content: "+hello, world!",
LeftLine: 0,
RightLine: 1,
},
},
numAdditions: 1,
numDeletions: 0,
},
},
numAdditions: 1,
numDeletions: 0,
oldName: "resources/newfile",
mode: 0o100644,
oldMode: 0o100644,
isBinary: false,
isSubmodule: false,
isIncomplete: false,
},
},
}

diff, err := repo.StashDiff(want[1].Index, 0, 0, 0, DiffOptions{
CommandOptions: CommandOptions{
Envs: []string{"GIT_CONFIG_GLOBAL=/dev/null"},
},
})
require.NoError(t, err)
require.Equalf(t, wantDiff, diff, "StashDiff() got = %v, want %v", diff, wantDiff)
}
Loading