commands: add a package to handle implicit bug selection

Michael Muré created

Change summary

bug/bug.go                     |   9 +-
cache/bug_cache.go             |   4 +
cache/repo_cache.go            |   4 +
commands/select/select.go      | 125 ++++++++++++++++++++++++++++++++++++
commands/select/select_test.go | 109 +++++++++++++++++++++++++++++++
5 files changed, 247 insertions(+), 4 deletions(-)

Detailed changes

bug/bug.go 🔗

@@ -27,6 +27,8 @@ const editClockEntryPattern = "edit-clock-%d"
 const idLength = 40
 const humanIdLength = 7
 
+var ErrBugNotExist = errors.New("bug doesn't exist")
+
 var _ Interface = &Bug{}
 
 // Bug hold the data of a bug thread, organized in a way close to
@@ -106,7 +108,7 @@ func readBug(repo repository.Repo, ref string) (*Bug, error) {
 	hashes, err := repo.ListCommits(ref)
 
 	if err != nil {
-		return nil, err
+		return nil, ErrBugNotExist
 	}
 
 	refSplit := strings.Split(ref, "/")
@@ -123,13 +125,12 @@ func readBug(repo repository.Repo, ref string) (*Bug, error) {
 	// Load each OperationPack
 	for _, hash := range hashes {
 		entries, err := repo.ListEntries(hash)
-
-		bug.lastCommit = hash
-
 		if err != nil {
 			return nil, err
 		}
 
+		bug.lastCommit = hash
+
 		var opsEntry repository.TreeEntry
 		opsFound := false
 		var rootEntry repository.TreeEntry

cache/bug_cache.go 🔗

@@ -22,6 +22,10 @@ func (c *BugCache) Snapshot() *bug.Snapshot {
 	return c.bug.Snapshot()
 }
 
+func (c *BugCache) Id() string {
+	return c.bug.Id()
+}
+
 func (c *BugCache) HumanId() string {
 	return c.bug.HumanId()
 }

cache/repo_cache.go 🔗

@@ -207,6 +207,10 @@ func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
 		return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
 	}
 
+	if len(matching) == 0 {
+		return nil, bug.ErrBugNotExist
+	}
+
 	return c.ResolveBug(matching[0])
 }
 

commands/select/select.go 🔗

@@ -0,0 +1,125 @@
+package _select
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path"
+
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/git"
+	"github.com/pkg/errors"
+)
+
+const selectFile = "select"
+
+var ErrNoValidId = errors.New("you must provide a bug id")
+
+// ResolveBug first try to resolve a bug using the first argument of the command
+// line. If it fails, it fallback to the select mechanism.
+//
+// Returns:
+// - the bug if any
+// - the new list of command line arguments with the bug prefix removed if it
+//   has been used
+// - an error if the process failed
+func ResolveBug(repo *cache.RepoCache, args []string) (*cache.BugCache, []string, error) {
+	if len(args) > 0 {
+		b, err := repo.ResolveBugPrefix(args[0])
+
+		if err == nil {
+			return b, args[1:], nil
+		}
+
+		if err != bug.ErrBugNotExist {
+			return nil, nil, err
+		}
+	}
+
+	// first arg is not a valid bug prefix
+
+	b, err := selected(repo)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if b != nil {
+		return b, args, nil
+	}
+
+	return nil, nil, ErrNoValidId
+}
+
+// Select will select a bug for future use
+func Select(repo *cache.RepoCache, id string) error {
+	selectPath := selectFilePath(repo.Repository())
+
+	f, err := os.OpenFile(selectPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+	if err != nil {
+		return err
+	}
+
+	_, err = f.WriteString(id)
+	if err != nil {
+		return err
+	}
+
+	return f.Close()
+}
+
+// Clear will clear the selected bug, if any
+func Clear(repo *cache.RepoCache) error {
+	selectPath := selectFilePath(repo.Repository())
+
+	return os.Remove(selectPath)
+}
+
+func selected(repo *cache.RepoCache) (*cache.BugCache, error) {
+	selectPath := selectFilePath(repo.Repository())
+
+	f, err := os.Open(selectPath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, nil
+		} else {
+			return nil, err
+		}
+	}
+
+	buf, err := ioutil.ReadAll(io.LimitReader(f, 100))
+	if err != nil {
+		return nil, err
+	}
+	if len(buf) == 100 {
+		return nil, fmt.Errorf("the select file should be < 100 bytes")
+	}
+
+	h := git.Hash(buf)
+	if !h.IsValid() {
+		err = os.Remove(selectPath)
+		if err != nil {
+			return nil, errors.Wrap(err, "error while removing invalid select file")
+		}
+
+		return nil, fmt.Errorf("select file in invalid, removing it")
+	}
+
+	b, err := repo.ResolveBug(string(h))
+	if err != nil {
+		return nil, err
+	}
+
+	err = f.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	return b, nil
+}
+
+func selectFilePath(repo repository.Repo) string {
+	return path.Join(repo.GetPath(), ".git", "git-bug", selectFile)
+}

commands/select/select_test.go 🔗

@@ -0,0 +1,109 @@
+package _select
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"testing"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/repository"
+)
+
+func TestSelect(t *testing.T) {
+	repo, err := cache.NewRepoCache(createRepo())
+	checkErr(t, err)
+
+	_, _, err = ResolveBug(repo, []string{})
+	if err != ErrNoValidId {
+		t.Fatal("expected no valid id error, got", err)
+	}
+
+	err = Select(repo, "invalid")
+	checkErr(t, err)
+
+	_, _, err = ResolveBug(repo, []string{})
+	if err == nil {
+		t.Fatal("expected invalid bug error")
+	}
+
+	// generate a bunch of bugs
+	for i := 0; i < 10; i++ {
+		_, err := repo.NewBug("title", "message")
+		checkErr(t, err)
+	}
+
+	// two more for testing
+	b1, err := repo.NewBug("title", "message")
+	checkErr(t, err)
+	b2, err := repo.NewBug("title", "message")
+	checkErr(t, err)
+
+	err = Select(repo, b1.Id())
+	checkErr(t, err)
+
+	// normal select without args
+	b3, _, err := ResolveBug(repo, []string{})
+	checkErr(t, err)
+	if b3.Id() != b1.Id() {
+		t.Fatal("incorrect bug returned")
+	}
+
+	// override selection with same id
+	b4, _, err := ResolveBug(repo, []string{b1.Id()})
+	checkErr(t, err)
+	if b4.Id() != b1.Id() {
+		t.Fatal("incorrect bug returned")
+	}
+
+	// override selection with a prefix
+	b5, _, err := ResolveBug(repo, []string{b1.HumanId()})
+	checkErr(t, err)
+	if b5.Id() != b1.Id() {
+		t.Fatal("incorrect bug returned")
+	}
+
+	// args that shouldn't override
+	b6, _, err := ResolveBug(repo, []string{"arg"})
+	checkErr(t, err)
+	if b6.Id() != b1.Id() {
+		t.Fatal("incorrect bug returned")
+	}
+
+	// override with a different id
+	b7, _, err := ResolveBug(repo, []string{b2.Id()})
+	checkErr(t, err)
+	if b7.Id() != b2.Id() {
+		t.Fatal("incorrect bug returned")
+	}
+
+	err = Clear(repo)
+	checkErr(t, err)
+
+	_, _, err = ResolveBug(repo, []string{})
+	if err == nil {
+		t.Fatal("expected invalid bug error")
+	}
+}
+
+func createRepo() *repository.GitRepo {
+	dir, err := ioutil.TempDir("", "")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Println("Creating repo:", dir)
+
+	repo, err := repository.InitGitRepo(dir)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return repo
+}
+
+func checkErr(t testing.TB, err error) {
+	if err != nil {
+		t.Fatal(err)
+	}
+}