commands/stack/save/stack_save.go (183 lines of code) (raw):
package save
import (
"encoding/hex"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/MakeNowJust/heredoc/v2"
"github.com/briandowns/spinner"
"gitlab.com/gitlab-org/cli/internal/run"
"gitlab.com/gitlab-org/cli/pkg/git"
"gitlab.com/gitlab-org/cli/pkg/text"
"golang.org/x/crypto/sha3"
"github.com/spf13/cobra"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
)
var description string
func NewCmdSaveStack(f *cmdutils.Factory, gr git.GitRunner, getText cmdutils.GetTextUsingEditor) *cobra.Command {
stackSaveCmd := &cobra.Command{
Use: "save",
Short: `Save your progress within a stacked diff. (EXPERIMENTAL.)`,
Long: `Save your current progress with a diff on the stack.
` + text.ExperimentalString,
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 {
if cmd.Flags().Changed("message") && cmd.Flags().Changed("description") {
return &cmdutils.FlagError{Err: errors.New("specify either of --message or --description.")}
}
// check if there are even any changes before we start
err := checkForChanges()
if err != nil {
return fmt.Errorf("could not save: %v", err)
}
// a description is required, so ask if one is not provided
if description == "" {
description, err = promptForCommit(f, getText, "")
if err != nil {
return fmt.Errorf("error getting commit message: %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)
}
author, err := git.GitUserName()
if err != nil {
return fmt.Errorf("error getting Git author: %v", err)
}
// generate a SHA based on: commit message, stack title, Git author name
sha, err := generateStackSha(description, title, string(author), time.Now())
if err != nil {
return fmt.Errorf("error generating hash for stack branch name: %v", err)
}
// create branch name from SHA
branch, err := createShaBranch(f, sha, title)
if err != nil {
return fmt.Errorf("error creating branch name: %v", err)
}
// create the branch prefix-stack_title-SHA
err = git.CheckoutNewBranch(branch)
if err != nil {
return fmt.Errorf("error running branch checkout: %v", err)
}
// commit files to branch
_, err = commitFiles(description)
if err != nil {
return fmt.Errorf("error committing files: %v", err)
}
stack, err := git.GatherStackRefs(title)
if err != nil {
return fmt.Errorf("error getting refs from file system: %v", err)
}
var stackRef git.StackRef
if !stack.Empty() {
lastRef := stack.Last()
// update the ref before it (the current last ref)
err = git.UpdateStackRefFile(title, git.StackRef{
Prev: lastRef.Prev,
MR: lastRef.MR,
Description: lastRef.Description,
SHA: lastRef.SHA,
Branch: lastRef.Branch,
Next: sha,
})
if err != nil {
return fmt.Errorf("error updating old ref: %v", err)
}
stackRef = git.StackRef{Prev: lastRef.SHA, SHA: sha, Branch: branch, Description: description}
} else {
stackRef = git.StackRef{SHA: sha, Branch: branch, Description: description}
}
err = git.AddStackRefFile(title, stackRef)
if err != nil {
return fmt.Errorf("error creating stack file: %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),
description,
)
}
s.Stop()
return nil
},
}
stackSaveCmd.Flags().StringVarP(&description, "description", "d", "", "Description of the change.")
stackSaveCmd.Flags().StringVarP(&description, "message", "m", "", "Alias for the description flag.")
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) {
if len(args) == 0 {
args = []string{"."}
}
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
}
func commitFiles(message string) (string, error) {
commitCmd := git.GitCommand("commit", "-m", message)
output, err := run.PrepareCmd(commitCmd).Output()
if err != nil {
return "", fmt.Errorf("error running Git command: %v", err)
}
return string(output), nil
}
func generateStackSha(message string, title string, author string, timestamp time.Time) (string, error) {
toSha := []byte(message + title + author + timestamp.String())
hashData := make([]byte, 4)
shakeHash := sha3.NewShake256()
shakeHash.Write(toSha)
_, err := shakeHash.Read(hashData)
if err != nil {
return "", fmt.Errorf("error generating hash for stack branch: %v", err)
}
return hex.EncodeToString(hashData), nil
}
func createShaBranch(f *cmdutils.Factory, sha string, title string) (string, error) {
cfg, err := f.Config()
if err != nil {
return "", fmt.Errorf("could not retrieve config file: %v", err)
}
prefix, err := cfg.Get("", "branch_prefix")
if err != nil {
return "", fmt.Errorf("could not get prefix config: %v", err)
}
if prefix == "" {
prefix = os.Getenv("USER")
if prefix == "" {
prefix = "glab-stack"
}
}
branchTitle := []string{prefix, title, sha}
branch := strings.Join(branchTitle, "-")
return string(branch), nil
}