Merge branch 'gmh-stacked-diffs-add-save-command' into 'gmh-stacked-diffs'

feat(stacked-diffs): Add save command

See merge request https://gitlab.com/gitlab-org/cli/-/merge_requests/1462

Merged-by: Gary Holtz <gholtz@gitlab.com>
Approved-by: Vitali Tatarintev <vtatarintev@gitlab.com>
Reviewed-by: Gary Holtz <gholtz@gitlab.com>
Reviewed-by: Jay McCure <jmccure@gitlab.com>
This commit is contained in:
Gary Holtz 2024-05-14 15:02:19 +00:00
commit df8e7f4f41
7 changed files with 366 additions and 2 deletions

View File

@ -0,0 +1,113 @@
package create
import (
"fmt"
"os"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/briandowns/spinner"
"gitlab.com/gitlab-org/cli/internal/run"
"gitlab.com/gitlab-org/cli/pkg/git"
"gitlab.com/gitlab-org/cli/pkg/prompt"
"github.com/spf13/cobra"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
)
var message string
func NewCmdSaveStack(f *cmdutils.Factory) *cobra.Command {
stackSaveCmd := &cobra.Command{
Use: "save",
Short: `Save your progress within stacked diff`,
Long: "\"save\" lets you save your current progress with a diff on the stack.\n",
Example: heredoc.Doc(`
glab stack save added_file
glab stack save . -m "added a function"
glab stack save -m "added a function"`),
RunE: func(cmd *cobra.Command, args []string) error {
// check if there are even any changes before we start
err := checkForChanges()
if err != nil {
return fmt.Errorf("could not save: %v", err)
}
// a title is required, so ask if one is not provided
if message == "" {
err := prompt.AskQuestionWithInput(&message, "title", "What would you like to name this change?", "", true)
if err != nil {
return fmt.Errorf("error prompting for save title: %v", err)
}
}
s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
// git add files
_, err = addFiles(args[0:])
if err != nil {
return fmt.Errorf("error adding files: %v", err)
}
// get stack title
title, err := git.GetCurrentStackTitle()
if err != nil {
return fmt.Errorf("error running git command: %v", err)
}
if f.IO.IsOutputTTY() {
color := f.IO.Color()
fmt.Fprintf(
f.IO.StdOut,
"%s %s: Saved with message: \"%s\".\n",
color.ProgressIcon(),
color.Blue(title),
message,
)
}
s.Stop()
return nil
},
}
stackSaveCmd.Flags().StringVarP(&message, "message", "m", "", "name the change")
return stackSaveCmd
}
func checkForChanges() error {
gitCmd := git.GitCommand("status", "--porcelain")
output, err := run.PrepareCmd(gitCmd).Output()
if err != nil {
return fmt.Errorf("error running git status: %v", err)
}
if string(output) == "" {
return fmt.Errorf("no changes to save")
}
return nil
}
func addFiles(args []string) (files []string, err error) {
for _, file := range args {
_, err = os.Stat(file)
if err != nil {
return
}
files = append(files, file)
}
cmdargs := append([]string{"add"}, args...)
gitCmd := git.GitCommand(cmdargs...)
_, err = run.PrepareCmd(gitCmd).Output()
if err != nil {
return []string{}, fmt.Errorf("error running git add: %v", err)
}
return files, err
}

View File

@ -0,0 +1,197 @@
package create
import (
"net/http"
"os"
"path"
"strings"
"testing"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/cli/commands/cmdtest"
"gitlab.com/gitlab-org/cli/internal/run"
"gitlab.com/gitlab-org/cli/pkg/git"
"gitlab.com/gitlab-org/cli/pkg/prompt"
"gitlab.com/gitlab-org/cli/test"
)
func runCommand(rt http.RoundTripper, isTTY bool, args string) (*test.CmdOut, error) {
ios, _, stdout, stderr := cmdtest.InitIOStreams(isTTY, "")
factory := cmdtest.InitFactory(ios, rt)
_, _ = factory.HttpClient()
cmd := NewCmdSaveStack(factory)
return cmdtest.ExecuteCommand(cmd, args, stdout, stderr)
}
func TestSaveNewStack(t *testing.T) {
tests := []struct {
desc string
args []string
files []string
message string
expected string
wantErr bool
}{
{
desc: "adding regular files",
args: []string{"testfile", "randomfile"},
files: []string{"testfile", "randomfile"},
message: "this is a commit message",
expected: "• cool test feature: Saved with message: \"this is a commit message\".\n",
},
{
desc: "adding files with a dot argument",
args: []string{"."},
files: []string{"testfile", "randomfile"},
message: "this is a commit message",
expected: "• cool test feature: Saved with message: \"this is a commit message\".\n",
},
{
desc: "omitting a message",
args: []string{"."},
files: []string{"testfile"},
expected: "• cool test feature: Saved with message: \"oh ok fine how about blah blah\".\n",
},
{
desc: "with no changed files",
args: []string{"."},
files: []string{},
expected: "could not save: \"no changes to save\"",
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
if tc.message == "" {
as, restoreAsk := prompt.InitAskStubber()
defer restoreAsk()
as.Stub([]*prompt.QuestionStub{
{
Name: "title",
Value: "oh ok fine how about blah blah",
},
})
} else {
tc.args = append(tc.args, "-m")
tc.args = append(tc.args, "\""+tc.message+"\"")
}
dir := git.InitGitRepoWithCommit(t)
err := git.SetLocalConfig("glab.currentstack", "cool test feature")
require.Nil(t, err)
createTemporaryFiles(t, dir, tc.files)
args := strings.Join(tc.args, " ")
output, err := runCommand(nil, true, args)
if tc.wantErr {
require.Errorf(t, err, tc.expected)
} else {
require.Nil(t, err)
require.Equal(t, tc.expected, output.String())
}
})
}
}
func Test_addFiles(t *testing.T) {
tests := []struct {
desc string
args []string
expected []string
}{
{
desc: "adding regular files",
args: []string{"file1", "file2"},
expected: []string{"file1", "file2"},
},
{
desc: "adding files with a dot argument",
args: []string{"."},
expected: []string{"file1", "file2"},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
dir := git.InitGitRepoWithCommit(t)
err := git.SetLocalConfig("glab.currentstack", "cool test feature")
require.Nil(t, err)
createTemporaryFiles(t, dir, tc.expected)
_, err = addFiles(tc.args)
require.Nil(t, err)
gitCmd := git.GitCommand("status", "--short", "-u")
output, err := run.PrepareCmd(gitCmd).Output()
require.Nil(t, err)
normalizedFiles := []string{}
for _, file := range tc.expected {
file = "A " + file
normalizedFiles = append(normalizedFiles, file)
}
formattedOutput := strings.Replace(string(output), "\n", "", -1)
require.Equal(t, formattedOutput, strings.Join(normalizedFiles, ""))
})
}
}
func Test_checkForChanges(t *testing.T) {
tests := []struct {
desc string
args []string
expected bool
}{
{
desc: "check for changes with modified files",
args: []string{"file1", "file2"},
expected: true,
},
{
desc: "check for changes without anything",
args: []string{},
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
dir := git.InitGitRepoWithCommit(t)
err := git.SetLocalConfig("glab.currentstack", "cool test feature")
require.Nil(t, err)
createTemporaryFiles(t, dir, tc.args)
err = checkForChanges()
if tc.expected {
require.Nil(t, err)
} else {
require.Error(t, err)
}
})
}
}
func createTemporaryFiles(t *testing.T, dir string, files []string) {
for _, file := range files {
file = path.Join(dir, file)
_, err := os.Create(file)
require.Nil(t, err)
}
}

View File

@ -3,6 +3,7 @@ package stack
import (
"gitlab.com/gitlab-org/cli/commands/cmdutils"
stackCreateCmd "gitlab.com/gitlab-org/cli/commands/stack/create"
stackSaveCmd "gitlab.com/gitlab-org/cli/commands/stack/save"
"github.com/spf13/cobra"
)
@ -18,5 +19,6 @@ func NewCmdStack(f *cmdutils.Factory) *cobra.Command {
cmdutils.EnableRepoOverride(stackCmd, f)
stackCmd.AddCommand(stackCreateCmd.NewCmdCreateStack(f))
stackCmd.AddCommand(stackSaveCmd.NewCmdSaveStack(f))
return stackCmd
}

View File

@ -34,3 +34,4 @@ stacks
## Subcommands
- [`create`](create.md)
- [`save`](save.md)

43
docs/source/stack/save.md Normal file
View File

@ -0,0 +1,43 @@
---
stage: Create
group: Code Review
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
<!--
This documentation is auto generated by a script.
Please do not edit this file directly. Run `make gen-docs` instead.
-->
# `glab stack save`
Save your progress within stacked diff
## Synopsis
"save" lets you save your current progress with a diff on the stack.
```plaintext
glab stack save [flags]
```
## Examples
```plaintext
glab stack save added_file
glab stack save . -m "added a function"
glab stack save -m "added a function"
```
## Options
```plaintext
-m, --message string name the change
```
## Options inherited from parent commands
```plaintext
--help Show help for command
-R, --repo OWNER/REPO Select another repository using the OWNER/REPO or `GROUP/NAMESPACE/REPO` format or full URL or git URL
```

View File

@ -294,6 +294,12 @@ func CheckoutBranch(branch string) error {
return err
}
func CheckoutNewBranch(branch string) error {
configCmd := GitCommand("checkout", "-b", branch)
err := run.PrepareCmd(configCmd).Run()
return err
}
func parseCloneArgs(extraArgs []string) (args []string, target string) {
args, target = parseArgs(extraArgs)
return

View File

@ -23,8 +23,8 @@ func InitGitRepo(t *testing.T) string {
return tempDir
}
func InitGitRepoWithCommit(t *testing.T) {
InitGitRepo(t)
func InitGitRepoWithCommit(t *testing.T) string {
tempDir := InitGitRepo(t)
configureGitConfig(t)
@ -38,6 +38,8 @@ func InitGitRepoWithCommit(t *testing.T) {
gitCommit := GitCommand("commit", "-m", "\"commit\"")
_, err = run.PrepareCmd(gitCommit).Output()
require.NoError(t, err)
return tempDir
}
func configureGitConfig(t *testing.T) {