diff --git a/command.go b/command.go index 4dc5f017..7e4b52db 100644 --- a/command.go +++ b/command.go @@ -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. diff --git a/repo_stash.go b/repo_stash.go new file mode 100644 index 00000000..4a24a316 --- /dev/null +++ b/repo_stash.go @@ -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 := range lines { + 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 { + continue + } + 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 +} diff --git a/repo_stash_test.go b/repo_stash_test.go new file mode 100644 index 00000000..43d7492f --- /dev/null +++ b/repo_stash_test.go @@ -0,0 +1,186 @@ +package git + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStashWorktreeError(t *testing.T) { + _, err := testrepo.StashList() + assert.Errorf(t, err, "StashList() should return an error when not run in a work tree") +} + +func TestStash(t *testing.T) { + tmp := t.TempDir() + path, err := filepath.Abs(repoPath) + require.NoError(t, err) + + require.NoError(t, Clone("file://"+path, tmp)) + + repo, err := Open(tmp) + require.NoError(t, err) + + err = os.WriteFile(tmp+"/resources/newfile", []byte("hello, world!"), 0o644) + require.NoError(t, err) + + f, err := os.OpenFile(tmp+"/README.txt", os.O_APPEND|os.O_WRONLY, 0o644) + require.NoError(t, err) + + _, err = f.WriteString("\n\ngit-module") + require.NoError(t, err) + + f.Close() + err = repo.Add(AddOptions{All: true}) + require.NoError(t, err) + + err = repo.StashPush("") + require.NoError(t, err) + + f, err = os.OpenFile(tmp+"/README.txt", os.O_APPEND|os.O_WRONLY, 0o644) + require.NoError(t, err) + + _, err = f.WriteString("\n\nstash 1") + require.NoError(t, err) + + f.Close() + err = repo.Add(AddOptions{All: true}) + require.NoError(t, err) + + err = repo.StashPush("custom message") + require.NoError(t, 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) +}