From 5e8facf7a21336cb70a3740a6c71b69ed75636b2 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 24 Oct 2023 15:55:31 -0400 Subject: [PATCH 1/2] feat: add stash command - Add stash push - List - Show --- repo_stash.go | 125 +++++++++++++++++++++++++++ repo_stash_test.go | 209 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 repo_stash.go create mode 100644 repo_stash_test.go diff --git a/repo_stash.go b/repo_stash.go new file mode 100644 index 00000000..3e86d260 --- /dev/null +++ b/repo_stash.go @@ -0,0 +1,125 @@ +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] + } + + 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++ { + line := strings.TrimSpace(lines[i]) + // Init entry + if match := stashLineRegexp.FindStringSubmatch(line); len(match) == 3 { + if entry != nil { + 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 && line != "" { + entry.Files = append(entry.Files, line) + } else { + continue + } + } + + if entry != nil { + 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 +} diff --git a/repo_stash_test.go b/repo_stash_test.go new file mode 100644 index 00000000..37517cae --- /dev/null +++ b/repo_stash_test.go @@ -0,0 +1,209 @@ +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(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) +} From aa89c50b8d95e6b9723bda98673d4109a3c7f5b9 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Sun, 29 Sep 2024 20:06:08 -0400 Subject: [PATCH 2/2] minor fixup logic --- .github/workflows/go.yml | 2 +- command.go | 8 ++++---- repo_stash.go | 29 +++++++++++++---------------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index bc4ff63f..3726563e 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -27,7 +27,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: 1.23.x - name: Check Go module tidiness 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 index 3e86d260..6974e8e3 100644 --- a/repo_stash.go +++ b/repo_stash.go @@ -12,10 +12,8 @@ import ( 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 } @@ -36,44 +34,43 @@ func (r *Repository) StashList(opts ...StashListOptions) ([]*Stash, error) { opt = opts[0] } - stash := make([]*Stash, 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 entry *Stash + var stash *Stash lines := strings.Split(stdout.String(), "\n") for i := 0; i < len(lines); i++ { line := strings.TrimSpace(lines[i]) // Init entry if match := stashLineRegexp.FindStringSubmatch(line); len(match) == 3 { - if entry != nil { - stash = append(stash, entry) + // Append the previous stash + if stash != nil { + stashes = append(stashes, stash) } idx, err := strconv.Atoi(match[1]) if err != nil { idx = -1 } - entry = &Stash{ + stash = &Stash{ Index: idx, Message: match[2], Files: make([]string, 0), } - } else if entry != nil && line != "" { - entry.Files = append(entry.Files, line) - } else { - continue + } else if stash != nil && line != "" { + stash.Files = append(stash.Files, line) } } - if entry != nil { - stash = append(stash, entry) + // Append the last stash + if stash != nil { + stashes = append(stashes, stash) } - - return stash, nil + return stashes, nil } // StashDiff returns a parsed diff object for the given stash index. @@ -90,7 +87,7 @@ func (r *Repository) StashDiff(index int, maxFiles, maxFileLines, maxLineChars i go StreamParseDiff(stdout, done, maxFiles, maxFileLines, maxLineChars) stderr := new(bytes.Buffer) - err := cmd.RunInDirPipelineWithTimeout(opt.Timeout, w, stderr, r.path) + err := cmd.RunInDirPipeline(w, stderr, r.path) _ = w.Close() // Close writer to exit parsing goroutine if err != nil { return nil, concatenateError(err, stderr.String())