add the new bug command with a very primitive bug datastructure

Michael Muré created

Change summary

bug/bug.go              |   6 ++
bug/comment.go          |   6 ++
bug/person.go           |  31 ++++++++++++
commands/commands.go    |   2 
commands/input/input.go | 104 +++++++++++++++++++++++++++++++++++++++++++
commands/new.go         |  75 +++++++++++++++++++++++++++++++
commands/pull.go        |  12 ++--
commands/push.go        |  12 ++--
repository/git.go       |  30 ++++++------
repository/mock_repo.go |   4 
repository/repo.go      |   3 +
11 files changed, 254 insertions(+), 31 deletions(-)

Detailed changes

bug/bug.go 🔗

@@ -0,0 +1,6 @@
+package bug
+
+type Bug struct {
+	Title    string
+	Comments []Comment
+}

bug/comment.go 🔗

@@ -0,0 +1,6 @@
+package bug
+
+type Comment struct {
+	Author  Person
+	Message string
+}

bug/person.go 🔗

@@ -0,0 +1,31 @@
+package bug
+
+import (
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/pkg/errors"
+)
+
+type Person struct {
+	Name  string
+	Email string
+}
+
+func GetUser(repo repository.Repo) (Person, error) {
+	name, err := repo.GetUserName()
+	if err != nil {
+		return Person{}, err
+	}
+	if name == "" {
+		return Person{}, errors.New("User name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`")
+	}
+
+	email, err := repo.GetUserEmail()
+	if err != nil {
+		return Person{}, err
+	}
+	if email == "" {
+		return Person{}, errors.New("User name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`")
+	}
+
+	return Person{Name: name, Email: email}, nil
+}

commands/commands.go 🔗

@@ -6,6 +6,7 @@ import (
 )
 
 const bugsRefPattern = "refs/bugs/*"
+const messageFilename = "BUG_MESSAGE_EDITMSG"
 
 // Command represents the definition of a single command.
 type Command struct {
@@ -23,6 +24,7 @@ func (cmd *Command) Run(repo repository.Repo, args []string) error {
 
 // CommandMap defines all of the available (sub)commands.
 var CommandMap = map[string]*Command{
+	"new":  newCmd,
 	"pull": pullCmd,
 	"push": pushCmd,
 

commands/input/input.go 🔗

@@ -0,0 +1,104 @@
+// Taken from the git-appraise project
+
+package input
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"github.com/MichaelMure/git-bug/repository"
+	"io/ioutil"
+	"os"
+	"os/exec"
+)
+
+// LaunchEditor launches the default editor configured for the given repo. This
+// method blocks until the editor command has returned.
+//
+// The specified filename should be a temporary file and provided as a relative path
+// from the repo (e.g. "FILENAME" will be converted to ".git/FILENAME"). This file
+// will be deleted after the editor is closed and its contents have been read.
+//
+// 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) {
+	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
+		// the editor string is not a path to an executable, but rather
+		// a shell command (e.g. "emacsclient --tty"). As such, we'll try
+		// to run the command through bash, and if that fails, try with sh
+		args := []string{"-c", fmt.Sprintf("%s %q", editor, path)}
+		cmd, err = startInlineCommand("bash", args...)
+		if err != nil {
+			cmd, err = startInlineCommand("sh", args...)
+		}
+	}
+	if err != nil {
+		return "", fmt.Errorf("Unable to start editor: %v\n", err)
+	}
+
+	if err := cmd.Wait(); err != nil {
+		return "", fmt.Errorf("Editing finished with error: %v\n", err)
+	}
+
+	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
+}
+
+// FromFile loads and returns the contents of a given file. If - is passed
+// through, much like git, it will read from stdin. This can be piped data,
+// unless there is a tty in which case the user will be prompted to enter a
+// message.
+func FromFile(fileName string) (string, error) {
+	if fileName == "-" {
+		stat, err := os.Stdin.Stat()
+		if err != nil {
+			return "", fmt.Errorf("Error reading from stdin: %v\n", err)
+		}
+		if (stat.Mode() & os.ModeCharDevice) == 0 {
+			// There is no tty. This will allow us to read piped data instead.
+			output, err := ioutil.ReadAll(os.Stdin)
+			if err != nil {
+				return "", fmt.Errorf("Error reading from stdin: %v\n", err)
+			}
+			return string(output), err
+		}
+
+		fmt.Printf("(reading comment from standard input)\n")
+		var output bytes.Buffer
+		s := bufio.NewScanner(os.Stdin)
+		for s.Scan() {
+			output.Write(s.Bytes())
+			output.WriteRune('\n')
+		}
+		return output.String(), nil
+	}
+
+	output, err := ioutil.ReadFile(fileName)
+	if err != nil {
+		return "", fmt.Errorf("Error reading file: %v\n", err)
+	}
+	return string(output), err
+}
+
+func startInlineCommand(command string, args ...string) (*exec.Cmd, error) {
+	cmd := exec.Command(command, args...)
+	cmd.Stdin = os.Stdin
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	err := cmd.Start()
+	return cmd, err
+}

commands/new.go 🔗

@@ -0,0 +1,75 @@
+package commands
+
+import (
+	"flag"
+	"fmt"
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/commands/input"
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/pkg/errors"
+)
+
+var newFlagSet = flag.NewFlagSet("new", flag.ExitOnError)
+
+var (
+	newMessageFile = newFlagSet.String("F", "", "Take the message from the given file. Use - to read the message from the standard input")
+	newMessage     = newFlagSet.String("m", "", "Provide a message to describe the issue")
+)
+
+func newBug(repo repository.Repo, args []string) error {
+	newFlagSet.Parse(args)
+	args = newFlagSet.Args()
+
+	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 err != nil {
+			return err
+		}
+	}
+
+	// Note: this is very primitive for now
+	author, err := bug.GetUser(repo)
+	if err != nil {
+		return err
+	}
+
+	comment := bug.Comment{
+		Author:  author,
+		Message: *newMessage,
+	}
+
+	bug := bug.Bug{
+		Title:    title,
+		Comments: []bug.Comment{comment},
+	}
+
+	fmt.Println(bug)
+
+	return nil
+
+}
+
+var newCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s new <title> [<option>...]\n\nOptions:\n", arg0)
+		newFlagSet.PrintDefaults()
+	},
+	RunMethod: newBug,
+}

commands/pull.go 🔗

@@ -1,14 +1,14 @@
-package  commands
+package commands
 
 import (
+	"errors"
 	"fmt"
 	"github.com/MichaelMure/git-bug/repository"
-	"errors"
 )
 
 func pull(repo repository.Repo, args []string) error {
 	if len(args) > 1 {
-		return errors.New("only pulling from one remote at a time is supported")
+		return errors.New("Only pulling from one remote at a time is supported")
 	}
 
 	remote := "origin"
@@ -27,7 +27,5 @@ var pullCmd = &Command{
 	Usage: func(arg0 string) {
 		fmt.Printf("Usage: %s pull [<remote>]\n", arg0)
 	},
-	RunMethod: func(repo repository.Repo, args []string) error {
-		return pull(repo, args)
-	},
-}
+	RunMethod: pull,
+}

commands/push.go 🔗

@@ -1,14 +1,14 @@
-package  commands
+package commands
 
 import (
+	"errors"
 	"fmt"
 	"github.com/MichaelMure/git-bug/repository"
-	"errors"
 )
 
 func push(repo repository.Repo, args []string) error {
 	if len(args) > 1 {
-		return errors.New("only pushing to one remote at a time is supported")
+		return errors.New("Only pushing to one remote at a time is supported")
 	}
 
 	remote := "origin"
@@ -27,7 +27,5 @@ var pushCmd = &Command{
 	Usage: func(arg0 string) {
 		fmt.Printf("Usage: %s push [<remote>]\n", arg0)
 	},
-	RunMethod: func(repo repository.Repo, args []string) error {
-		return push(repo, args)
-	},
-}
+	RunMethod: push,
+}

repository/git.go 🔗

@@ -78,6 +78,11 @@ func (repo *GitRepo) GetRepoStateHash() (string, error) {
 	return fmt.Sprintf("%x", sha1.Sum([]byte(stateSummary))), err
 }
 
+// GetUserName returns the name the the user has used to configure git
+func (repo *GitRepo) GetUserName() (string, error) {
+	return repo.runGitCommand("config", "user.name")
+}
+
 // GetUserEmail returns the email address that the user has used to configure git.
 func (repo *GitRepo) GetUserEmail() (string, error) {
 	return repo.runGitCommand("config", "user.email")
@@ -88,30 +93,25 @@ func (repo *GitRepo) GetCoreEditor() (string, error) {
 	return repo.runGitCommand("var", "GIT_EDITOR")
 }
 
-
 // PullRefs pull git refs from a remote
 func (repo *GitRepo) PullRefs(remote string, refPattern string) error {
-	refspec := fmt.Sprintf("%s:%s", refPattern, refPattern)
+	fetchRefSpec := fmt.Sprintf("+%s:%s", refPattern, refPattern)
+	err := repo.runGitCommandInline("fetch", remote, fetchRefSpec)
 
-	// The push is liable to fail if the user forgot to do a pull first, so
-	// we treat errors as user errors rather than fatal errors.
-	err := repo.runGitCommandInline("push", remote, refspec)
-	if err != nil {
-		return fmt.Errorf("failed to push to the remote '%s': %v", remote, err)
-	}
-	return nil
+	// TODO: merge new data
+
+	return err
 }
 
 // PushRefs push git refs to a remote
 func (repo *GitRepo) PushRefs(remote string, refPattern string) error {
-	remoteNotesRefPattern := getRemoteNotesRef(remote, refPattern)
-	fetchRefSpec := fmt.Sprintf("+%s:%s", refPattern, remoteNotesRefPattern)
-	err := repo.runGitCommandInline("fetch", remote, fetchRefSpec)
+	// The push is liable to fail if the user forgot to do a pull first, so
+	// we treat errors as user errors rather than fatal errors.
+	err := repo.runGitCommandInline("push", remote, refPattern)
 	if err != nil {
-		return err
+		return fmt.Errorf("failed to push to the remote '%s': %v", remote, err)
 	}
-
-	return repo.mergeRemoteNotes(remote, notesRefPattern)
+	return nil
 }
 
 /*

repository/mock_repo.go 🔗

@@ -7,7 +7,7 @@ import (
 )
 
 // mockRepoForTest defines an instance of Repo that can be used for testing.
-type mockRepoForTest struct {}
+type mockRepoForTest struct{}
 
 // GetPath returns the path to the repo.
 func (r *mockRepoForTest) GetPath() string { return "~/mockRepo/" }
@@ -30,4 +30,4 @@ func (r *mockRepoForTest) GetCoreEditor() (string, error) { return "vi", nil }
 // PushRefs push git refs to a remote
 func (r *mockRepoForTest) PushRefs(remote string, refPattern string) error {
 	return nil
-}
+}

repository/repo.go 🔗

@@ -6,6 +6,9 @@ type Repo interface {
 	// GetPath returns the path to the repo.
 	GetPath() string
 
+	// GetUserName returns the name the the user has used to configure git
+	GetUserName() (string, error)
+
 	// GetUserEmail returns the email address that the user has used to configure git.
 	GetUserEmail() (string, error)