Skip to content

Commit 5e8facf

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

File tree

2 files changed

+334
-0
lines changed

2 files changed

+334
-0
lines changed

repo_stash.go

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+
line := strings.TrimSpace(lines[i])
50+
// Init entry
51+
if match := stashLineRegexp.FindStringSubmatch(line); len(match) == 3 {
52+
if entry != nil {
53+
stash = append(stash, entry)
54+
}
55+
56+
idx, err := strconv.Atoi(match[1])
57+
if err != nil {
58+
idx = -1
59+
}
60+
entry = &Stash{
61+
Index: idx,
62+
Message: match[2],
63+
Files: make([]string, 0),
64+
}
65+
} else if entry != nil && line != "" {
66+
entry.Files = append(entry.Files, line)
67+
} else {
68+
continue
69+
}
70+
}
71+
72+
if entry != nil {
73+
stash = append(stash, entry)
74+
}
75+
76+
return stash, nil
77+
}
78+
79+
// StashDiff returns a parsed diff object for the given stash index.
80+
// This must be run in a work tree.
81+
func (r *Repository) StashDiff(index int, maxFiles, maxFileLines, maxLineChars int, opts ...DiffOptions) (*Diff, error) {
82+
var opt DiffOptions
83+
if len(opts) > 0 {
84+
opt = opts[0]
85+
}
86+
87+
cmd := NewCommand("stash", "show", "-p", "--full-index", "-M", strconv.Itoa(index)).AddOptions(opt.CommandOptions)
88+
stdout, w := io.Pipe()
89+
done := make(chan SteamParseDiffResult)
90+
go StreamParseDiff(stdout, done, maxFiles, maxFileLines, maxLineChars)
91+
92+
stderr := new(bytes.Buffer)
93+
err := cmd.RunInDirPipelineWithTimeout(opt.Timeout, w, stderr, r.path)
94+
_ = w.Close() // Close writer to exit parsing goroutine
95+
if err != nil {
96+
return nil, concatenateError(err, stderr.String())
97+
}
98+
99+
result := <-done
100+
return result.Diff, result.Err
101+
}
102+
103+
// StashPushOptions describes the options for the StashPush function.
104+
type StashPushOptions struct {
105+
// CommandOptions describes the options for the command.
106+
CommandOptions
107+
}
108+
109+
// StashPush pushes the current worktree to the stash.
110+
// This must be run in a work tree.
111+
func (r *Repository) StashPush(msg string, opts ...StashPushOptions) error {
112+
var opt StashPushOptions
113+
if len(opts) > 0 {
114+
opt = opts[0]
115+
}
116+
117+
cmd := NewCommand("stash", "push")
118+
if msg != "" {
119+
cmd.AddArgs("-m", msg)
120+
}
121+
cmd.AddOptions(opt.CommandOptions)
122+
123+
_, err := cmd.RunInDir(r.path)
124+
return err
125+
}

repo_stash_test.go

+209
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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(StashListOptions{
93+
CommandOptions: CommandOptions{
94+
Envs: []string{"GIT_CONFIG_GLOBAL=/dev/null"},
95+
},
96+
})
97+
require.NoError(t, err)
98+
require.Equalf(t, want, stash, "StashList() got = %v, want %v", stash, want)
99+
100+
wantDiff := &Diff{
101+
totalAdditions: 4,
102+
totalDeletions: 0,
103+
isIncomplete: false,
104+
Files: []*DiffFile{
105+
{
106+
Name: "README.txt",
107+
Type: DiffFileChange,
108+
Index: "72e29aca01368bc0aca5d599c31fa8705b11787d",
109+
OldIndex: "adfd6da3c0a3fb038393144becbf37f14f780087",
110+
Sections: []*DiffSection{
111+
{
112+
Lines: []*DiffLine{
113+
{
114+
Type: DiffLineSection,
115+
Content: `@@ -13,3 +13,6 @@ As a quick reminder, this came from one of three locations in either SSH, Git, o`,
116+
},
117+
{
118+
Type: DiffLinePlain,
119+
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.",
120+
LeftLine: 13,
121+
RightLine: 13,
122+
},
123+
{
124+
Type: DiffLinePlain,
125+
Content: " ",
126+
LeftLine: 14,
127+
RightLine: 14,
128+
},
129+
{
130+
Type: DiffLinePlain,
131+
Content: " This demo also includes an image with changes on a branch for examination of image diff on GitHub.",
132+
LeftLine: 15,
133+
RightLine: 15,
134+
},
135+
{
136+
Type: DiffLineAdd,
137+
Content: "+",
138+
LeftLine: 0,
139+
RightLine: 16,
140+
},
141+
{
142+
Type: DiffLineAdd,
143+
Content: "+",
144+
LeftLine: 0,
145+
RightLine: 17,
146+
},
147+
{
148+
Type: DiffLineAdd,
149+
Content: "+git-module",
150+
LeftLine: 0,
151+
RightLine: 18,
152+
},
153+
},
154+
numAdditions: 3,
155+
numDeletions: 0,
156+
},
157+
},
158+
numAdditions: 3,
159+
numDeletions: 0,
160+
oldName: "README.txt",
161+
mode: 0o100644,
162+
oldMode: 0o100644,
163+
isBinary: false,
164+
isSubmodule: false,
165+
isIncomplete: false,
166+
},
167+
{
168+
Name: "resources/newfile",
169+
Type: DiffFileAdd,
170+
Index: "30f51a3fba5274d53522d0f19748456974647b4f",
171+
OldIndex: "0000000000000000000000000000000000000000",
172+
Sections: []*DiffSection{
173+
{
174+
Lines: []*DiffLine{
175+
{
176+
Type: DiffLineSection,
177+
Content: "@@ -0,0 +1 @@",
178+
},
179+
{
180+
Type: DiffLineAdd,
181+
Content: "+hello, world!",
182+
LeftLine: 0,
183+
RightLine: 1,
184+
},
185+
},
186+
numAdditions: 1,
187+
numDeletions: 0,
188+
},
189+
},
190+
numAdditions: 1,
191+
numDeletions: 0,
192+
oldName: "resources/newfile",
193+
mode: 0o100644,
194+
oldMode: 0o100644,
195+
isBinary: false,
196+
isSubmodule: false,
197+
isIncomplete: false,
198+
},
199+
},
200+
}
201+
202+
diff, err := repo.StashDiff(want[1].Index, 0, 0, 0, DiffOptions{
203+
CommandOptions: CommandOptions{
204+
Envs: []string{"GIT_CONFIG_GLOBAL=/dev/null"},
205+
},
206+
})
207+
require.NoError(t, err)
208+
require.Equalf(t, wantDiff, diff, "StashDiff() got = %v, want %v", diff, wantDiff)
209+
}

0 commit comments

Comments
 (0)