Skip to content

Commit 4658277

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

File tree

2 files changed

+325
-0
lines changed

2 files changed

+325
-0
lines changed

repo_stash.go

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

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)