bug: refactor to limit abstraction leak and to have a more reusable code for the UIs

Michael Muré created

Change summary

bug/bug.go              |  98 ++++++++++++++++++++++++++++-------
bug/remote_actions.go   | 119 +++++++++++++++++++++++++++++++++++++++++++
commands/close.go       |   2 
commands/comment.go     |   2 
commands/label.go       |   2 
commands/ls.go          |  22 ++-----
commands/open.go        |   2 
commands/pull.go        |  55 ++-----------------
commands/push.go        |   3 
commands/show.go        |   2 
commands/webui.go       |   2 
graphql/schema.go       |  19 ++----
repository/git.go       |  12 ++--
repository/mock_repo.go |   4 
repository/repo.go      |   4 
15 files changed, 235 insertions(+), 113 deletions(-)

Detailed changes

bug/bug.go 🔗

@@ -8,16 +8,16 @@ import (
 	"strings"
 )
 
-const BugsRefPattern = "refs/bugs/"
-const BugsRemoteRefPattern = "refs/remote/%s/bugs/"
-const OpsEntryName = "ops"
-const RootEntryName = "root"
+const bugsRefPattern = "refs/bugs/"
+const bugsRemoteRefPattern = "refs/remote/%s/bugs/"
+const opsEntryName = "ops"
+const rootEntryName = "root"
 
-const IdLength = 40
-const HumanIdLength = 7
+const idLength = 40
+const humanIdLength = 7
 
 // Bug hold the data of a bug thread, organized in a way close to
-// how it will be persisted inside Git. This is the datastructure
+// how it will be persisted inside Git. This is the data structure
 // used for merge of two different version.
 type Bug struct {
 	// Id used as unique identifier
@@ -40,8 +40,8 @@ func NewBug() *Bug {
 }
 
 // Find an existing Bug matching a prefix
-func FindBug(repo repository.Repo, prefix string) (*Bug, error) {
-	ids, err := repo.ListRefs(BugsRefPattern)
+func FindLocalBug(repo repository.Repo, prefix string) (*Bug, error) {
+	ids, err := repo.ListRefs(bugsRefPattern)
 
 	if err != nil {
 		return nil, err
@@ -64,11 +64,21 @@ func FindBug(repo repository.Repo, prefix string) (*Bug, error) {
 		return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
 	}
 
-	return ReadBug(repo, BugsRefPattern+matching[0])
+	return ReadLocalBug(repo, matching[0])
+}
+
+func ReadLocalBug(repo repository.Repo, id string) (*Bug, error) {
+	ref := bugsRefPattern + id
+	return readBug(repo, ref)
+}
+
+func ReadRemoteBug(repo repository.Repo, remote string, id string) (*Bug, error) {
+	ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id
+	return readBug(repo, ref)
 }
 
 // Read and parse a Bug from git
-func ReadBug(repo repository.Repo, ref string) (*Bug, error) {
+func readBug(repo repository.Repo, ref string) (*Bug, error) {
 	hashes, err := repo.ListCommits(ref)
 
 	if err != nil {
@@ -78,7 +88,7 @@ func ReadBug(repo repository.Repo, ref string) (*Bug, error) {
 	refSplitted := strings.Split(ref, "/")
 	id := refSplitted[len(refSplitted)-1]
 
-	if len(id) != IdLength {
+	if len(id) != idLength {
 		return nil, fmt.Errorf("Invalid ref length")
 	}
 
@@ -102,12 +112,12 @@ func ReadBug(repo repository.Repo, ref string) (*Bug, error) {
 		rootFound := false
 
 		for _, entry := range entries {
-			if entry.Name == OpsEntryName {
+			if entry.Name == opsEntryName {
 				opsEntry = entry
 				opsFound = true
 				continue
 			}
-			if entry.Name == RootEntryName {
+			if entry.Name == rootEntryName {
 				rootEntry = entry
 				rootFound = true
 			}
@@ -150,6 +160,50 @@ func ReadBug(repo repository.Repo, ref string) (*Bug, error) {
 	return &bug, nil
 }
 
+type StreamedBug struct {
+	Bug *Bug
+	Err error
+}
+
+// Read and parse all local bugs
+func ReadAllLocalBugs(repo repository.Repo) <-chan StreamedBug {
+	return readAllBugs(repo, bugsRefPattern)
+}
+
+// Read and parse all remote bugs for a given remote
+func ReadAllRemoteBugs(repo repository.Repo, remote string) <-chan StreamedBug {
+	refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote)
+	return readAllBugs(repo, refPrefix)
+}
+
+// Read and parse all available bug with a given ref prefix
+func readAllBugs(repo repository.Repo, refPrefix string) <-chan StreamedBug {
+	out := make(chan StreamedBug)
+
+	go func() {
+		defer close(out)
+
+		refs, err := repo.ListRefs(refPrefix)
+		if err != nil {
+			out <- StreamedBug{Err: err}
+			return
+		}
+
+		for _, ref := range refs {
+			b, err := readBug(repo, ref)
+
+			if err != nil {
+				out <- StreamedBug{Err: err}
+				return
+			}
+
+			out <- StreamedBug{Bug: b}
+		}
+	}()
+
+	return out
+}
+
 // IsValid check if the Bug data is valid
 func (bug *Bug) IsValid() bool {
 	// non-empty
@@ -216,9 +270,9 @@ func (bug *Bug) Commit(repo repository.Repo) error {
 	// Write a Git tree referencing this blob
 	hash, err = repo.StoreTree([]repository.TreeEntry{
 		// the last pack of ops
-		{ObjectType: repository.Blob, Hash: hash, Name: OpsEntryName},
+		{ObjectType: repository.Blob, Hash: hash, Name: opsEntryName},
 		// always the first pack of ops (might be the same)
-		{ObjectType: repository.Blob, Hash: bug.rootPack, Name: RootEntryName},
+		{ObjectType: repository.Blob, Hash: bug.rootPack, Name: rootEntryName},
 	})
 
 	if err != nil {
@@ -244,7 +298,7 @@ func (bug *Bug) Commit(repo repository.Repo) error {
 	}
 
 	// Create or update the Git reference for this bug
-	ref := fmt.Sprintf("%s%s", BugsRefPattern, bug.id)
+	ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.id)
 	err = repo.UpdateRef(ref, hash)
 
 	if err != nil {
@@ -326,7 +380,7 @@ func (bug *Bug) Merge(repo repository.Repo, other *Bug) (bool, error) {
 
 	// Update the git ref
 	if updated {
-		err := repo.UpdateRef(BugsRefPattern+bug.id, bug.lastCommit)
+		err := repo.UpdateRef(bugsRefPattern+bug.id, bug.lastCommit)
 		if err != nil {
 			return false, err
 		}
@@ -347,8 +401,12 @@ func (bug *Bug) Id() string {
 
 // Return the Bug identifier truncated for human consumption
 func (bug *Bug) HumanId() string {
-	format := fmt.Sprintf("%%.%ds", HumanIdLength)
-	return fmt.Sprintf(format, bug.Id())
+	return formatHumanId(bug.Id())
+}
+
+func formatHumanId(id string) string {
+	format := fmt.Sprintf("%%.%ds", humanIdLength)
+	return fmt.Sprintf(format, id)
 }
 
 // Lookup for the very first operation of the bug.

bug/remote_actions.go 🔗

@@ -0,0 +1,119 @@
+package bug
+
+import (
+	"fmt"
+	"github.com/MichaelMure/git-bug/repository"
+	"strings"
+)
+
+const MsgNew = "new"
+const MsgInvalid = "invalid data"
+const MsgUpdated = "updated"
+const MsgNothing = "nothing to do"
+
+func Fetch(repo repository.Repo, remote string) error {
+	remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
+	fetchRefSpec := fmt.Sprintf("%s*:%s*", bugsRefPattern, remoteRefSpec)
+
+	return repo.FetchRefs(remote, fetchRefSpec)
+}
+
+func Push(repo repository.Repo, remote string) error {
+	return repo.PushRefs(remote, bugsRefPattern+"*")
+}
+
+type MergeResult struct {
+	Err error
+
+	Id      string
+	HumanId string
+	Status  string
+}
+
+func newMergeError(id string, err error) MergeResult {
+	return MergeResult{
+		Id:      id,
+		HumanId: formatHumanId(id),
+		Status:  err.Error(),
+	}
+}
+
+func newMergeStatus(id string, status string) MergeResult {
+	return MergeResult{
+		Id:      id,
+		HumanId: formatHumanId(id),
+		Status:  status,
+	}
+}
+
+func MergeAll(repo repository.Repo, remote string) <-chan MergeResult {
+	out := make(chan MergeResult)
+
+	go func() {
+		defer close(out)
+
+		remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
+		remoteRefs, err := repo.ListRefs(remoteRefSpec)
+
+		if err != nil {
+			out <- MergeResult{Err: err}
+			return
+		}
+
+		for _, remoteRef := range remoteRefs {
+			refSplitted := strings.Split(remoteRef, "/")
+			id := refSplitted[len(refSplitted)-1]
+
+			remoteBug, err := readBug(repo, remoteRef)
+
+			if err != nil {
+				out <- newMergeError(id, err)
+				continue
+			}
+
+			// Check for error in remote data
+			if !remoteBug.IsValid() {
+				out <- newMergeStatus(id, MsgInvalid)
+				continue
+			}
+
+			localRef := bugsRefPattern + remoteBug.Id()
+			localExist, err := repo.RefExist(localRef)
+
+			// the bug is not local yet, simply create the reference
+			if !localExist {
+				err := repo.CopyRef(remoteRef, localRef)
+
+				if err != nil {
+					out <- newMergeError(id, err)
+					return
+				}
+
+				out <- newMergeStatus(id, MsgNew)
+				continue
+			}
+
+			localBug, err := readBug(repo, localRef)
+
+			if err != nil {
+				out <- newMergeError(id, err)
+				return
+			}
+
+			updated, err := localBug.Merge(repo, remoteBug)
+
+			if err != nil {
+				out <- newMergeError(id, err)
+				return
+			}
+
+			if updated {
+				out <- newMergeStatus(id, MsgUpdated)
+			} else {
+				out <- newMergeStatus(id, MsgNothing)
+			}
+		}
+	}()
+
+	return out
+}

commands/close.go 🔗

@@ -18,7 +18,7 @@ func runCloseBug(cmd *cobra.Command, args []string) error {
 
 	prefix := args[0]
 
-	b, err := bug.FindBug(repo, prefix)
+	b, err := bug.FindLocalBug(repo, prefix)
 	if err != nil {
 		return err
 	}

commands/comment.go 🔗

@@ -44,7 +44,7 @@ func runComment(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	b, err := bug.FindBug(repo, prefix)
+	b, err := bug.FindLocalBug(repo, prefix)
 	if err != nil {
 		return err
 	}

commands/label.go 🔗

@@ -21,7 +21,7 @@ func runLabel(cmd *cobra.Command, args []string) error {
 
 	prefix := args[0]
 
-	b, err := bug.FindBug(repo, prefix)
+	b, err := bug.FindLocalBug(repo, prefix)
 	if err != nil {
 		return err
 	}

commands/ls.go 🔗

@@ -2,28 +2,22 @@ package commands
 
 import (
 	"fmt"
-	b "github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/util"
 	"github.com/spf13/cobra"
 )
 
 func runLsBug(cmd *cobra.Command, args []string) error {
-	ids, err := repo.ListRefs(b.BugsRefPattern)
+	bugs := bug.ReadAllLocalBugs(repo)
 
-	if err != nil {
-		return err
-	}
-
-	for _, ref := range ids {
-		bug, err := b.ReadBug(repo, b.BugsRefPattern+ref)
-
-		if err != nil {
-			return err
+	for b := range bugs {
+		if b.Err != nil {
+			return b.Err
 		}
 
-		snapshot := bug.Compile()
+		snapshot := b.Bug.Compile()
 
-		var author b.Person
+		var author bug.Person
 
 		if len(snapshot.Comments) > 0 {
 			create := snapshot.Comments[0]
@@ -35,7 +29,7 @@ func runLsBug(cmd *cobra.Command, args []string) error {
 		authorFmt := fmt.Sprintf("%-15.15s", author.Name)
 
 		fmt.Printf("%s %s\t%s\t%s\t%s\n",
-			util.Cyan(bug.HumanId()),
+			util.Cyan(b.Bug.HumanId()),
 			util.Yellow(snapshot.Status),
 			titleFmt,
 			util.Magenta(authorFmt),

commands/open.go 🔗

@@ -18,7 +18,7 @@ func runOpenBug(cmd *cobra.Command, args []string) error {
 
 	prefix := args[0]
 
-	b, err := bug.FindBug(repo, prefix)
+	b, err := bug.FindLocalBug(repo, prefix)
 	if err != nil {
 		return err
 	}

commands/pull.go 🔗

@@ -19,62 +19,19 @@ func runPull(cmd *cobra.Command, args []string) error {
 
 	fmt.Printf("Fetching remote ...\n\n")
 
-	if err := repo.FetchRefs(remote, bug.BugsRefPattern+"*", bug.BugsRemoteRefPattern+"*"); err != nil {
+	if err := bug.Fetch(repo, remote); err != nil {
 		return err
 	}
 
 	fmt.Printf("\nMerging data ...\n\n")
 
-	remoteRefSpec := fmt.Sprintf(bug.BugsRemoteRefPattern, remote)
-	remoteRefs, err := repo.ListRefs(remoteRefSpec)
-
-	if err != nil {
-		return err
-	}
-
-	for _, ref := range remoteRefs {
-		remoteRef := fmt.Sprintf(bug.BugsRemoteRefPattern, remote) + ref
-		remoteBug, err := bug.ReadBug(repo, remoteRef)
-
-		if err != nil {
-			return err
-		}
-
-		// Check for error in remote data
-		if !remoteBug.IsValid() {
-			fmt.Printf("%s: %s\n", remoteBug.HumanId(), "invalid remote data")
-			continue
-		}
-
-		localRef := bug.BugsRefPattern + remoteBug.Id()
-		localExist, err := repo.RefExist(localRef)
-
-		// the bug is not local yet, simply create the reference
-		if !localExist {
-			err := repo.CopyRef(remoteRef, localRef)
-
-			if err != nil {
-				return err
-			}
-
-			fmt.Printf("%s: %s\n", remoteBug.HumanId(), "new")
-			continue
-		}
-
-		localBug, err := bug.ReadBug(repo, localRef)
-
-		if err != nil {
-			return err
-		}
-
-		updated, err := localBug.Merge(repo, remoteBug)
-
-		if err != nil {
-			return err
+	for merge := range bug.MergeAll(repo, remote) {
+		if merge.Err != nil {
+			return merge.Err
 		}
 
-		if updated {
-			fmt.Printf("%s: %s\n", remoteBug.HumanId(), "updated")
+		if merge.Status != bug.MsgNothing {
+			fmt.Printf("%s: %s\n", merge.HumanId, merge.Status)
 		}
 	}
 

commands/push.go 🔗

@@ -16,9 +16,10 @@ func runPush(cmd *cobra.Command, args []string) error {
 		remote = args[0]
 	}
 
-	if err := repo.PushRefs(remote, bug.BugsRefPattern+"*"); err != nil {
+	if err := bug.Push(repo, remote); err != nil {
 		return err
 	}
+
 	return nil
 }
 

commands/show.go 🔗

@@ -20,7 +20,7 @@ func runShowBug(cmd *cobra.Command, args []string) error {
 
 	prefix := args[0]
 
-	b, err := bug.FindBug(repo, prefix)
+	b, err := bug.FindLocalBug(repo, prefix)
 	if err != nil {
 		return err
 	}

commands/webui.go 🔗

@@ -19,7 +19,7 @@ func runWebUI(cmd *cobra.Command, args []string) error {
 		var err error
 		port, err = freeport.GetFreePort()
 		if err != nil {
-			log.Fatal(err)
+			return err
 		}
 	}
 

graphql/schema.go 🔗

@@ -18,12 +18,12 @@ func graphqlSchema() (graphql.Schema, error) {
 			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
 				repo := p.Context.Value("repo").(repository.Repo)
 				id, _ := p.Args["id"].(string)
-				bug, err := bug.FindBug(repo, id)
+				b, err := bug.FindLocalBug(repo, id)
 				if err != nil {
 					return nil, err
 				}
 
-				snapshot := bug.Compile()
+				snapshot := b.Compile()
 
 				return snapshot, nil
 			},
@@ -33,22 +33,15 @@ func graphqlSchema() (graphql.Schema, error) {
 			Type: graphql.NewList(bugType),
 			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
 				repo := p.Context.Value("repo").(repository.Repo)
-				ids, err := repo.ListRefs(bug.BugsRefPattern)
-
-				if err != nil {
-					return nil, err
-				}
 
 				var snapshots []bug.Snapshot
 
-				for _, ref := range ids {
-					bug, err := bug.ReadBug(repo, bug.BugsRefPattern+ref)
-
-					if err != nil {
-						return nil, err
+				for sb := range bug.ReadAllLocalBugs(repo) {
+					if sb.Err != nil {
+						return nil, sb.Err
 					}
 
-					snapshots = append(snapshots, bug.Compile())
+					snapshots = append(snapshots, sb.Bug.Compile())
 				}
 
 				return snapshots, nil

repository/git.go 🔗

@@ -19,6 +19,8 @@ type GitRepo struct {
 
 // Run the given git command with the given I/O reader/writers, returning an error if it fails.
 func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
+	fmt.Println("Running git", strings.Join(args, " "))
+
 	cmd := exec.Command("git", args...)
 	cmd.Dir = repo.Path
 	cmd.Stdin = stdin
@@ -99,10 +101,8 @@ func (repo *GitRepo) GetCoreEditor() (string, error) {
 }
 
 // FetchRefs fetch git refs from a remote
-func (repo *GitRepo) FetchRefs(remote, refPattern, remoteRefPattern string) error {
-	remoteRefSpec := fmt.Sprintf(remoteRefPattern, remote)
-	fetchRefSpec := fmt.Sprintf("%s:%s", refPattern, remoteRefSpec)
-	err := repo.runGitCommandInline("fetch", remote, fetchRefSpec)
+func (repo *GitRepo) FetchRefs(remote, refSpec string) error {
+	err := repo.runGitCommandInline("fetch", remote, "\""+refSpec+"\"")
 
 	if err != nil {
 		return fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
@@ -112,8 +112,8 @@ func (repo *GitRepo) FetchRefs(remote, refPattern, remoteRefPattern string) erro
 }
 
 // PushRefs push git refs to a remote
-func (repo *GitRepo) PushRefs(remote string, refPattern string) error {
-	err := repo.runGitCommandInline("push", remote, refPattern)
+func (repo *GitRepo) PushRefs(remote string, refSpec string) error {
+	err := repo.runGitCommandInline("push", remote, refSpec)
 
 	if err != nil {
 		return fmt.Errorf("failed to push to the remote '%s': %v", remote, err)

repository/mock_repo.go 🔗

@@ -48,11 +48,11 @@ func (r *mockRepoForTest) GetCoreEditor() (string, error) {
 }
 
 // PushRefs push git refs to a remote
-func (r *mockRepoForTest) PushRefs(remote string, refPattern string) error {
+func (r *mockRepoForTest) PushRefs(remote string, refSpec string) error {
 	return nil
 }
 
-func (r *mockRepoForTest) FetchRefs(remote string, refPattern string, remoteRefPattern string) error {
+func (r *mockRepoForTest) FetchRefs(remote string, refSpec string) error {
 	return nil
 }
 

repository/repo.go 🔗

@@ -22,10 +22,10 @@ type Repo interface {
 	GetCoreEditor() (string, error)
 
 	// FetchRefs fetch git refs from a remote
-	FetchRefs(remote string, refPattern string, remoteRefPattern string) error
+	FetchRefs(remote string, refSpec string) error
 
 	// PushRefs push git refs to a remote
-	PushRefs(remote string, refPattern string) error
+	PushRefs(remote string, refSpec string) error
 
 	// StoreData will store arbitrary data and return the corresponding hash
 	StoreData(data []byte) (util.Hash, error)