Skip to content

Commit

Permalink
feat: add stash command
Browse files Browse the repository at this point in the history
- Add stash push
- List
- Show
  • Loading branch information
aymanbagabas committed Oct 24, 2023
1 parent 77db94e commit 4e835a2
Show file tree
Hide file tree
Showing 2 changed files with 331 additions and 0 deletions.
130 changes: 130 additions & 0 deletions repo_stash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package git

import (
"bytes"
"io"
stdlog "log"
"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]
}

stash := 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 entry *Stash
lines := strings.Split(stdout.String(), "\n")
for i := 0; i < len(lines); i++ {
// Init entry
if match := stashLineRegexp.FindStringSubmatch(lines[i]); len(match) == 3 {
stdlog.Printf("match: %v", match)
if entry != nil {
stdlog.Printf("stash: %v", entry)
stash = append(stash, entry)
}

idx, err := strconv.Atoi(match[1])
if err != nil {
idx = -1
}
entry = &Stash{
Index: idx,
Message: match[2],
Files: make([]string, 0),
}
} else if entry != nil && lines[i] != "" {
stdlog.Printf("file: %s", lines[i])
entry.Files = append(entry.Files, lines[i])
} else {
stdlog.Printf("skip: %s", lines[i])
continue
}
}

if entry != nil {
stdlog.Printf("stash: %v", entry)
stash = append(stash, entry)
}

return stash, 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.RunInDirPipelineWithTimeout(opt.Timeout, 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
}
201 changes: 201 additions & 0 deletions repo_stash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
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()
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)
require.NoError(t, err)
require.Equalf(t, wantDiff, diff, "StashDiff() got = %v, want %v", diff, wantDiff)
}

0 comments on commit 4e835a2

Please sign in to comment.