cli: rework new and comment command to better use the editor

Michael Muré created

a nice templace is now provided with explanations

new: title and message can now be provided from the editor. Title will
be the first non-empty line

Change summary

bug/operation.go            |   3 +
commands/comment.go         |  12 +++-
commands/new.go             |  36 ++++++------
doc/bash_completion/git-bug |   3 +
doc/man/git-bug-new.3       |   6 +
doc/md/git-bug_new.md       |   3 
input/input.go              | 111 +++++++++++++++++++++++++++++++++++++-
7 files changed, 146 insertions(+), 28 deletions(-)

Detailed changes

bug/operation.go 🔗

@@ -17,6 +17,9 @@ type Operation interface {
 	OpType() OperationType
 	Time() time.Time
 	Apply(snapshot Snapshot) Snapshot
+
+	// TODO: data validation (ex: a title is a single line)
+	// Validate() bool
 }
 
 type OpBase struct {

commands/comment.go 🔗

@@ -2,9 +2,10 @@ package commands
 
 import (
 	"errors"
+	"fmt"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/bug/operations"
-	"github.com/MichaelMure/git-bug/commands/input"
+	"github.com/MichaelMure/git-bug/input"
 	"github.com/spf13/cobra"
 )
 
@@ -32,8 +33,13 @@ func runComment(cmd *cobra.Command, args []string) error {
 			return err
 		}
 	}
-	if commentMessageFile == "" && commentMessage == "" {
-		commentMessage, err = input.LaunchEditor(repo, messageFilename)
+
+	if commentMessage == "" {
+		commentMessage, err = input.BugCommentEditorInput(repo, messageFilename)
+		if err == input.ErrEmptyMessage {
+			fmt.Println("Empty message, aborting.")
+			return nil
+		}
 		if err != nil {
 			return err
 		}

commands/new.go 🔗

@@ -1,39 +1,36 @@
 package commands
 
 import (
-	"errors"
 	"fmt"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/bug/operations"
-	"github.com/MichaelMure/git-bug/commands/input"
+	"github.com/MichaelMure/git-bug/input"
 	"github.com/spf13/cobra"
 )
 
 var (
-	newMessageFile string
+	newTitle       string
 	newMessage     string
+	newMessageFile string
 )
 
 func runNewBug(cmd *cobra.Command, args []string) error {
 	var err error
 
-	if len(args) == 0 {
-		return errors.New("No title provided")
-	}
-	if len(args) > 1 {
-		return errors.New("Only accepting one title is supported")
-	}
-
-	title := args[0]
-
 	if newMessageFile != "" && newMessage == "" {
 		newMessage, err = input.FromFile(newMessageFile)
 		if err != nil {
 			return err
 		}
 	}
-	if newMessageFile == "" && newMessage == "" {
-		newMessage, err = input.LaunchEditor(repo, messageFilename)
+
+	if newMessage == "" || newTitle == "" {
+		newTitle, newMessage, err = input.BugCreateEditorInput(repo, messageFilename, newTitle, newMessage)
+
+		if err == input.ErrEmptyTitle {
+			fmt.Println("Empty title, aborting.")
+			return nil
+		}
 		if err != nil {
 			return err
 		}
@@ -44,7 +41,7 @@ func runNewBug(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	newBug, err := operations.Create(author, title, newMessage)
+	newBug, err := operations.Create(author, newTitle, newMessage)
 	if err != nil {
 		return err
 	}
@@ -61,7 +58,7 @@ func runNewBug(cmd *cobra.Command, args []string) error {
 }
 
 var newCmd = &cobra.Command{
-	Use:   "new <title> [<option>...]",
+	Use:   "new [<option>...]",
 	Short: "Create a new bug",
 	RunE:  runNewBug,
 }
@@ -69,10 +66,13 @@ var newCmd = &cobra.Command{
 func init() {
 	RootCmd.AddCommand(newCmd)
 
-	newCmd.Flags().StringVarP(&newMessageFile, "file", "F", "",
-		"Take the message from the given file. Use - to read the message from the standard input",
+	newCmd.Flags().StringVarP(&newTitle, "title", "t", "",
+		"Provide a title to describe the issue",
 	)
 	newCmd.Flags().StringVarP(&newMessage, "message", "m", "",
 		"Provide a message to describe the issue",
 	)
+	newCmd.Flags().StringVarP(&newMessageFile, "file", "F", "",
+		"Take the message from the given file. Use - to read the message from the standard input",
+	)
 }

doc/bash_completion/git-bug 🔗

@@ -375,6 +375,9 @@ _git-bug_new()
     flags+=("--message=")
     two_word_flags+=("-m")
     local_nonpersistent_flags+=("--message=")
+    flags+=("--title=")
+    two_word_flags+=("-t")
+    local_nonpersistent_flags+=("--title=")
 
     must_have_one_flag=()
     must_have_one_noun=()

doc/man/git-bug-new.3 🔗

@@ -10,7 +10,7 @@ git\-bug\-new \- Create a new bug
 
 .SH SYNOPSIS
 .PP
-\fBgit\-bug new <title> [<option>\&...] [flags]\fP
+\fBgit\-bug new [<option>\&...] [flags]\fP
 
 
 .SH DESCRIPTION
@@ -31,6 +31,10 @@ Create a new bug
 \fB\-m\fP, \fB\-\-message\fP=""
     Provide a message to describe the issue
 
+.PP
+\fB\-t\fP, \fB\-\-title\fP=""
+    Provide a title to describe the issue
+
 
 .SH SEE ALSO
 .PP

doc/md/git-bug_new.md 🔗

@@ -7,7 +7,7 @@ Create a new bug
 Create a new bug
 
 ```
-git-bug new <title> [<option>...] [flags]
+git-bug new [<option>...] [flags]
 ```
 
 ### Options
@@ -16,6 +16,7 @@ git-bug new <title> [<option>...] [flags]
   -F, --file string      Take the message from the given file. Use - to read the message from the standard input
   -h, --help             help for new
   -m, --message string   Provide a message to describe the issue
+  -t, --title string     Provide a title to describe the issue
 ```
 
 ### SEE ALSO

commands/input/input.go → input/input.go 🔗

@@ -1,4 +1,4 @@
-// Taken from the git-appraise project
+// Originally taken from the git-appraise project
 
 package input
 
@@ -7,11 +7,111 @@ import (
 	"bytes"
 	"fmt"
 	"github.com/MichaelMure/git-bug/repository"
+	"github.com/pkg/errors"
 	"io/ioutil"
 	"os"
 	"os/exec"
+	"strings"
 )
 
+var ErrEmptyMessage = errors.New("empty message")
+var ErrEmptyTitle = errors.New("empty title")
+
+const bugTitleCommentTemplate = `%s%s
+
+# Please enter the title and comment message. The first non-empty line will be
+# used as the title. Lines starting with '#' will be ignored.
+# An empty title aborts the operation.
+`
+
+func BugCreateEditorInput(repo repository.Repo, fileName string, preTitle string, preMessage string) (string, string, error) {
+	if preMessage != "" {
+		preMessage = "\n\n" + preMessage
+	}
+
+	template := fmt.Sprintf(bugTitleCommentTemplate, preTitle, preMessage)
+
+	raw, err := LaunchEditorWithTemplate(repo, fileName, template)
+
+	if err != nil {
+		return "", "", err
+	}
+
+	lines := strings.Split(raw, "\n")
+
+	var title string
+	var b strings.Builder
+	for _, line := range lines {
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+
+		if title == "" {
+			trimmed := strings.TrimSpace(line)
+			if trimmed != "" {
+				title = trimmed
+			}
+			continue
+		}
+
+		b.WriteString(line)
+		b.WriteString("\n")
+	}
+
+	if title == "" {
+		return "", "", ErrEmptyTitle
+	}
+
+	message := strings.TrimSpace(b.String())
+
+	return title, message, nil
+}
+
+const bugCommentTemplate = `
+
+# Please enter the comment message. Lines starting with '#' will be ignored,
+# and an empty message aborts the operation.
+`
+
+func BugCommentEditorInput(repo repository.Repo, fileName string) (string, error) {
+	raw, err := LaunchEditorWithTemplate(repo, fileName, bugCommentTemplate)
+
+	if err != nil {
+		return "", err
+	}
+
+	lines := strings.Split(raw, "\n")
+
+	var b strings.Builder
+	for _, line := range lines {
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		b.WriteString(line)
+		b.WriteString("\n")
+	}
+
+	message := strings.TrimSpace(b.String())
+
+	if message == "" {
+		return "", ErrEmptyMessage
+	}
+
+	return message, nil
+}
+
+func LaunchEditorWithTemplate(repo repository.Repo, fileName string, template string) (string, error) {
+	path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
+
+	err := ioutil.WriteFile(path, []byte(template), 0644)
+
+	if err != nil {
+		return "", err
+	}
+
+	return LaunchEditor(repo, fileName)
+}
+
 // LaunchEditor launches the default editor configured for the given repo. This
 // method blocks until the editor command has returned.
 //
@@ -22,13 +122,14 @@ import (
 // This method returns the text that was read from the temporary file, or
 // an error if any step in the process failed.
 func LaunchEditor(repo repository.Repo, fileName string) (string, error) {
+	path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
+	defer os.Remove(path)
+
 	editor, err := repo.GetCoreEditor()
 	if err != nil {
 		return "", fmt.Errorf("Unable to detect default git editor: %v\n", err)
 	}
 
-	path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
-
 	cmd, err := startInlineCommand(editor, path)
 	if err != nil {
 		// Running the editor directly did not work. This might mean that
@@ -50,11 +151,11 @@ func LaunchEditor(repo repository.Repo, fileName string) (string, error) {
 	}
 
 	output, err := ioutil.ReadFile(path)
+
 	if err != nil {
-		os.Remove(path)
 		return "", fmt.Errorf("Error reading edited file: %v\n", err)
 	}
-	os.Remove(path)
+
 	return string(output), err
 }