termui: implement push/pull

Michael Muré created

Change summary

bug/bug_actions.go      |  12 +++-
cache/cache.go          |  21 +++++++++
commands/push.go        |   7 ++
repository/git.go       |  16 +++---
repository/mock_repo.go |   8 +-
repository/repo.go      |   4 
termui/bug_table.go     | 100 ++++++++++++++++++++++++++++++++++++++++++
termui/error_popup.go   |  72 ------------------------------
termui/msg_popup.go     |  89 ++++++++++++++++++++++++++++++++++++++
termui/show_bug.go      |   6 +-
termui/termui.go        |  14 +++---
11 files changed, 247 insertions(+), 102 deletions(-)

Detailed changes

bug/bug_actions.go 🔗

@@ -13,25 +13,28 @@ const MsgMergeInvalid = "invalid data"
 const MsgMergeUpdated = "updated"
 const MsgMergeNothing = "nothing to do"
 
-func Fetch(repo repository.Repo, remote string) error {
+func Fetch(repo repository.Repo, remote string) (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 {
+func Push(repo repository.Repo, remote string) (string, error) {
 	return repo.PushRefs(remote, bugsRefPattern+"*")
 }
 
 func Pull(repo repository.Repo, out io.Writer, remote string) error {
 	fmt.Fprintf(out, "Fetching remote ...\n")
 
-	if err := Fetch(repo, remote); err != nil {
+	stdout, err := Fetch(repo, remote)
+	if err != nil {
 		return err
 	}
 
-	fmt.Fprintf(out, "\nMerging data ...\n")
+	out.Write([]byte(stdout))
+
+	fmt.Fprintf(out, "Merging data ...\n")
 
 	for merge := range MergeAll(repo, remote) {
 		if merge.Err != nil {
@@ -42,6 +45,7 @@ func Pull(repo repository.Repo, out io.Writer, remote string) error {
 			fmt.Fprintf(out, "%s: %s\n", merge.HumanId, merge.Status)
 		}
 	}
+
 	return nil
 }
 

cache/cache.go 🔗

@@ -2,6 +2,7 @@ package cache
 
 import (
 	"fmt"
+	"io"
 	"strings"
 
 	"github.com/MichaelMure/git-bug/bug"
@@ -28,6 +29,10 @@ type RepoCacher interface {
 	// Mutations
 	NewBug(title string, message string) (BugCacher, error)
 	NewBugWithFiles(title string, message string, files []util.Hash) (BugCacher, error)
+	Fetch(remote string) (string, error)
+	MergeAll(remote string) <-chan bug.MergeResult
+	Pull(remote string, out io.Writer) error
+	Push(remote string) (string, error)
 }
 
 type BugCacher interface {
@@ -188,6 +193,22 @@ func (c *RepoCache) NewBugWithFiles(title string, message string, files []util.H
 	return cached, nil
 }
 
+func (c *RepoCache) Fetch(remote string) (string, error) {
+	return bug.Fetch(c.repo, remote)
+}
+
+func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
+	return bug.MergeAll(c.repo, remote)
+}
+
+func (c *RepoCache) Pull(remote string, out io.Writer) error {
+	return bug.Pull(c.repo, out, remote)
+}
+
+func (c *RepoCache) Push(remote string) (string, error) {
+	return bug.Push(c.repo, remote)
+}
+
 // Bug ------------------------
 
 type BugCache struct {

commands/push.go 🔗

@@ -2,6 +2,8 @@ package commands
 
 import (
 	"errors"
+	"fmt"
+
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/spf13/cobra"
 )
@@ -16,10 +18,13 @@ func runPush(cmd *cobra.Command, args []string) error {
 		remote = args[0]
 	}
 
-	if err := bug.Push(repo, remote); err != nil {
+	stdout, err := bug.Push(repo, remote)
+	if err != nil {
 		return err
 	}
 
+	fmt.Println(stdout)
+
 	return nil
 }
 

repository/git.go 🔗

@@ -151,24 +151,24 @@ func (repo *GitRepo) GetCoreEditor() (string, error) {
 }
 
 // FetchRefs fetch git refs from a remote
-func (repo *GitRepo) FetchRefs(remote, refSpec string) error {
-	err := repo.runGitCommandInline("fetch", remote, refSpec)
+func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
+	stdout, err := repo.runGitCommand("fetch", remote, refSpec)
 
 	if err != nil {
-		return fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
+		return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
 	}
 
-	return err
+	return stdout, err
 }
 
 // PushRefs push git refs to a remote
-func (repo *GitRepo) PushRefs(remote string, refSpec string) error {
-	err := repo.runGitCommandInline("push", remote, refSpec)
+func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
+	stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
 
 	if err != nil {
-		return fmt.Errorf("failed to push to the remote '%s': %v", remote, err)
+		return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, err)
 	}
-	return nil
+	return stdout + stderr, nil
 }
 
 // StoreData will store arbitrary data and return the corresponding hash

repository/mock_repo.go 🔗

@@ -54,12 +54,12 @@ func (r *mockRepoForTest) GetCoreEditor() (string, error) {
 }
 
 // PushRefs push git refs to a remote
-func (r *mockRepoForTest) PushRefs(remote string, refSpec string) error {
-	return nil
+func (r *mockRepoForTest) PushRefs(remote string, refSpec string) (string, error) {
+	return "", nil
 }
 
-func (r *mockRepoForTest) FetchRefs(remote string, refSpec string) error {
-	return nil
+func (r *mockRepoForTest) FetchRefs(remote string, refSpec string) (string, error) {
+	return "", nil
 }
 
 func (r *mockRepoForTest) StoreData(data []byte) (util.Hash, error) {

repository/repo.go 🔗

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

termui/bug_table.go 🔗

@@ -1,7 +1,9 @@
 package termui
 
 import (
+	"bytes"
 	"fmt"
+
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/util"
@@ -14,6 +16,8 @@ const bugTableHeaderView = "bugTableHeaderView"
 const bugTableFooterView = "bugTableFooterView"
 const bugTableInstructionView = "bugTableInstructionView"
 
+const remote = "origin"
+
 type bugTable struct {
 	repo         cache.RepoCacher
 	allIds       []string
@@ -105,7 +109,7 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
 		v.Frame = false
 		v.BgColor = gocui.ColorBlue
 
-		fmt.Fprintf(v, "[q] Quit [←,h] Previous page [↓,j] Down [↑,k] Up [→,l] Next page [enter] Open bug [n] New bug")
+		fmt.Fprintf(v, "[q] Quit [←↓↑→,hjkl] Navigation [enter] Open bug [n] New bug [i] Pull [o] Push")
 	}
 
 	_, err = g.SetCurrentView(bugTableView)
@@ -176,6 +180,18 @@ func (bt *bugTable) keybindings(g *gocui.Gui) error {
 		return err
 	}
 
+	// Pull
+	if err := g.SetKeybinding(bugTableView, 'i', gocui.ModNone,
+		bt.pull); err != nil {
+		return err
+	}
+
+	// Push
+	if err := g.SetKeybinding(bugTableView, 'o', gocui.ModNone,
+		bt.push); err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -383,3 +399,85 @@ func (bt *bugTable) openBug(g *gocui.Gui, v *gocui.View) error {
 	ui.showBug.SetBug(bt.bugs[bt.pageCursor+y])
 	return ui.activateWindow(ui.showBug)
 }
+
+func (bt *bugTable) pull(g *gocui.Gui, v *gocui.View) error {
+	// Note: this is very hacky
+
+	ui.msgPopup.Activate("Pull from remote "+remote, "...")
+
+	go func() {
+		stdout, err := bt.repo.Fetch(remote)
+
+		if err != nil {
+			g.Update(func(gui *gocui.Gui) error {
+				ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
+				return nil
+			})
+		} else {
+			g.Update(func(gui *gocui.Gui) error {
+				ui.msgPopup.UpdateMessage(stdout)
+				return nil
+			})
+		}
+
+		var buffer bytes.Buffer
+		beginLine := ""
+
+		for merge := range bt.repo.MergeAll(remote) {
+			if merge.Status == bug.MsgMergeNothing {
+				continue
+			}
+
+			if merge.Err != nil {
+				g.Update(func(gui *gocui.Gui) error {
+					ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
+					return nil
+				})
+			} else {
+				fmt.Fprintf(&buffer, "%s%s: %s",
+					beginLine, util.Cyan(merge.HumanId), merge.Status,
+				)
+
+				beginLine = "\n"
+
+				g.Update(func(gui *gocui.Gui) error {
+					ui.msgPopup.UpdateMessage(buffer.String())
+					return nil
+				})
+			}
+		}
+
+		fmt.Fprintf(&buffer, "%sdone", beginLine)
+
+		g.Update(func(gui *gocui.Gui) error {
+			ui.msgPopup.UpdateMessage(buffer.String())
+			return nil
+		})
+
+	}()
+
+	return nil
+}
+
+func (bt *bugTable) push(g *gocui.Gui, v *gocui.View) error {
+	ui.msgPopup.Activate("Push to remote "+remote, "...")
+
+	go func() {
+		// TODO: make the remote configurable
+		stdout, err := bt.repo.Push(remote)
+
+		if err != nil {
+			g.Update(func(gui *gocui.Gui) error {
+				ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
+				return nil
+			})
+		} else {
+			g.Update(func(gui *gocui.Gui) error {
+				ui.msgPopup.UpdateMessage(stdout)
+				return nil
+			})
+		}
+	}()
+
+	return nil
+}

termui/error_popup.go 🔗

@@ -1,72 +0,0 @@
-package termui
-
-import (
-	"fmt"
-
-	"github.com/MichaelMure/git-bug/util"
-	"github.com/jroimartin/gocui"
-)
-
-const errorPopupView = "errorPopupView"
-
-type errorPopup struct {
-	message string
-}
-
-func newErrorPopup() *errorPopup {
-	return &errorPopup{
-		message: "",
-	}
-}
-
-func (ep *errorPopup) keybindings(g *gocui.Gui) error {
-	if err := g.SetKeybinding(errorPopupView, gocui.KeySpace, gocui.ModNone, ep.close); err != nil {
-		return err
-	}
-	if err := g.SetKeybinding(errorPopupView, gocui.KeyEnter, gocui.ModNone, ep.close); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func (ep *errorPopup) layout(g *gocui.Gui) error {
-	if ep.message == "" {
-		return nil
-	}
-
-	maxX, maxY := g.Size()
-
-	width := minInt(30, maxX)
-	wrapped, nblines := util.WordWrap(ep.message, width-2)
-	height := minInt(nblines+1, maxY)
-	x0 := (maxX - width) / 2
-	y0 := (maxY - height) / 2
-
-	v, err := g.SetView(errorPopupView, x0, y0, x0+width, y0+height)
-	if err != nil {
-		if err != gocui.ErrUnknownView {
-			return err
-		}
-
-		v.Frame = true
-		v.Title = "Error"
-
-		fmt.Fprintf(v, wrapped)
-	}
-
-	if _, err := g.SetCurrentView(errorPopupView); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func (ep *errorPopup) close(g *gocui.Gui, v *gocui.View) error {
-	ep.message = ""
-	return g.DeleteView(errorPopupView)
-}
-
-func (ep *errorPopup) Activate(message string) {
-	ep.message = message
-}

termui/msg_popup.go 🔗

@@ -0,0 +1,89 @@
+package termui
+
+import (
+	"fmt"
+
+	"github.com/MichaelMure/git-bug/util"
+	"github.com/jroimartin/gocui"
+)
+
+const msgPopupView = "msgPopupView"
+
+const msgPopupErrorTitle = "Error"
+
+type msgPopup struct {
+	active  bool
+	title   string
+	message string
+}
+
+func newMsgPopup() *msgPopup {
+	return &msgPopup{
+		message: "",
+	}
+}
+
+func (ep *msgPopup) keybindings(g *gocui.Gui) error {
+	if err := g.SetKeybinding(msgPopupView, gocui.KeySpace, gocui.ModNone, ep.close); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(msgPopupView, gocui.KeyEnter, gocui.ModNone, ep.close); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(msgPopupView, 'q', gocui.ModNone, ep.close); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (ep *msgPopup) layout(g *gocui.Gui) error {
+	if !ep.active {
+		return nil
+	}
+
+	maxX, maxY := g.Size()
+
+	width := minInt(60, maxX)
+	wrapped, lines := util.TextWrap(ep.message, width-2)
+	height := minInt(lines+1, maxY-3)
+	x0 := (maxX - width) / 2
+	y0 := (maxY - height) / 2
+
+	v, err := g.SetView(msgPopupView, x0, y0, x0+width, y0+height)
+	if err != nil {
+		if err != gocui.ErrUnknownView {
+			return err
+		}
+
+		v.Frame = true
+		v.Autoscroll = true
+	}
+
+	v.Title = ep.title
+
+	v.Clear()
+	fmt.Fprintf(v, wrapped)
+
+	if _, err := g.SetCurrentView(msgPopupView); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (ep *msgPopup) close(g *gocui.Gui, v *gocui.View) error {
+	ep.active = false
+	ep.message = ""
+	return g.DeleteView(msgPopupView)
+}
+
+func (ep *msgPopup) Activate(title string, message string) {
+	ep.active = true
+	ep.title = title
+	ep.message = message
+}
+
+func (ep *msgPopup) UpdateMessage(message string) {
+	ep.message = message
+}

termui/show_bug.go 🔗

@@ -93,7 +93,7 @@ func (sb *showBug) layout(g *gocui.Gui) error {
 	}
 
 	v.Clear()
-	fmt.Fprintf(v, "[q] Save and return [←,h] Left [↓,j] Down [↑,k] Up [→,l] Right ")
+	fmt.Fprintf(v, "[q] Save and return [←↓↑→,hjkl] Navigation ")
 
 	if sb.isOnSide {
 		fmt.Fprint(v, "[a] Add label [r] Remove label")
@@ -591,7 +591,7 @@ func (sb *showBug) addLabel(g *gocui.Gui, v *gocui.View) error {
 
 		err := sb.bug.ChangeLabels(trimLabels(labels), nil)
 		if err != nil {
-			ui.errorPopup.Activate(err.Error())
+			ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
 		}
 
 		g.Update(func(gui *gocui.Gui) error {
@@ -614,7 +614,7 @@ func (sb *showBug) removeLabel(g *gocui.Gui, v *gocui.View) error {
 
 		err := sb.bug.ChangeLabels(nil, trimLabels(labels))
 		if err != nil {
-			ui.errorPopup.Activate(err.Error())
+			ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
 		}
 
 		g.Update(func(gui *gocui.Gui) error {

termui/termui.go 🔗

@@ -19,7 +19,7 @@ type termUI struct {
 
 	bugTable   *bugTable
 	showBug    *showBug
-	errorPopup *errorPopup
+	msgPopup   *msgPopup
 	inputPopup *inputPopup
 }
 
@@ -49,7 +49,7 @@ func Run(repo repository.Repo) error {
 		cache:      c,
 		bugTable:   newBugTable(c),
 		showBug:    newShowBug(c),
-		errorPopup: newErrorPopup(),
+		msgPopup:   newMsgPopup(),
 		inputPopup: newInputPopup(),
 	}
 
@@ -106,7 +106,7 @@ func layout(g *gocui.Gui) error {
 		return err
 	}
 
-	if err := ui.errorPopup.layout(g); err != nil {
+	if err := ui.msgPopup.layout(g); err != nil {
 		return err
 	}
 
@@ -131,7 +131,7 @@ func keybindings(g *gocui.Gui) error {
 		return err
 	}
 
-	if err := ui.errorPopup.keybindings(g); err != nil {
+	if err := ui.msgPopup.keybindings(g); err != nil {
 		return err
 	}
 
@@ -166,7 +166,7 @@ func newBugWithEditor(repo cache.RepoCacher) error {
 	}
 
 	if err == input.ErrEmptyTitle {
-		ui.errorPopup.Activate("Empty title, aborting.")
+		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
 	} else {
 		_, err := repo.NewBug(title, message)
 		if err != nil {
@@ -199,7 +199,7 @@ func addCommentWithEditor(bug cache.BugCacher) error {
 	}
 
 	if err == input.ErrEmptyMessage {
-		ui.errorPopup.Activate("Empty message, aborting.")
+		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
 	} else {
 		err := bug.AddComment(message)
 		if err != nil {
@@ -232,7 +232,7 @@ func setTitleWithEditor(bug cache.BugCacher) error {
 	}
 
 	if err == input.ErrEmptyTitle {
-		ui.errorPopup.Activate("Empty title, aborting.")
+		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
 	} else {
 		err := bug.SetTitle(title)
 		if err != nil {