Detailed changes
  
  
    
    @@ -15,10 +15,6 @@ type Cacher interface {
 
 	ResolveRepo(ref string) (RepoCacher, error)
 	DefaultRepo() (RepoCacher, error)
-
-	// Shortcut to resolve on the default repo for convenience
-	DefaultResolveBug(id string) (BugCacher, error)
-	DefaultResolveBugPrefix(prefix string) (BugCacher, error)
 }
 
 type RepoCacher interface {
@@ -37,13 +33,13 @@ type BugCacher interface {
 	ClearSnapshot()
 
 	// Mutations
-	AddComment(message string) (BugCacher, error)
-	ChangeLabels(added []string, removed []string) (BugCacher, error)
-	Open() (BugCacher, error)
-	Close() (BugCacher, error)
-	SetTitle(title string) (BugCacher, error)
+	AddComment(message string) error
+	ChangeLabels(added []string, removed []string) error
+	Open() error
+	Close() error
+	SetTitle(title string) error
 
-	Commit() (BugCacher, error)
+	Commit() error
 }
 
 // Cacher ------------------------
@@ -86,26 +82,6 @@ func (c *RootCache) ResolveRepo(ref string) (RepoCacher, error) {
 	return r, nil
 }
 
-func (c *RootCache) DefaultResolveBug(id string) (BugCacher, error) {
-	repo, err := c.DefaultRepo()
-
-	if err != nil {
-		return nil, err
-	}
-
-	return repo.ResolveBug(id)
-}
-
-func (c *RootCache) DefaultResolveBugPrefix(prefix string) (BugCacher, error) {
-	repo, err := c.DefaultRepo()
-
-	if err != nil {
-		return nil, err
-	}
-
-	return repo.ResolveBugPrefix(prefix)
-}
-
 // Repo ------------------------
 
 type RepoCache struct {
@@ -231,10 +207,10 @@ func (c *BugCache) ClearSnapshot() {
 	c.snap = nil
 }
 
-func (c *BugCache) AddComment(message string) (BugCacher, error) {
+func (c *BugCache) AddComment(message string) error {
 	author, err := bug.GetUser(c.repo)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	operations.Comment(c.bug, author, message)
@@ -242,30 +218,30 @@ func (c *BugCache) AddComment(message string) (BugCacher, error) {
 	// TODO: perf --> the snapshot could simply be updated with the new op
 	c.ClearSnapshot()
 
-	return c, nil
+	return nil
 }
 
-func (c *BugCache) ChangeLabels(added []string, removed []string) (BugCacher, error) {
+func (c *BugCache) ChangeLabels(added []string, removed []string) error {
 	author, err := bug.GetUser(c.repo)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	err = operations.ChangeLabels(nil, c.bug, author, added, removed)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	// TODO: perf --> the snapshot could simply be updated with the new op
 	c.ClearSnapshot()
 
-	return c, nil
+	return nil
 }
 
-func (c *BugCache) Open() (BugCacher, error) {
+func (c *BugCache) Open() error {
 	author, err := bug.GetUser(c.repo)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	operations.Open(c.bug, author)
@@ -273,13 +249,13 @@ func (c *BugCache) Open() (BugCacher, error) {
 	// TODO: perf --> the snapshot could simply be updated with the new op
 	c.ClearSnapshot()
 
-	return c, nil
+	return nil
 }
 
-func (c *BugCache) Close() (BugCacher, error) {
+func (c *BugCache) Close() error {
 	author, err := bug.GetUser(c.repo)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	operations.Close(c.bug, author)
@@ -287,13 +263,13 @@ func (c *BugCache) Close() (BugCacher, error) {
 	// TODO: perf --> the snapshot could simply be updated with the new op
 	c.ClearSnapshot()
 
-	return c, nil
+	return nil
 }
 
-func (c *BugCache) SetTitle(title string) (BugCacher, error) {
+func (c *BugCache) SetTitle(title string) error {
 	author, err := bug.GetUser(c.repo)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	operations.SetTitle(c.bug, author, title)
@@ -301,13 +277,9 @@ func (c *BugCache) SetTitle(title string) (BugCacher, error) {
 	// TODO: perf --> the snapshot could simply be updated with the new op
 	c.ClearSnapshot()
 
-	return c, nil
+	return nil
 }
 
-func (c *BugCache) Commit() (BugCacher, error) {
-	err := c.bug.Commit(c.repo)
-	if err != nil {
-		return nil, err
-	}
-	return c, nil
+func (c *BugCache) Commit() error {
+	return c.bug.Commit(c.repo)
 }
  
  
  
    
    @@ -46,7 +46,7 @@ func (r mutationResolver) Commit(ctx context.Context, repoRef *string, prefix st
 		return bug.Snapshot{}, err
 	}
 
-	b, err = b.Commit()
+	err = b.Commit()
 	if err != nil {
 		return bug.Snapshot{}, err
 	}
@@ -67,7 +67,7 @@ func (r mutationResolver) AddComment(ctx context.Context, repoRef *string, prefi
 		return bug.Snapshot{}, err
 	}
 
-	b, err = b.AddComment(message)
+	err = b.AddComment(message)
 	if err != nil {
 		return bug.Snapshot{}, err
 	}
@@ -88,7 +88,7 @@ func (r mutationResolver) ChangeLabels(ctx context.Context, repoRef *string, pre
 		return bug.Snapshot{}, err
 	}
 
-	b, err = b.ChangeLabels(added, removed)
+	err = b.ChangeLabels(added, removed)
 	if err != nil {
 		return bug.Snapshot{}, err
 	}
@@ -109,7 +109,7 @@ func (r mutationResolver) Open(ctx context.Context, repoRef *string, prefix stri
 		return bug.Snapshot{}, err
 	}
 
-	b, err = b.Open()
+	err = b.Open()
 	if err != nil {
 		return bug.Snapshot{}, err
 	}
@@ -130,7 +130,7 @@ func (r mutationResolver) Close(ctx context.Context, repoRef *string, prefix str
 		return bug.Snapshot{}, err
 	}
 
-	b, err = b.Close()
+	err = b.Close()
 	if err != nil {
 		return bug.Snapshot{}, err
 	}
@@ -151,7 +151,7 @@ func (r mutationResolver) SetTitle(ctx context.Context, repoRef *string, prefix
 		return bug.Snapshot{}, err
 	}
 
-	b, err = b.SetTitle(title)
+	err = b.SetTitle(title)
 	if err != nil {
 		return bug.Snapshot{}, err
 	}
  
  
  
    
    @@ -102,6 +102,41 @@ func BugCommentEditorInput(repo repository.Repo) (string, error) {
 	return message, nil
 }
 
+const bugTitleTemplate = `
+
+# Please enter the new title. Only one line will used.
+# Lines starting with '#' will be ignored, and an empty title aborts the operation.
+`
+
+func BugTitleEditorInput(repo repository.Repo) (string, error) {
+	raw, err := LaunchEditorWithTemplate(repo, messageFilename, bugTitleTemplate)
+
+	if err != nil {
+		return "", err
+	}
+
+	lines := strings.Split(raw, "\n")
+
+	var title string
+	for _, line := range lines {
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		trimmed := strings.TrimSpace(line)
+		if trimmed == "" {
+			continue
+		}
+		title = trimmed
+		break
+	}
+
+	if title == "" {
+		return "", ErrEmptyTitle
+	}
+
+	return title, nil
+}
+
 func LaunchEditorWithTemplate(repo repository.Repo, fileName string, template string) (string, error) {
 	path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
 
  
  
  
    
    @@ -14,16 +14,16 @@ const bugTableFooterView = "bugTableFooterView"
 const bugTableInstructionView = "bugTableInstructionView"
 
 type bugTable struct {
-	cache        cache.RepoCacher
+	repo         cache.RepoCacher
 	allIds       []string
-	bugs         []*bug.Snapshot
+	bugs         []cache.BugCacher
 	pageCursor   int
 	selectCursor int
 }
 
 func newBugTable(cache cache.RepoCacher) *bugTable {
 	return &bugTable{
-		cache:        cache,
+		repo:         cache,
 		pageCursor:   0,
 		selectCursor: 0,
 	}
@@ -165,7 +165,7 @@ func (bt *bugTable) keybindings(g *gocui.Gui) error {
 
 	// New bug
 	if err := g.SetKeybinding(bugTableView, 'n', gocui.ModNone,
-		newBugWithEditor); err != nil {
+		bt.newBug); err != nil {
 		return err
 	}
 
@@ -195,7 +195,7 @@ func (bt *bugTable) disable(g *gocui.Gui) error {
 }
 
 func (bt *bugTable) paginate(max int) error {
-	allIds, err := bt.cache.AllBugIds()
+	allIds, err := bt.repo.AllBugIds()
 	if err != nil {
 		return err
 	}
@@ -213,22 +213,22 @@ func (bt *bugTable) doPaginate(allIds []string, max int) error {
 	nb := minInt(len(allIds)-bt.pageCursor, max)
 
 	if nb < 0 {
-		bt.bugs = []*bug.Snapshot{}
+		bt.bugs = []cache.BugCacher{}
 		return nil
 	}
 
 	// slice the data
 	ids := allIds[bt.pageCursor : bt.pageCursor+nb]
 
-	bt.bugs = make([]*bug.Snapshot, len(ids))
+	bt.bugs = make([]cache.BugCacher, len(ids))
 
 	for i, id := range ids {
-		b, err := bt.cache.ResolveBug(id)
+		b, err := bt.repo.ResolveBug(id)
 		if err != nil {
 			return err
 		}
 
-		bt.bugs[i] = b.Snapshot()
+		bt.bugs[i] = b
 	}
 
 	return nil
@@ -259,16 +259,17 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
 
 	for _, b := range bt.bugs {
 		person := bug.Person{}
-		if len(b.Comments) > 0 {
-			create := b.Comments[0]
+		snap := b.Snapshot()
+		if len(snap.Comments) > 0 {
+			create := snap.Comments[0]
 			person = create.Author
 		}
 
-		id := util.LeftPaddedString(b.HumanId(), columnWidths["id"], 2)
-		status := util.LeftPaddedString(b.Status.String(), columnWidths["status"], 2)
-		title := util.LeftPaddedString(b.Title, columnWidths["title"], 2)
+		id := util.LeftPaddedString(snap.HumanId(), columnWidths["id"], 2)
+		status := util.LeftPaddedString(snap.Status.String(), columnWidths["status"], 2)
+		title := util.LeftPaddedString(snap.Title, columnWidths["title"], 2)
 		author := util.LeftPaddedString(person.Name, columnWidths["author"], 2)
-		summary := util.LeftPaddedString(b.Summary(), columnWidths["summary"], 2)
+		summary := util.LeftPaddedString(snap.Summary(), columnWidths["summary"], 2)
 
 		fmt.Fprintf(v, "%s %s %s %s %s\n", id, status, title, author, summary)
 	}
@@ -330,7 +331,7 @@ func (bt *bugTable) cursorClamp(v *gocui.View) error {
 func (bt *bugTable) nextPage(g *gocui.Gui, v *gocui.View) error {
 	_, max := v.Size()
 
-	allIds, err := bt.cache.AllBugIds()
+	allIds, err := bt.repo.AllBugIds()
 	if err != nil {
 		return err
 	}
@@ -348,7 +349,7 @@ func (bt *bugTable) nextPage(g *gocui.Gui, v *gocui.View) error {
 
 func (bt *bugTable) previousPage(g *gocui.Gui, v *gocui.View) error {
 	_, max := v.Size()
-	allIds, err := bt.cache.AllBugIds()
+	allIds, err := bt.repo.AllBugIds()
 	if err != nil {
 		return err
 	}
@@ -360,6 +361,10 @@ func (bt *bugTable) previousPage(g *gocui.Gui, v *gocui.View) error {
 	return bt.doPaginate(allIds, max)
 }
 
+func (bt *bugTable) newBug(g *gocui.Gui, v *gocui.View) error {
+	return newBugWithEditor(bt.repo)
+}
+
 func (bt *bugTable) openBug(g *gocui.Gui, v *gocui.View) error {
 	_, y := v.Cursor()
 	ui.showBug.bug = bt.bugs[bt.pageCursor+y]
  
  
  
    
    @@ -2,7 +2,6 @@ package termui
 
 import (
 	"fmt"
-	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/bug/operations"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/util"
@@ -17,7 +16,7 @@ const timeLayout = "Jan _2 2006"
 
 type showBug struct {
 	cache cache.RepoCacher
-	bug   *bug.Snapshot
+	bug   cache.BugCacher
 }
 
 func newShowBug(cache cache.RepoCacher) *showBug {
@@ -65,7 +64,7 @@ func (sb *showBug) layout(g *gocui.Gui) error {
 		v.Frame = false
 		v.BgColor = gocui.ColorBlue
 
-		fmt.Fprintf(v, "[q] Return")
+		fmt.Fprintf(v, "[q] Return [c] Add comment [t] Change title")
 	}
 
 	_, err = g.SetCurrentView(showBugView)
@@ -78,6 +77,7 @@ func (sb *showBug) keybindings(g *gocui.Gui) error {
 		return err
 	}
 
+	// Scrolling
 	if err := g.SetKeybinding(showBugView, gocui.KeyPgup, gocui.ModNone,
 		sb.scrollUp); err != nil {
 		return err
@@ -87,6 +87,20 @@ func (sb *showBug) keybindings(g *gocui.Gui) error {
 		return err
 	}
 
+	// Comment
+	if err := g.SetKeybinding(showBugView, 'c', gocui.ModNone,
+		sb.comment); err != nil {
+		return err
+	}
+
+	// Title
+	if err := g.SetKeybinding(showBugView, 't', gocui.ModNone,
+		sb.title); err != nil {
+		return err
+	}
+
+	// Labels
+
 	return nil
 }
 
@@ -105,15 +119,16 @@ func (sb *showBug) disable(g *gocui.Gui) error {
 
 func (sb *showBug) renderMain(v *gocui.View) {
 	maxX, _ := v.Size()
+	snap := sb.bug.Snapshot()
 
-	header1 := fmt.Sprintf("[%s] %s", sb.bug.HumanId(), sb.bug.Title)
+	header1 := fmt.Sprintf("[%s] %s", snap.HumanId(), snap.Title)
 	fmt.Fprintf(v, util.LeftPaddedString(header1, maxX, 2)+"\n\n")
 
 	header2 := fmt.Sprintf("[%s] %s opened this bug on %s",
-		sb.bug.Status, sb.bug.Author.Name, sb.bug.CreatedAt.Format(timeLayout))
+		snap.Status, snap.Author.Name, snap.CreatedAt.Format(timeLayout))
 	fmt.Fprintf(v, util.LeftPaddedString(header2, maxX, 2)+"\n\n")
 
-	for _, op := range sb.bug.Operations {
+	for _, op := range snap.Operations {
 		switch op.(type) {
 
 		case operations.CreateOperation:
@@ -133,11 +148,12 @@ func (sb *showBug) renderMain(v *gocui.View) {
 
 func (sb *showBug) renderSidebar(v *gocui.View) {
 	maxX, _ := v.Size()
+	snap := sb.bug.Snapshot()
 
 	title := util.LeftPaddedString("LABEL", maxX, 2)
 	fmt.Fprintf(v, title+"\n\n")
 
-	for _, label := range sb.bug.Labels {
+	for _, label := range snap.Labels {
 		fmt.Fprintf(v, util.LeftPaddedString(label.String(), maxX, 2))
 		fmt.Fprintln(v)
 	}
@@ -156,3 +172,11 @@ func (sb *showBug) scrollUp(g *gocui.Gui, v *gocui.View) error {
 func (sb *showBug) scrollDown(g *gocui.Gui, v *gocui.View) error {
 	return nil
 }
+
+func (sb *showBug) comment(g *gocui.Gui, v *gocui.View) error {
+	return addCommentWithEditor(sb.bug)
+}
+
+func (sb *showBug) title(g *gocui.Gui, v *gocui.View) error {
+	return setTitleWithEditor(sb.bug)
+}
  
  
  
    
    @@ -136,9 +136,9 @@ func quit(g *gocui.Gui, v *gocui.View) error {
 	return gocui.ErrQuit
 }
 
-func newBugWithEditor(g *gocui.Gui, v *gocui.View) error {
+func newBugWithEditor(repo cache.RepoCacher) error {
 	// This is somewhat hacky.
-	// As there is no way to pause gocui, run the editor, restart gocui,
+	// As there is no way to pause gocui, run the editor and restart gocui,
 	// we have to stop it entirely and start a new one later.
 	//
 	// - an error channel is used to route the returned error of this new
@@ -158,7 +158,73 @@ func newBugWithEditor(g *gocui.Gui, v *gocui.View) error {
 	if err == input.ErrEmptyTitle {
 		ui.errorPopup.activate("Empty title, aborting.")
 	} else {
-		_, err = ui.cache.NewBug(title, message)
+		_, err := repo.NewBug(title, message)
+		if err != nil {
+			return err
+		}
+	}
+
+	initGui()
+
+	return errTerminateMainloop
+}
+
+func addCommentWithEditor(bug cache.BugCacher) error {
+	// This is somewhat hacky.
+	// As there is no way to pause gocui, run the editor and restart gocui,
+	// we have to stop it entirely and start a new one later.
+	//
+	// - an error channel is used to route the returned error of this new
+	// 		instance into the original launch function
+	// - a custom error (errTerminateMainloop) is used to terminate the original
+	//		instance's mainLoop. This error is then filtered.
+
+	ui.g.Close()
+	ui.g = nil
+
+	message, err := input.BugCommentEditorInput(ui.cache.Repository())
+
+	if err != nil && err != input.ErrEmptyMessage {
+		return err
+	}
+
+	if err == input.ErrEmptyMessage {
+		ui.errorPopup.activate("Empty message, aborting.")
+	} else {
+		err := bug.AddComment(message)
+		if err != nil {
+			return err
+		}
+	}
+
+	initGui()
+
+	return errTerminateMainloop
+}
+
+func setTitleWithEditor(bug cache.BugCacher) error {
+	// This is somewhat hacky.
+	// As there is no way to pause gocui, run the editor and restart gocui,
+	// we have to stop it entirely and start a new one later.
+	//
+	// - an error channel is used to route the returned error of this new
+	// 		instance into the original launch function
+	// - a custom error (errTerminateMainloop) is used to terminate the original
+	//		instance's mainLoop. This error is then filtered.
+
+	ui.g.Close()
+	ui.g = nil
+
+	title, err := input.BugTitleEditorInput(ui.cache.Repository())
+
+	if err != nil && err != input.ErrEmptyTitle {
+		return err
+	}
+
+	if err == input.ErrEmptyTitle {
+		ui.errorPopup.activate("Empty title, aborting.")
+	} else {
+		err := bug.SetTitle(title)
 		if err != nil {
 			return err
 		}