Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add stash command #101

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: 1.23.x
- name: Check Go module tidiness
Expand Down
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 := 0; i < len(lines); i++ {
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 {
idx = -1
}
Comment on lines +56 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean to have Index == -1? Should we ignore the line instead?

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
}
209 changes: 209 additions & 0 deletions repo_stash_test.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you convert all if err != nil to require.NoError/require.Error/assert.Error accordingly?

Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package git

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

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

func TestStashWorktreeError(t *testing.T) {
_, err := testrepo.StashList()
if err == nil {
t.Errorf("StashList() error = %v, wantErr %v", err, true)
return
}
}

func TestStash(t *testing.T) {
tmp := t.TempDir()
path, err := filepath.Abs(repoPath)
if err != nil {
t.Fatal(err)
}

if err := Clone("file://"+path, tmp); err != nil {
t.Fatal(err)
}

repo, err := Open(tmp)
if err != nil {
t.Fatal(err)
}

if err := os.WriteFile(tmp+"/resources/newfile", []byte("hello, world!"), 0o644); err != nil {
t.Fatal(err)
}

f, err := os.OpenFile(tmp+"/README.txt", os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
t.Fatal(err)
}

if _, err := f.WriteString("\n\ngit-module"); err != nil {
t.Fatal(err)
}

f.Close()
if err := repo.Add(AddOptions{
All: true,
}); err != nil {
t.Fatal(err)
}

if err := repo.StashPush(""); err != nil {
t.Fatal(err)
}

f, err = os.OpenFile(tmp+"/README.txt", os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
t.Fatal(err)
}

if _, err := f.WriteString("\n\nstash 1"); err != nil {
t.Fatal(err)
}

f.Close()
if err := repo.Add(AddOptions{
All: true,
}); err != nil {
t.Fatal(err)
}

if err := repo.StashPush("custom message"); err != nil {
t.Fatal(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