Skip to content

Commit 4e835a2

Browse files
committed
feat: add stash command
- Add stash push - List - Show
1 parent 77db94e commit 4e835a2

File tree

2 files changed

+331
-0
lines changed

2 files changed

+331
-0
lines changed

repo_stash.go

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package git
2+
3+
import (
4+
"bytes"
5+
"io"
6+
stdlog "log"
7+
"regexp"
8+
"strconv"
9+
"strings"
10+
)
11+
12+
// Stash represents a stash in the repository.
13+
type Stash struct {
14+
// Index is the index of the stash.
15+
Index int
16+
17+
// Message is the message of the stash.
18+
Message string
19+
20+
// Files is the list of files in the stash.
21+
Files []string
22+
}
23+
24+
// StashListOptions describes the options for the StashList function.
25+
type StashListOptions struct {
26+
// CommandOptions describes the options for the command.
27+
CommandOptions
28+
}
29+
30+
var stashLineRegexp = regexp.MustCompile(`^stash@\{(\d+)\}: (.*)$`)
31+
32+
// StashList returns a list of stashes in the repository.
33+
// This must be run in a work tree.
34+
func (r *Repository) StashList(opts ...StashListOptions) ([]*Stash, error) {
35+
var opt StashListOptions
36+
if len(opts) > 0 {
37+
opt = opts[0]
38+
}
39+
40+
stash := make([]*Stash, 0)
41+
cmd := NewCommand("stash", "list", "--name-only").AddOptions(opt.CommandOptions)
42+
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
43+
if err := cmd.RunInDirPipeline(stdout, stderr, r.path); err != nil {
44+
return nil, concatenateError(err, stderr.String())
45+
}
46+
47+
var entry *Stash
48+
lines := strings.Split(stdout.String(), "\n")
49+
for i := 0; i < len(lines); i++ {
50+
// Init entry
51+
if match := stashLineRegexp.FindStringSubmatch(lines[i]); len(match) == 3 {
52+
stdlog.Printf("match: %v", match)
53+
if entry != nil {
54+
stdlog.Printf("stash: %v", entry)
55+
stash = append(stash, entry)
56+
}
57+
58+
idx, err := strconv.Atoi(match[1])
59+
if err != nil {
60+
idx = -1
61+
}
62+
entry = &Stash{
63+
Index: idx,
64+
Message: match[2],
65+
Files: make([]string, 0),
66+
}
67+
} else if entry != nil && lines[i] != "" {
68+
stdlog.Printf("file: %s", lines[i])
69+
entry.Files = append(entry.Files, lines[i])
70+
} else {
71+
stdlog.Printf("skip: %s", lines[i])
72+
continue
73+
}
74+
}
75+
76+
if entry != nil {
77+
stdlog.Printf("stash: %v", entry)
78+
stash = append(stash, entry)
79+
}
80+
81+
return stash, nil
82+
}
83+
84+
// StashDiff returns a parsed diff object for the given stash index.
85+
// This must be run in a work tree.
86+
func (r *Repository) StashDiff(index int, maxFiles, maxFileLines, maxLineChars int, opts ...DiffOptions) (*Diff, error) {
87+
var opt DiffOptions
88+
if len(opts) > 0 {
89+
opt = opts[0]
90+
}
91+
92+
cmd := NewCommand("stash", "show", "-p", "--full-index", "-M", strconv.Itoa(index)).AddOptions(opt.CommandOptions)
93+
stdout, w := io.Pipe()
94+
done := make(chan SteamParseDiffResult)
95+
go StreamParseDiff(stdout, done, maxFiles, maxFileLines, maxLineChars)
96+
97+
stderr := new(bytes.Buffer)
98+
err := cmd.RunInDirPipelineWithTimeout(opt.Timeout, w, stderr, r.path)
99+
_ = w.Close() // Close writer to exit parsing goroutine
100+
if err != nil {
101+
return nil, concatenateError(err, stderr.String())
102+
}
103+
104+
result := <-done
105+
return result.Diff, result.Err
106+
}
107+
108+
// StashPushOptions describes the options for the StashPush function.
109+
type StashPushOptions struct {
110+
// CommandOptions describes the options for the command.
111+
CommandOptions
112+
}
113+
114+
// StashPush pushes the current worktree to the stash.
115+
// This must be run in a work tree.
116+
func (r *Repository) StashPush(msg string, opts ...StashPushOptions) error {
117+
var opt StashPushOptions
118+
if len(opts) > 0 {
119+
opt = opts[0]
120+
}
121+
122+
cmd := NewCommand("stash", "push")
123+
if msg != "" {
124+
cmd.AddArgs("-m", msg)
125+
}
126+
cmd.AddOptions(opt.CommandOptions)
127+
128+
_, err := cmd.RunInDir(r.path)
129+
return err
130+
}

repo_stash_test.go

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package git
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestStashWorktreeError(t *testing.T) {
12+
_, err := testrepo.StashList()
13+
if err == nil {
14+
t.Errorf("StashList() error = %v, wantErr %v", err, true)
15+
return
16+
}
17+
}
18+
19+
func TestStash(t *testing.T) {
20+
tmp := t.TempDir()
21+
path, err := filepath.Abs(repoPath)
22+
if err != nil {
23+
t.Fatal(err)
24+
}
25+
26+
if err := Clone("file://"+path, tmp); err != nil {
27+
t.Fatal(err)
28+
}
29+
30+
repo, err := Open(tmp)
31+
if err != nil {
32+
t.Fatal(err)
33+
}
34+
35+
if err := os.WriteFile(tmp+"/resources/newfile", []byte("hello, world!"), 0o644); err != nil {
36+
t.Fatal(err)
37+
}
38+
39+
f, err := os.OpenFile(tmp+"/README.txt", os.O_APPEND|os.O_WRONLY, 0o644)
40+
if err != nil {
41+
t.Fatal(err)
42+
}
43+
44+
if _, err := f.WriteString("\n\ngit-module"); err != nil {
45+
t.Fatal(err)
46+
}
47+
48+
f.Close()
49+
if err := repo.Add(AddOptions{
50+
All: true,
51+
}); err != nil {
52+
t.Fatal(err)
53+
}
54+
55+
if err := repo.StashPush(""); err != nil {
56+
t.Fatal(err)
57+
}
58+
59+
f, err = os.OpenFile(tmp+"/README.txt", os.O_APPEND|os.O_WRONLY, 0o644)
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
64+
if _, err := f.WriteString("\n\nstash 1"); err != nil {
65+
t.Fatal(err)
66+
}
67+
68+
f.Close()
69+
if err := repo.Add(AddOptions{
70+
All: true,
71+
}); err != nil {
72+
t.Fatal(err)
73+
}
74+
75+
if err := repo.StashPush("custom message"); err != nil {
76+
t.Fatal(err)
77+
}
78+
79+
want := []*Stash{
80+
{
81+
Index: 0,
82+
Message: "On master: custom message",
83+
Files: []string{"README.txt"},
84+
},
85+
{
86+
Index: 1,
87+
Message: "WIP on master: cfc3b29 Add files with same SHA",
88+
Files: []string{"README.txt", "resources/newfile"},
89+
},
90+
}
91+
92+
stash, err := repo.StashList()
93+
require.NoError(t, err)
94+
require.Equalf(t, want, stash, "StashList() got = %v, want %v", stash, want)
95+
96+
wantDiff := &Diff{
97+
totalAdditions: 4,
98+
totalDeletions: 0,
99+
isIncomplete: false,
100+
Files: []*DiffFile{
101+
{
102+
Name: "README.txt",
103+
Type: DiffFileChange,
104+
Index: "72e29aca01368bc0aca5d599c31fa8705b11787d",
105+
OldIndex: "adfd6da3c0a3fb038393144becbf37f14f780087",
106+
Sections: []*DiffSection{
107+
{
108+
Lines: []*DiffLine{
109+
{
110+
Type: DiffLineSection,
111+
Content: `@@ -13,3 +13,6 @@ As a quick reminder, this came from one of three locations in either SSH, Git, o`,
112+
},
113+
{
114+
Type: DiffLinePlain,
115+
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.",
116+
LeftLine: 13,
117+
RightLine: 13,
118+
},
119+
{
120+
Type: DiffLinePlain,
121+
Content: " ",
122+
LeftLine: 14,
123+
RightLine: 14,
124+
},
125+
{
126+
Type: DiffLinePlain,
127+
Content: " This demo also includes an image with changes on a branch for examination of image diff on GitHub.",
128+
LeftLine: 15,
129+
RightLine: 15,
130+
},
131+
{
132+
Type: DiffLineAdd,
133+
Content: "+",
134+
LeftLine: 0,
135+
RightLine: 16,
136+
},
137+
{
138+
Type: DiffLineAdd,
139+
Content: "+",
140+
LeftLine: 0,
141+
RightLine: 17,
142+
},
143+
{
144+
Type: DiffLineAdd,
145+
Content: "+git-module",
146+
LeftLine: 0,
147+
RightLine: 18,
148+
},
149+
},
150+
numAdditions: 3,
151+
numDeletions: 0,
152+
},
153+
},
154+
numAdditions: 3,
155+
numDeletions: 0,
156+
oldName: "README.txt",
157+
mode: 0o100644,
158+
oldMode: 0o100644,
159+
isBinary: false,
160+
isSubmodule: false,
161+
isIncomplete: false,
162+
},
163+
{
164+
Name: "resources/newfile",
165+
Type: DiffFileAdd,
166+
Index: "30f51a3fba5274d53522d0f19748456974647b4f",
167+
OldIndex: "0000000000000000000000000000000000000000",
168+
Sections: []*DiffSection{
169+
{
170+
Lines: []*DiffLine{
171+
{
172+
Type: DiffLineSection,
173+
Content: "@@ -0,0 +1 @@",
174+
},
175+
{
176+
Type: DiffLineAdd,
177+
Content: "+hello, world!",
178+
LeftLine: 0,
179+
RightLine: 1,
180+
},
181+
},
182+
numAdditions: 1,
183+
numDeletions: 0,
184+
},
185+
},
186+
numAdditions: 1,
187+
numDeletions: 0,
188+
oldName: "resources/newfile",
189+
mode: 0o100644,
190+
oldMode: 0o100644,
191+
isBinary: false,
192+
isSubmodule: false,
193+
isIncomplete: false,
194+
},
195+
},
196+
}
197+
198+
diff, err := repo.StashDiff(want[1].Index, 0, 0, 0)
199+
require.NoError(t, err)
200+
require.Equalf(t, wantDiff, diff, "StashDiff() got = %v, want %v", diff, wantDiff)
201+
}

0 commit comments

Comments
 (0)