Merge pull request #510 from MichaelMure/repo-rework

Michael MurΓ© created

Repo rework

Change summary

.github/workflows/go.yml             |  41 +++++
.github/workflows/nodejs.yml         |  38 +++++
api/graphql/resolvers/repo.go        |   5 
bridge/github/config_test.go         |   3 
bug/bug_test.go                      |   4 
cache/repo_cache.go                  |  45 +----
cache/repo_cache_bug.go              |  70 +++-----
cache/repo_cache_common.go           |  24 +-
cache/repo_cache_identity.go         |  11 -
cache/repo_cache_test.go             |   8 
commands/env.go                      |   2 
commands/ls.go                       |   5 
commands/select/select.go            |  22 --
doc/gen_docs.go                      |   5 
go.mod                               |   1 
go.sum                               |  19 --
identity/identity_test.go            |   4 
input/input.go                       |  36 ++--
misc/gen_completion.go               |  14 
misc/random_bugs/cmd/main.go         |   2 
repository/git.go                    | 132 ++++++++++++++--
repository/git_testing.go            |   6 
repository/gogit.go                  | 227 +++++++++++++++++++++--------
repository/gogit_config.go           |   6 
repository/gogit_test.go             |   6 
repository/gogit_testing.go          |   6 
repository/keyring.go                |   4 
repository/mock_repo.go              |  84 ++++++++++
repository/repo.go                   |  36 ++++
repository/repo_testing.go           |  28 --
termui/bug_table.go                  |   6 
util/lamport/persisted_clock.go      |  29 ++-
util/lamport/persisted_clock_test.go |   8 
33 files changed, 620 insertions(+), 317 deletions(-)

Detailed changes

.github/workflows/go.yml πŸ”—

@@ -0,0 +1,41 @@
+name: Go build and test
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+  workflow_dispatch:
+
+jobs:
+  build:
+
+    strategy:
+      matrix:
+        go-version: [1.13.x, 1.14.x, 1.15.x]
+        platform: [ubuntu-latest, macos-latest, windows-latest]
+
+    runs-on: ${{ matrix.platform }}
+
+    steps:
+
+      - name: Set up Go ${{ matrix.node-version }}
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ matrix.go-version }}
+
+      - name: Check out code
+        uses: actions/checkout@v2
+
+      - name: Build
+        run: make
+
+      - name: Test
+        run: make test
+        env:
+          GITHUB_TEST_USER: ${{ secrets._GITHUB_TEST_USER }}
+          GITHUB_TOKEN_ADMIN: ${{ secrets._GITHUB_TOKEN_ADMIN }}
+          GITHUB_TOKEN_PRIVATE: ${{ secrets._GITHUB_TOKEN_PRIVATE }}
+          GITHUB_TOKEN_PUBLIC: ${{ secrets._GITHUB_TOKEN_PUBLIC }}
+          GITLAB_API_TOKEN: ${{ secrets.GITLAB_API_TOKEN }}
+          GITLAB_PROJECT_ID: ${{ secrets.GITLAB_PROJECT_ID }}

.github/workflows/nodejs.yml πŸ”—

@@ -0,0 +1,38 @@
+name: Node.js build and test
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+  workflow_dispatch:
+
+defaults:
+  run:
+    working-directory: webui
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [10.x, 12.x, 14.x]
+
+    steps:
+      - name: Setup Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v1
+        with:
+          node-version: ${{ matrix.node-version }}
+
+      - name: Check out code
+        uses: actions/checkout@v2
+
+      - name: Install
+        run: make install
+
+      - name: Build
+        run: make build
+
+      - name: Test
+        run: make test

api/graphql/resolvers/repo.go πŸ”—

@@ -41,7 +41,10 @@ func (repoResolver) AllBugs(_ context.Context, obj *models.Repository, after *st
 	}
 
 	// Simply pass a []string with the ids to the pagination algorithm
-	source := obj.Repo.QueryBugs(q)
+	source, err := obj.Repo.QueryBugs(q)
+	if err != nil {
+		return nil, err
+	}
 
 	// The edger create a custom edge holding just the id
 	edger := func(id entity.Id, offset int) connections.Edge {

bridge/github/config_test.go πŸ”—

@@ -103,6 +103,9 @@ func TestValidateUsername(t *testing.T) {
 	if env := os.Getenv("TRAVIS"); env == "true" {
 		t.Skip("Travis environment: avoiding non authenticated requests")
 	}
+	if _, has := os.LookupEnv("CI"); has {
+		t.Skip("Github action environment: avoiding non authenticated requests")
+	}
 
 	tests := []struct {
 		name  string

bug/bug_test.go πŸ”—

@@ -130,10 +130,10 @@ func TestBugRemove(t *testing.T) {
 	remoteB := repository.CreateGoGitTestRepo(true)
 	defer repository.CleanupTestRepos(repo, remoteA, remoteB)
 
-	err := repo.AddRemote("remoteA", "file://"+remoteA.GetPath())
+	err := repo.AddRemote("remoteA", remoteA.GetLocalRemote())
 	require.NoError(t, err)
 
-	err = repo.AddRemote("remoteB", "file://"+remoteB.GetPath())
+	err = repo.AddRemote("remoteB", remoteB.GetLocalRemote())
 	require.NoError(t, err)
 
 	// generate a bunch of bugs

cache/repo_cache.go πŸ”—

@@ -5,8 +5,6 @@ import (
 	"io"
 	"io/ioutil"
 	"os"
-	"path"
-	"path/filepath"
 	"strconv"
 	"sync"
 
@@ -15,7 +13,6 @@ import (
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/process"
-	"github.com/blevesearch/bleve"
 )
 
 // 1: original format
@@ -57,8 +54,6 @@ type RepoCache struct {
 	muBug sync.RWMutex
 	// excerpt of bugs data for all bugs
 	bugExcerpts map[entity.Id]*BugExcerpt
-	// searchable cache of all bugs
-	searchCache bleve.Index
 	// bug loaded in memory
 	bugs map[entity.Id]*BugCache
 	// loadedBugs is an LRU cache that records which bugs the cache has loaded in
@@ -133,25 +128,18 @@ func (c *RepoCache) write() error {
 }
 
 func (c *RepoCache) lock() error {
-	lockPath := repoLockFilePath(c.repo)
-
 	err := repoIsAvailable(c.repo)
 	if err != nil {
 		return err
 	}
 
-	err = os.MkdirAll(filepath.Dir(lockPath), 0777)
-	if err != nil {
-		return err
-	}
-
-	f, err := os.Create(lockPath)
+	f, err := c.repo.LocalStorage().Create(lockfile)
 	if err != nil {
 		return err
 	}
 
 	pid := fmt.Sprintf("%d", os.Getpid())
-	_, err = f.WriteString(pid)
+	_, err = f.Write([]byte(pid))
 	if err != nil {
 		return err
 	}
@@ -170,16 +158,17 @@ func (c *RepoCache) Close() error {
 	c.bugs = make(map[entity.Id]*BugCache)
 	c.bugExcerpts = nil
 
-	if c.searchCache != nil {
-		c.searchCache.Close()
-		c.searchCache = nil
+	err := c.repo.Close()
+	if err != nil {
+		return err
 	}
 
-	lockPath := repoLockFilePath(c.repo)
-	return os.Remove(lockPath)
+	return c.repo.LocalStorage().Remove(lockfile)
 }
 
 func (c *RepoCache) buildCache() error {
+	// TODO: make that parallel
+
 	c.muBug.Lock()
 	defer c.muBug.Unlock()
 	c.muIdentity.Lock()
@@ -207,9 +196,10 @@ func (c *RepoCache) buildCache() error {
 
 	allBugs := bug.ReadAllLocal(c.repo)
 
-	err := c.createBleveIndex()
+	// wipe the index just to be sure
+	err := c.repo.ClearBleveIndex("bug")
 	if err != nil {
-		return fmt.Errorf("Unable to create search cache. Error: %v", err)
+		return err
 	}
 
 	for b := range allBugs {
@@ -230,17 +220,11 @@ func (c *RepoCache) buildCache() error {
 	return nil
 }
 
-func repoLockFilePath(repo repository.Repo) string {
-	return path.Join(repo.GetPath(), "git-bug", lockfile)
-}
-
 // repoIsAvailable check is the given repository is locked by a Cache.
 // Note: this is a smart function that will cleanup the lock file if the
 // corresponding process is not there anymore.
 // If no error is returned, the repo is free to edit.
-func repoIsAvailable(repo repository.Repo) error {
-	lockPath := repoLockFilePath(repo)
-
+func repoIsAvailable(repo repository.RepoStorage) error {
 	// Todo: this leave way for a racey access to the repo between the test
 	// if the file exist and the actual write. It's probably not a problem in
 	// practice because using a repository will be done from user interaction
@@ -252,8 +236,7 @@ func repoIsAvailable(repo repository.Repo) error {
 	// computer. Should add a configuration that prevent the cleaning of the
 	// lock file
 
-	f, err := os.Open(lockPath)
-
+	f, err := repo.LocalStorage().Open(lockfile)
 	if err != nil && !os.IsNotExist(err) {
 		return err
 	}
@@ -285,7 +268,7 @@ func repoIsAvailable(repo repository.Repo) error {
 			return err
 		}
 
-		err = os.Remove(lockPath)
+		err = repo.LocalStorage().Remove(lockfile)
 		if err != nil {
 			return err
 		}

cache/repo_cache_bug.go πŸ”—

@@ -5,8 +5,6 @@ import (
 	"encoding/gob"
 	"errors"
 	"fmt"
-	"os"
-	"path"
 	"sort"
 	"strings"
 	"time"
@@ -25,14 +23,6 @@ const (
 
 var errBugNotInCache = errors.New("bug missing from cache")
 
-func bugCacheFilePath(repo repository.Repo) string {
-	return path.Join(repo.GetPath(), "git-bug", bugCacheFile)
-}
-
-func searchCacheDirPath(repo repository.Repo) string {
-	return path.Join(repo.GetPath(), "git-bug", searchCacheDir)
-}
-
 // bugUpdated is a callback to trigger when the excerpt of a bug changed,
 // that is each time a bug is updated
 func (c *RepoCache) bugUpdated(id entity.Id) error {
@@ -65,7 +55,7 @@ func (c *RepoCache) loadBugCache() error {
 	c.muBug.Lock()
 	defer c.muBug.Unlock()
 
-	f, err := os.Open(bugCacheFilePath(c.repo))
+	f, err := c.repo.LocalStorage().Open(bugCacheFile)
 	if err != nil {
 		return err
 	}
@@ -88,14 +78,13 @@ func (c *RepoCache) loadBugCache() error {
 
 	c.bugExcerpts = aux.Excerpts
 
-	blevePath := searchCacheDirPath(c.repo)
-	searchCache, err := bleve.Open(blevePath)
+	index, err := c.repo.GetBleveIndex("bug")
 	if err != nil {
-		return fmt.Errorf("Unable to open search cache. Error: %v", err)
+		return err
 	}
-	c.searchCache = searchCache
 
-	count, err := c.searchCache.DocCount()
+	// simple heuristic to detect a mismatch between the index and the bugs
+	count, err := index.DocCount()
 	if err != nil {
 		return err
 	}
@@ -106,26 +95,6 @@ func (c *RepoCache) loadBugCache() error {
 	return nil
 }
 
-func (c *RepoCache) createBleveIndex() error {
-	blevePath := searchCacheDirPath(c.repo)
-
-	_ = os.RemoveAll(blevePath)
-
-	mapping := bleve.NewIndexMapping()
-	mapping.DefaultAnalyzer = "en"
-
-	dir := searchCacheDirPath(c.repo)
-
-	bleveIndex, err := bleve.New(dir, mapping)
-	if err != nil {
-		return err
-	}
-
-	c.searchCache = bleveIndex
-
-	return nil
-}
-
 // write will serialize on disk the bug cache file
 func (c *RepoCache) writeBugCache() error {
 	c.muBug.RLock()
@@ -148,7 +117,7 @@ func (c *RepoCache) writeBugCache() error {
 		return err
 	}
 
-	f, err := os.Create(bugCacheFilePath(c.repo))
+	f, err := c.repo.LocalStorage().Create(bugCacheFile)
 	if err != nil {
 		return err
 	}
@@ -293,12 +262,12 @@ func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, erro
 }
 
 // QueryBugs return the id of all Bug matching the given Query
-func (c *RepoCache) QueryBugs(q *query.Query) []entity.Id {
+func (c *RepoCache) QueryBugs(q *query.Query) ([]entity.Id, error) {
 	c.muBug.RLock()
 	defer c.muBug.RUnlock()
 
 	if q == nil {
-		return c.AllBugsIds()
+		return c.AllBugsIds(), nil
 	}
 
 	matcher := compileMatcher(q.Filters)
@@ -319,9 +288,15 @@ func (c *RepoCache) QueryBugs(q *query.Query) []entity.Id {
 
 		bleveQuery := bleve.NewQueryStringQuery(strings.Join(terms, " "))
 		bleveSearch := bleve.NewSearchRequest(bleveQuery)
-		searchResults, err := c.searchCache.Search(bleveSearch)
+
+		index, err := c.repo.GetBleveIndex("bug")
 		if err != nil {
-			panic("bleve search failed")
+			return nil, err
+		}
+
+		searchResults, err := index.Search(bleveSearch)
+		if err != nil {
+			return nil, err
 		}
 
 		for _, hit := range searchResults.Hits {
@@ -347,7 +322,7 @@ func (c *RepoCache) QueryBugs(q *query.Query) []entity.Id {
 	case query.OrderByEdit:
 		sorter = BugsByEditTime(filtered)
 	default:
-		panic("missing sort type")
+		return nil, errors.New("missing sort type")
 	}
 
 	switch q.OrderDirection {
@@ -356,7 +331,7 @@ func (c *RepoCache) QueryBugs(q *query.Query) []entity.Id {
 	case query.OrderDescending:
 		sorter = sort.Reverse(sorter)
 	default:
-		panic("missing sort direction")
+		return nil, errors.New("missing sort direction")
 	}
 
 	sort.Sort(sorter)
@@ -367,7 +342,7 @@ func (c *RepoCache) QueryBugs(q *query.Query) []entity.Id {
 		result[i] = val.Id
 	}
 
-	return result
+	return result, nil
 }
 
 // AllBugsIds return all known bug ids
@@ -510,7 +485,12 @@ func (c *RepoCache) addBugToSearchIndex(snap *bug.Snapshot) error {
 
 	searchableBug.Text = append(searchableBug.Text, snap.Title)
 
-	err := c.searchCache.Index(snap.Id().String(), searchableBug)
+	index, err := c.repo.GetBleveIndex("bug")
+	if err != nil {
+		return err
+	}
+
+	err = index.Index(snap.Id().String(), searchableBug)
 	if err != nil {
 		return err
 	}

cache/repo_cache_common.go πŸ”—

@@ -3,6 +3,7 @@ package cache
 import (
 	"fmt"
 
+	"github.com/go-git/go-billy/v5"
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/bug"
@@ -30,13 +31,19 @@ func (c *RepoCache) AnyConfig() repository.ConfigRead {
 	return c.repo.AnyConfig()
 }
 
+// Keyring give access to a user-wide storage for secrets
 func (c *RepoCache) Keyring() repository.Keyring {
 	return c.repo.Keyring()
 }
 
-// GetPath returns the path to the repo.
-func (c *RepoCache) GetPath() string {
-	return c.repo.GetPath()
+// GetUserName returns the name the the user has used to configure git
+func (c *RepoCache) GetUserName() (string, error) {
+	return c.repo.GetUserName()
+}
+
+// GetUserEmail returns the email address that the user has used to configure git.
+func (c *RepoCache) GetUserEmail() (string, error) {
+	return c.repo.GetUserEmail()
 }
 
 // GetCoreEditor returns the name of the editor that the user has used to configure git.
@@ -49,14 +56,9 @@ func (c *RepoCache) GetRemotes() (map[string]string, error) {
 	return c.repo.GetRemotes()
 }
 
-// GetUserName returns the name the the user has used to configure git
-func (c *RepoCache) GetUserName() (string, error) {
-	return c.repo.GetUserName()
-}
-
-// GetUserEmail returns the email address that the user has used to configure git.
-func (c *RepoCache) GetUserEmail() (string, error) {
-	return c.repo.GetUserEmail()
+// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug
+func (c *RepoCache) LocalStorage() billy.Filesystem {
+	return c.repo.LocalStorage()
 }
 
 // ReadData will attempt to read arbitrary data from the given hash

cache/repo_cache_identity.go πŸ”—

@@ -4,20 +4,13 @@ import (
 	"bytes"
 	"encoding/gob"
 	"fmt"
-	"os"
-	"path"
 
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/identity"
-	"github.com/MichaelMure/git-bug/repository"
 )
 
 const identityCacheFile = "identity-cache"
 
-func identityCacheFilePath(repo repository.Repo) string {
-	return path.Join(repo.GetPath(), "git-bug", identityCacheFile)
-}
-
 // identityUpdated is a callback to trigger when the excerpt of an identity
 // changed, that is each time an identity is updated
 func (c *RepoCache) identityUpdated(id entity.Id) error {
@@ -41,7 +34,7 @@ func (c *RepoCache) loadIdentityCache() error {
 	c.muIdentity.Lock()
 	defer c.muIdentity.Unlock()
 
-	f, err := os.Open(identityCacheFilePath(c.repo))
+	f, err := c.repo.LocalStorage().Open(identityCacheFile)
 	if err != nil {
 		return err
 	}
@@ -88,7 +81,7 @@ func (c *RepoCache) writeIdentityCache() error {
 		return err
 	}
 
-	f, err := os.Create(identityCacheFilePath(c.repo))
+	f, err := c.repo.LocalStorage().Create(identityCacheFile)
 	if err != nil {
 		return err
 	}

cache/repo_cache_test.go πŸ”—

@@ -73,7 +73,9 @@ func TestCache(t *testing.T) {
 	// Querying
 	q, err := query.Parse("status:open author:descartes sort:edit-asc")
 	require.NoError(t, err)
-	require.Len(t, cache.QueryBugs(q), 2)
+	res, err := cache.QueryBugs(q)
+	require.NoError(t, err)
+	require.Len(t, res, 2)
 
 	// Close
 	require.NoError(t, cache.Close())
@@ -167,10 +169,10 @@ func TestRemove(t *testing.T) {
 	remoteB := repository.CreateGoGitTestRepo(true)
 	defer repository.CleanupTestRepos(repo, remoteA, remoteB)
 
-	err := repo.AddRemote("remoteA", "file://"+remoteA.GetPath())
+	err := repo.AddRemote("remoteA", remoteA.GetLocalRemote())
 	require.NoError(t, err)
 
-	err = repo.AddRemote("remoteB", "file://"+remoteB.GetPath())
+	err = repo.AddRemote("remoteB", remoteB.GetLocalRemote())
 	require.NoError(t, err)
 
 	repoCache, err := NewRepoCache(repo)

commands/env.go πŸ”—

@@ -54,7 +54,7 @@ func loadRepo(env *Env) func(*cobra.Command, []string) error {
 			return fmt.Errorf("unable to get the current working directory: %q", err)
 		}
 
-		env.repo, err = repository.NewGoGitRepo(cwd, []repository.ClockLoader{bug.ClockLoader})
+		env.repo, err = repository.OpenGoGitRepo(cwd, []repository.ClockLoader{bug.ClockLoader})
 		if err == repository.ErrNotARepo {
 			return fmt.Errorf("%s must be run from within a git repo", rootCommandName)
 		}

commands/ls.go πŸ”—

@@ -110,7 +110,10 @@ func runLs(env *Env, opts lsOptions, args []string) error {
 		return err
 	}
 
-	allIds := env.backend.QueryBugs(q)
+	allIds, err := env.backend.QueryBugs(q)
+	if err != nil {
+		return err
+	}
 
 	bugExcerpt := make([]*cache.BugExcerpt, len(allIds))
 	for i, id := range allIds {

commands/select/select.go πŸ”—

@@ -5,14 +5,12 @@ import (
 	"io"
 	"io/ioutil"
 	"os"
-	"path"
 
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/repository"
 )
 
 const selectFile = "select"
@@ -71,14 +69,12 @@ func ResolveBug(repo *cache.RepoCache, args []string) (*cache.BugCache, []string
 
 // Select will select a bug for future use
 func Select(repo *cache.RepoCache, id entity.Id) error {
-	selectPath := selectFilePath(repo)
-
-	f, err := os.OpenFile(selectPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+	f, err := repo.LocalStorage().OpenFile(selectFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
 	if err != nil {
 		return err
 	}
 
-	_, err = f.WriteString(id.String())
+	_, err = f.Write([]byte(id.String()))
 	if err != nil {
 		return err
 	}
@@ -88,15 +84,11 @@ func Select(repo *cache.RepoCache, id entity.Id) error {
 
 // Clear will clear the selected bug, if any
 func Clear(repo *cache.RepoCache) error {
-	selectPath := selectFilePath(repo)
-
-	return os.Remove(selectPath)
+	return repo.LocalStorage().Remove(selectFile)
 }
 
 func selected(repo *cache.RepoCache) (*cache.BugCache, error) {
-	selectPath := selectFilePath(repo)
-
-	f, err := os.Open(selectPath)
+	f, err := repo.LocalStorage().Open(selectFile)
 	if err != nil {
 		if os.IsNotExist(err) {
 			return nil, nil
@@ -115,7 +107,7 @@ func selected(repo *cache.RepoCache) (*cache.BugCache, error) {
 
 	id := entity.Id(buf)
 	if err := id.Validate(); err != nil {
-		err = os.Remove(selectPath)
+		err = repo.LocalStorage().Remove(selectFile)
 		if err != nil {
 			return nil, errors.Wrap(err, "error while removing invalid select file")
 		}
@@ -135,7 +127,3 @@ func selected(repo *cache.RepoCache) (*cache.BugCache, error) {
 
 	return b, nil
 }
-
-func selectFilePath(repo repository.RepoCommon) string {
-	return path.Join(repo.GetPath(), "git-bug", selectFile)
-}

doc/gen_docs.go πŸ”—

@@ -3,7 +3,6 @@ package main
 import (
 	"fmt"
 	"os"
-	"path"
 	"path/filepath"
 	"sync"
 	"time"
@@ -42,7 +41,7 @@ func main() {
 
 func genManPage(root *cobra.Command) error {
 	cwd, _ := os.Getwd()
-	dir := path.Join(cwd, "doc", "man")
+	dir := filepath.Join(cwd, "doc", "man")
 
 	// fixed date to avoid having to commit each month
 	date := time.Date(2019, 4, 1, 12, 0, 0, 0, time.UTC)
@@ -69,7 +68,7 @@ func genManPage(root *cobra.Command) error {
 
 func genMarkdown(root *cobra.Command) error {
 	cwd, _ := os.Getwd()
-	dir := path.Join(cwd, "doc", "md")
+	dir := filepath.Join(cwd, "doc", "md")
 
 	files, err := filepath.Glob(dir + "/*.md")
 	if err != nil {

go.mod πŸ”—

@@ -20,6 +20,7 @@ require (
 	github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
 	github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
 	github.com/fatih/color v1.10.0
+	github.com/go-git/go-billy/v5 v5.0.0
 	github.com/go-git/go-git/v5 v5.2.0
 	github.com/golang/protobuf v1.4.3 // indirect
 	github.com/gorilla/mux v1.8.0

go.sum πŸ”—

@@ -50,15 +50,10 @@ github.com/RoaringBitmap/roaring v0.4.23 h1:gpyfd12QohbqhFO4NVDUdoPOCXsyahYRQhIN
 github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
 github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
 github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
-github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
-github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
 github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 h1:c4mLfegoDw6OhSJXTd2jUEQgZUQuJWtocudb97Qn9EM=
 github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
@@ -66,14 +61,12 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 h1:QvIfX96O11qjX1Zr3hKkG0dI12JBRBGABWffyZ1GI60=
 github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
 github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0=
 github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
@@ -178,12 +171,10 @@ github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+ne
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
 github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
-github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
 github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
 github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 h1:Ujru1hufTHVb++eG6OuNDKMxZnGIvF6o/u8q/8h2+I4=
 github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
@@ -194,9 +185,10 @@ github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
 github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
+github.com/go-git/go-billy v4.2.0+incompatible h1:Z6QtVXd5tjxUtcODLugkJg4WaZnGg13CD8qB9pr+7q0=
+github.com/go-git/go-billy v4.2.0+incompatible/go.mod h1:hedUGslB3n31bx5SW9KMjV/t0CUKnrapjVG9fT7xKX4=
 github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
 github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
 github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
 github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2JgI=
 github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs=
@@ -286,7 +278,6 @@ github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
@@ -317,7 +308,6 @@ github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGzny
 github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
@@ -333,7 +323,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
 github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
-github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM=
 github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -343,7 +332,6 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -382,7 +370,6 @@ github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs
 github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -728,7 +715,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -786,7 +772,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=

identity/identity_test.go πŸ”—

@@ -273,10 +273,10 @@ func TestIdentityRemove(t *testing.T) {
 	remoteB := repository.CreateGoGitTestRepo(true)
 	defer repository.CleanupTestRepos(repo, remoteA, remoteB)
 
-	err := repo.AddRemote("remoteA", "file://"+remoteA.GetPath())
+	err := repo.AddRemote("remoteA", remoteA.GetLocalRemote())
 	require.NoError(t, err)
 
-	err = repo.AddRemote("remoteB", "file://"+remoteB.GetPath())
+	err = repo.AddRemote("remoteB", remoteB.GetLocalRemote())
 	require.NoError(t, err)
 
 	// generate an identity for testing

input/input.go πŸ”—

@@ -11,9 +11,11 @@ import (
 	"io/ioutil"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"strings"
 
 	"github.com/MichaelMure/git-bug/repository"
+	"github.com/go-git/go-billy/v5/util"
 	"github.com/pkg/errors"
 )
 
@@ -35,7 +37,7 @@ const bugTitleCommentTemplate = `%s%s
 // BugCreateEditorInput will open the default editor in the terminal with a
 // template for the user to fill. The file is then processed to extract title
 // and message.
-func BugCreateEditorInput(repo repository.RepoCommon, preTitle string, preMessage string) (string, string, error) {
+func BugCreateEditorInput(repo repository.RepoCommonStorage, preTitle string, preMessage string) (string, string, error) {
 	if preMessage != "" {
 		preMessage = "\n\n" + preMessage
 	}
@@ -101,10 +103,10 @@ const bugCommentTemplate = `%s
 
 // BugCommentEditorInput will open the default editor in the terminal with a
 // template for the user to fill. The file is then processed to extract a comment.
-func BugCommentEditorInput(repo repository.RepoCommon, preMessage string) (string, error) {
+func BugCommentEditorInput(repo repository.RepoCommonStorage, preMessage string) (string, error) {
 	template := fmt.Sprintf(bugCommentTemplate, preMessage)
-	raw, err := launchEditorWithTemplate(repo, messageFilename, template)
 
+	raw, err := launchEditorWithTemplate(repo, messageFilename, template)
 	if err != nil {
 		return "", err
 	}
@@ -152,10 +154,10 @@ const bugTitleTemplate = `%s
 
 // BugTitleEditorInput will open the default editor in the terminal with a
 // template for the user to fill. The file is then processed to extract a title.
-func BugTitleEditorInput(repo repository.RepoCommon, preTitle string) (string, error) {
+func BugTitleEditorInput(repo repository.RepoCommonStorage, preTitle string) (string, error) {
 	template := fmt.Sprintf(bugTitleTemplate, preTitle)
-	raw, err := launchEditorWithTemplate(repo, messageFilename, template)
 
+	raw, err := launchEditorWithTemplate(repo, messageFilename, template)
 	if err != nil {
 		return "", err
 	}
@@ -212,10 +214,10 @@ const queryTemplate = `%s
 
 // QueryEditorInput will open the default editor in the terminal with a
 // template for the user to fill. The file is then processed to extract a query.
-func QueryEditorInput(repo repository.RepoCommon, preQuery string) (string, error) {
+func QueryEditorInput(repo repository.RepoCommonStorage, preQuery string) (string, error) {
 	template := fmt.Sprintf(queryTemplate, preQuery)
-	raw, err := launchEditorWithTemplate(repo, messageFilename, template)
 
+	raw, err := launchEditorWithTemplate(repo, messageFilename, template)
 	if err != nil {
 		return "", err
 	}
@@ -238,11 +240,8 @@ func QueryEditorInput(repo repository.RepoCommon, preQuery string) (string, erro
 
 // launchEditorWithTemplate will launch an editor as launchEditor do, but with a
 // provided template.
-func launchEditorWithTemplate(repo repository.RepoCommon, fileName string, template string) (string, error) {
-	path := fmt.Sprintf("%s/%s", repo.GetPath(), fileName)
-
-	err := ioutil.WriteFile(path, []byte(template), 0644)
-
+func launchEditorWithTemplate(repo repository.RepoCommonStorage, fileName string, template string) (string, error) {
+	err := util.WriteFile(repo.LocalStorage(), fileName, []byte(template), 0644)
 	if err != nil {
 		return "", err
 	}
@@ -254,20 +253,25 @@ func launchEditorWithTemplate(repo repository.RepoCommon, fileName string, templ
 // 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 "[<reporoot>/].git/FILENAME"). This file
+// from the repo (e.g. "FILENAME" will be converted to "[<reporoot>/].git/git-bug/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.RepoCommon, fileName string) (string, error) {
-	path := fmt.Sprintf("%s/%s", repo.GetPath(), fileName)
-	defer os.Remove(path)
+func launchEditor(repo repository.RepoCommonStorage, fileName string) (string, error) {
+	defer repo.LocalStorage().Remove(fileName)
 
 	editor, err := repo.GetCoreEditor()
 	if err != nil {
 		return "", fmt.Errorf("Unable to detect default git editor: %v\n", err)
 	}
 
+	repo.LocalStorage().Root()
+
+	// bypass the interface but that's ok: we need that because we are communicating
+	// the absolute path to an external program
+	path := filepath.Join(repo.LocalStorage().Root(), fileName)
+
 	cmd, err := startInlineCommand(editor, path)
 	if err != nil {
 		// Running the editor directly did not work. This might mean that

misc/gen_completion.go πŸ”—

@@ -3,7 +3,7 @@ package main
 import (
 	"fmt"
 	"os"
-	"path"
+	"path/filepath"
 	"sync"
 
 	"github.com/spf13/cobra"
@@ -41,24 +41,24 @@ func main() {
 
 func genBash(root *cobra.Command) error {
 	cwd, _ := os.Getwd()
-	dir := path.Join(cwd, "misc", "bash_completion", "git-bug")
+	dir := filepath.Join(cwd, "misc", "bash_completion", "git-bug")
 	return root.GenBashCompletionFile(dir)
 }
 
 func genFish(root *cobra.Command) error {
 	cwd, _ := os.Getwd()
-	dir := path.Join(cwd, "misc", "fish_completion", "git-bug")
+	dir := filepath.Join(cwd, "misc", "fish_completion", "git-bug")
 	return root.GenFishCompletionFile(dir, true)
 }
 
 func genPowerShell(root *cobra.Command) error {
 	cwd, _ := os.Getwd()
-	filepath := path.Join(cwd, "misc", "powershell_completion", "git-bug")
-	return root.GenPowerShellCompletionFile(filepath)
+	path := filepath.Join(cwd, "misc", "powershell_completion", "git-bug")
+	return root.GenPowerShellCompletionFile(path)
 }
 
 func genZsh(root *cobra.Command) error {
 	cwd, _ := os.Getwd()
-	filepath := path.Join(cwd, "misc", "zsh_completion", "git-bug")
-	return root.GenZshCompletionFile(filepath)
+	path := filepath.Join(cwd, "misc", "zsh_completion", "git-bug")
+	return root.GenZshCompletionFile(path)
 }

misc/random_bugs/cmd/main.go πŸ”—

@@ -20,7 +20,7 @@ func main() {
 		bug.ClockLoader,
 	}
 
-	repo, err := repository.NewGoGitRepo(dir, loaders)
+	repo, err := repository.OpenGoGitRepo(dir, loaders)
 	if err != nil {
 		panic(err)
 	}

repository/git.go πŸ”—

@@ -4,10 +4,15 @@ package repository
 import (
 	"bytes"
 	"fmt"
-	"path"
+	"os"
+	"path/filepath"
 	"strings"
 	"sync"
 
+	"github.com/blevesearch/bleve"
+	"github.com/go-git/go-billy/v5"
+	"github.com/go-git/go-billy/v5/osfs"
+
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
@@ -26,12 +31,15 @@ type GitRepo struct {
 	clocksMutex sync.Mutex
 	clocks      map[string]lamport.Clock
 
+	indexesMutex sync.Mutex
+	indexes      map[string]bleve.Index
+
 	keyring Keyring
 }
 
-// NewGitRepo determines if the given working directory is inside of a git repository,
+// OpenGitRepo determines if the given working directory is inside of a git repository,
 // and returns the corresponding GitRepo instance if it is.
-func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
+func OpenGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
 	k, err := defaultKeyring()
 	if err != nil {
 		return nil, err
@@ -41,6 +49,7 @@ func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
 		gitCli:  gitCli{path: path},
 		path:    path,
 		clocks:  make(map[string]lamport.Clock),
+		indexes: make(map[string]bleve.Index),
 		keyring: k,
 	}
 
@@ -80,9 +89,10 @@ func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
 // InitGitRepo create a new empty git repo at the given path
 func InitGitRepo(path string) (*GitRepo, error) {
 	repo := &GitRepo{
-		gitCli: gitCli{path: path},
-		path:   path + "/.git",
-		clocks: make(map[string]lamport.Clock),
+		gitCli:  gitCli{path: path},
+		path:    path + "/.git",
+		clocks:  make(map[string]lamport.Clock),
+		indexes: make(map[string]bleve.Index),
 	}
 
 	_, err := repo.runGitCommand("init", path)
@@ -96,9 +106,10 @@ func InitGitRepo(path string) (*GitRepo, error) {
 // InitBareGitRepo create a new --bare empty git repo at the given path
 func InitBareGitRepo(path string) (*GitRepo, error) {
 	repo := &GitRepo{
-		gitCli: gitCli{path: path},
-		path:   path,
-		clocks: make(map[string]lamport.Clock),
+		gitCli:  gitCli{path: path},
+		path:    path,
+		clocks:  make(map[string]lamport.Clock),
+		indexes: make(map[string]bleve.Index),
 	}
 
 	_, err := repo.runGitCommand("init", "--bare", path)
@@ -109,6 +120,17 @@ func InitBareGitRepo(path string) (*GitRepo, error) {
 	return repo, nil
 }
 
+func (repo *GitRepo) Close() error {
+	var firstErr error
+	for _, index := range repo.indexes {
+		err := index.Close()
+		if err != nil && firstErr == nil {
+			firstErr = err
+		}
+	}
+	return firstErr
+}
+
 // LocalConfig give access to the repository scoped configuration
 func (repo *GitRepo) LocalConfig() Config {
 	return newGitConfig(repo.gitCli, false)
@@ -174,6 +196,63 @@ func (repo *GitRepo) GetRemotes() (map[string]string, error) {
 	return remotes, nil
 }
 
+// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug
+func (repo *GitRepo) LocalStorage() billy.Filesystem {
+	return osfs.New(repo.path)
+}
+
+// GetBleveIndex return a bleve.Index that can be used to index documents
+func (repo *GitRepo) GetBleveIndex(name string) (bleve.Index, error) {
+	repo.indexesMutex.Lock()
+	defer repo.indexesMutex.Unlock()
+
+	if index, ok := repo.indexes[name]; ok {
+		return index, nil
+	}
+
+	path := filepath.Join(repo.path, "indexes", name)
+
+	index, err := bleve.Open(path)
+	if err == nil {
+		repo.indexes[name] = index
+		return index, nil
+	}
+
+	err = os.MkdirAll(path, os.ModeDir)
+	if err != nil {
+		return nil, err
+	}
+
+	mapping := bleve.NewIndexMapping()
+	mapping.DefaultAnalyzer = "en"
+
+	index, err = bleve.New(path, mapping)
+	if err != nil {
+		return nil, err
+	}
+
+	repo.indexes[name] = index
+
+	return index, nil
+}
+
+// ClearBleveIndex will wipe the given index
+func (repo *GitRepo) ClearBleveIndex(name string) error {
+	repo.indexesMutex.Lock()
+	defer repo.indexesMutex.Unlock()
+
+	path := filepath.Join(repo.path, "indexes", name)
+
+	err := os.RemoveAll(path)
+	if err != nil {
+		return err
+	}
+
+	delete(repo.indexes, name)
+
+	return nil
+}
+
 // FetchRefs fetch git refs from a remote
 func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
 	stdout, err := repo.runGitCommand("fetch", remote, refSpec)
@@ -358,6 +437,9 @@ func (repo *GitRepo) GetTreeHash(commit Hash) (Hash, error) {
 // GetOrCreateClock return a Lamport clock stored in the Repo.
 // If the clock doesn't exist, it's created.
 func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
+	repo.clocksMutex.Lock()
+	defer repo.clocksMutex.Unlock()
+
 	c, err := repo.getClock(name)
 	if err == nil {
 		return c, nil
@@ -366,12 +448,7 @@ func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
 		return nil, err
 	}
 
-	repo.clocksMutex.Lock()
-	defer repo.clocksMutex.Unlock()
-
-	p := path.Join(repo.path, clockPath, name+"-clock")
-
-	c, err = lamport.NewPersistedClock(p)
+	c, err = lamport.NewPersistedClock(repo.LocalStorage(), name+"-clock")
 	if err != nil {
 		return nil, err
 	}
@@ -381,16 +458,11 @@ func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
 }
 
 func (repo *GitRepo) getClock(name string) (lamport.Clock, error) {
-	repo.clocksMutex.Lock()
-	defer repo.clocksMutex.Unlock()
-
 	if c, ok := repo.clocks[name]; ok {
 		return c, nil
 	}
 
-	p := path.Join(repo.path, clockPath, name+"-clock")
-
-	c, err := lamport.LoadPersistedClock(p)
+	c, err := lamport.LoadPersistedClock(repo.LocalStorage(), name+"-clock")
 	if err == nil {
 		repo.clocks[name] = c
 		return c, nil
@@ -408,3 +480,21 @@ func (repo *GitRepo) AddRemote(name string, url string) error {
 
 	return err
 }
+
+// GetLocalRemote return the URL to use to add this repo as a local remote
+func (repo *GitRepo) GetLocalRemote() string {
+	return repo.path
+}
+
+// EraseFromDisk delete this repository entirely from the disk
+func (repo *GitRepo) EraseFromDisk() error {
+	err := repo.Close()
+	if err != nil {
+		return err
+	}
+
+	path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git"))
+
+	// fmt.Println("Cleaning repo:", path)
+	return os.RemoveAll(path)
+}

repository/git_testing.go πŸ”—

@@ -48,14 +48,12 @@ func SetupReposAndRemote() (repoA, repoB, remote TestedRepo) {
 	repoB = CreateGoGitTestRepo(false)
 	remote = CreateGoGitTestRepo(true)
 
-	remoteAddr := "file://" + remote.GetPath()
-
-	err := repoA.AddRemote("origin", remoteAddr)
+	err := repoA.AddRemote("origin", remote.GetLocalRemote())
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	err = repoB.AddRemote("origin", remoteAddr)
+	err = repoB.AddRemote("origin", remote.GetLocalRemote())
 	if err != nil {
 		log.Fatal(err)
 	}

repository/gogit.go πŸ”—

@@ -6,13 +6,15 @@ import (
 	"io/ioutil"
 	"os"
 	"os/exec"
-	stdpath "path"
 	"path/filepath"
 	"sort"
 	"strings"
 	"sync"
 	"time"
 
+	"github.com/blevesearch/bleve"
+	"github.com/go-git/go-billy/v5"
+	"github.com/go-git/go-billy/v5/osfs"
 	gogit "github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/config"
 	"github.com/go-git/go-git/v5/plumbing"
@@ -23,6 +25,7 @@ import (
 )
 
 var _ ClockedRepo = &GoGitRepo{}
+var _ TestedRepo = &GoGitRepo{}
 
 type GoGitRepo struct {
 	r    *gogit.Repository
@@ -31,10 +34,15 @@ type GoGitRepo struct {
 	clocksMutex sync.Mutex
 	clocks      map[string]lamport.Clock
 
-	keyring Keyring
+	indexesMutex sync.Mutex
+	indexes      map[string]bleve.Index
+
+	keyring      Keyring
+	localStorage billy.Filesystem
 }
 
-func NewGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) {
+// OpenGoGitRepo open an already existing repo at the given path
+func OpenGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) {
 	path, err := detectGitPath(path)
 	if err != nil {
 		return nil, err
@@ -51,10 +59,12 @@ func NewGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) {
 	}
 
 	repo := &GoGitRepo{
-		r:       r,
-		path:    path,
-		clocks:  make(map[string]lamport.Clock),
-		keyring: k,
+		r:            r,
+		path:         path,
+		clocks:       make(map[string]lamport.Clock),
+		indexes:      make(map[string]bleve.Index),
+		keyring:      k,
+		localStorage: osfs.New(filepath.Join(path, "git-bug")),
 	}
 
 	for _, loader := range clockLoaders {
@@ -76,6 +86,50 @@ func NewGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) {
 	return repo, nil
 }
 
+// InitGoGitRepo create a new empty git repo at the given path
+func InitGoGitRepo(path string) (*GoGitRepo, error) {
+	r, err := gogit.PlainInit(path, false)
+	if err != nil {
+		return nil, err
+	}
+
+	k, err := defaultKeyring()
+	if err != nil {
+		return nil, err
+	}
+
+	return &GoGitRepo{
+		r:            r,
+		path:         filepath.Join(path, ".git"),
+		clocks:       make(map[string]lamport.Clock),
+		indexes:      make(map[string]bleve.Index),
+		keyring:      k,
+		localStorage: osfs.New(filepath.Join(path, ".git", "git-bug")),
+	}, nil
+}
+
+// InitBareGoGitRepo create a new --bare empty git repo at the given path
+func InitBareGoGitRepo(path string) (*GoGitRepo, error) {
+	r, err := gogit.PlainInit(path, true)
+	if err != nil {
+		return nil, err
+	}
+
+	k, err := defaultKeyring()
+	if err != nil {
+		return nil, err
+	}
+
+	return &GoGitRepo{
+		r:            r,
+		path:         path,
+		clocks:       make(map[string]lamport.Clock),
+		indexes:      make(map[string]bleve.Index),
+		keyring:      k,
+		localStorage: osfs.New(filepath.Join(path, "git-bug")),
+	}, nil
+}
+
 func detectGitPath(path string) (string, error) {
 	// normalize the path
 	path, err := filepath.Abs(path)
@@ -84,12 +138,12 @@ func detectGitPath(path string) (string, error) {
 	}
 
 	for {
-		fi, err := os.Stat(stdpath.Join(path, ".git"))
+		fi, err := os.Stat(filepath.Join(path, ".git"))
 		if err == nil {
 			if !fi.IsDir() {
 				return "", fmt.Errorf(".git exist but is not a directory")
 			}
-			return stdpath.Join(path, ".git"), nil
+			return filepath.Join(path, ".git"), nil
 		}
 		if !os.IsNotExist(err) {
 			// unknown error
@@ -117,7 +171,7 @@ func isGitDir(path string) (bool, error) {
 	markers := []string{"HEAD", "objects", "refs"}
 
 	for _, marker := range markers {
-		_, err := os.Stat(stdpath.Join(path, marker))
+		_, err := os.Stat(filepath.Join(path, marker))
 		if err == nil {
 			continue
 		}
@@ -132,44 +186,15 @@ func isGitDir(path string) (bool, error) {
 	return true, nil
 }
 
-// InitGoGitRepo create a new empty git repo at the given path
-func InitGoGitRepo(path string) (*GoGitRepo, error) {
-	r, err := gogit.PlainInit(path, false)
-	if err != nil {
-		return nil, err
-	}
-
-	k, err := defaultKeyring()
-	if err != nil {
-		return nil, err
-	}
-
-	return &GoGitRepo{
-		r:       r,
-		path:    path + "/.git",
-		clocks:  make(map[string]lamport.Clock),
-		keyring: k,
-	}, nil
-}
-
-// InitBareGoGitRepo create a new --bare empty git repo at the given path
-func InitBareGoGitRepo(path string) (*GoGitRepo, error) {
-	r, err := gogit.PlainInit(path, true)
-	if err != nil {
-		return nil, err
-	}
-
-	k, err := defaultKeyring()
-	if err != nil {
-		return nil, err
+func (repo *GoGitRepo) Close() error {
+	var firstErr error
+	for _, index := range repo.indexes {
+		err := index.Close()
+		if err != nil && firstErr == nil {
+			firstErr = err
+		}
 	}
-
-	return &GoGitRepo{
-		r:       r,
-		path:    path,
-		clocks:  make(map[string]lamport.Clock),
-		keyring: k,
-	}, nil
+	return firstErr
 }
 
 // LocalConfig give access to the repository scoped configuration
@@ -179,10 +204,7 @@ func (repo *GoGitRepo) LocalConfig() Config {
 
 // GlobalConfig give access to the global scoped configuration
 func (repo *GoGitRepo) GlobalConfig() Config {
-	// TODO: replace that with go-git native implementation once it's supported
-	// see: https://github.com/go-git/go-git
-	// see: https://github.com/src-d/go-git/issues/760
-	return newGoGitGlobalConfig(repo.r)
+	return newGoGitGlobalConfig()
 }
 
 // AnyConfig give access to a merged local/global configuration
@@ -195,11 +217,6 @@ func (repo *GoGitRepo) Keyring() Keyring {
 	return repo.keyring
 }
 
-// GetPath returns the path to the repo.
-func (repo *GoGitRepo) GetPath() string {
-	return repo.path
-}
-
 // GetUserName returns the name the the user has used to configure git
 func (repo *GoGitRepo) GetUserName() (string, error) {
 	return repo.AnyConfig().ReadString("user.name")
@@ -270,6 +287,69 @@ func (repo *GoGitRepo) GetRemotes() (map[string]string, error) {
 	return result, nil
 }
 
+// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug
+func (repo *GoGitRepo) LocalStorage() billy.Filesystem {
+	return repo.localStorage
+}
+
+// GetBleveIndex return a bleve.Index that can be used to index documents
+func (repo *GoGitRepo) GetBleveIndex(name string) (bleve.Index, error) {
+	repo.indexesMutex.Lock()
+	defer repo.indexesMutex.Unlock()
+
+	if index, ok := repo.indexes[name]; ok {
+		return index, nil
+	}
+
+	path := filepath.Join(repo.path, "git-bug", "indexes", name)
+
+	index, err := bleve.Open(path)
+	if err == nil {
+		repo.indexes[name] = index
+		return index, nil
+	}
+
+	err = os.MkdirAll(path, os.ModePerm)
+	if err != nil {
+		return nil, err
+	}
+
+	mapping := bleve.NewIndexMapping()
+	mapping.DefaultAnalyzer = "en"
+
+	index, err = bleve.New(path, mapping)
+	if err != nil {
+		return nil, err
+	}
+
+	repo.indexes[name] = index
+
+	return index, nil
+}
+
+// ClearBleveIndex will wipe the given index
+func (repo *GoGitRepo) ClearBleveIndex(name string) error {
+	repo.indexesMutex.Lock()
+	defer repo.indexesMutex.Unlock()
+
+	path := filepath.Join(repo.path, "indexes", name)
+
+	err := os.RemoveAll(path)
+	if err != nil {
+		return err
+	}
+
+	if index, ok := repo.indexes[name]; ok {
+		err = index.Close()
+		if err != nil {
+			return err
+		}
+		delete(repo.indexes, name)
+	}
+
+	return nil
+}
+
 // FetchRefs fetch git refs from a remote
 func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) {
 	buf := bytes.NewBuffer(nil)
@@ -600,6 +680,9 @@ func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
 // GetOrCreateClock return a Lamport clock stored in the Repo.
 // If the clock doesn't exist, it's created.
 func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
+	repo.clocksMutex.Lock()
+	defer repo.clocksMutex.Unlock()
+
 	c, err := repo.getClock(name)
 	if err == nil {
 		return c, nil
@@ -608,12 +691,7 @@ func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
 		return nil, err
 	}
 
-	repo.clocksMutex.Lock()
-	defer repo.clocksMutex.Unlock()
-
-	p := stdpath.Join(repo.path, clockPath, name+"-clock")
-
-	c, err = lamport.NewPersistedClock(p)
+	c, err = lamport.NewPersistedClock(repo.localStorage, name+"-clock")
 	if err != nil {
 		return nil, err
 	}
@@ -623,16 +701,11 @@ func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
 }
 
 func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
-	repo.clocksMutex.Lock()
-	defer repo.clocksMutex.Unlock()
-
 	if c, ok := repo.clocks[name]; ok {
 		return c, nil
 	}
 
-	p := stdpath.Join(repo.path, clockPath, name+"-clock")
-
-	c, err := lamport.LoadPersistedClock(p)
+	c, err := lamport.LoadPersistedClock(repo.localStorage, name+"-clock")
 	if err == nil {
 		repo.clocks[name] = c
 		return c, nil
@@ -653,3 +726,21 @@ func (repo *GoGitRepo) AddRemote(name string, url string) error {
 
 	return err
 }
+
+// GetLocalRemote return the URL to use to add this repo as a local remote
+func (repo *GoGitRepo) GetLocalRemote() string {
+	return repo.path
+}
+
+// EraseFromDisk delete this repository entirely from the disk
+func (repo *GoGitRepo) EraseFromDisk() error {
+	err := repo.Close()
+	if err != nil {
+		return err
+	}
+
+	path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git"))
+
+	// fmt.Println("Cleaning repo:", path)
+	return os.RemoveAll(path)
+}

repository/gogit_config.go πŸ”—

@@ -24,7 +24,11 @@ func newGoGitLocalConfig(repo *gogit.Repository) *goGitConfig {
 	}
 }
 
-func newGoGitGlobalConfig(repo *gogit.Repository) *goGitConfig {
+func newGoGitGlobalConfig() *goGitConfig {
+	// TODO: replace that with go-git native implementation once it's supported
+	// see: https://github.com/go-git/go-git
+	// see: https://github.com/src-d/go-git/issues/760
+
 	return &goGitConfig{
 		ConfigRead: &goGitConfigReader{getConfig: func() (*config.Config, error) {
 			return config.LoadConfig(config.GlobalScope)

repository/gogit_test.go πŸ”—

@@ -19,7 +19,7 @@ func TestNewGoGitRepo(t *testing.T) {
 
 	_, err = InitGoGitRepo(plainRoot)
 	require.NoError(t, err)
-	plainGitDir := path.Join(plainRoot, ".git")
+	plainGitDir := filepath.Join(plainRoot, ".git")
 
 	// Bare
 	bareRoot, err := ioutil.TempDir("", "")
@@ -52,13 +52,13 @@ func TestNewGoGitRepo(t *testing.T) {
 	}
 
 	for i, tc := range tests {
-		r, err := NewGoGitRepo(tc.inPath, nil)
+		r, err := OpenGoGitRepo(tc.inPath, nil)
 
 		if tc.err {
 			require.Error(t, err, i)
 		} else {
 			require.NoError(t, err, i)
-			assert.Equal(t, filepath.ToSlash(tc.outPath), filepath.ToSlash(r.GetPath()), i)
+			assert.Equal(t, filepath.ToSlash(tc.outPath), filepath.ToSlash(r.path), i)
 		}
 	}
 }

repository/gogit_testing.go πŸ”—

@@ -42,14 +42,12 @@ func SetupGoGitReposAndRemote() (repoA, repoB, remote TestedRepo) {
 	repoB = CreateGoGitTestRepo(false)
 	remote = CreateGoGitTestRepo(true)
 
-	remoteAddr := "file://" + remote.GetPath()
-
-	err := repoA.AddRemote("origin", remoteAddr)
+	err := repoA.AddRemote("origin", remote.GetLocalRemote())
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	err = repoB.AddRemote("origin", remoteAddr)
+	err = repoB.AddRemote("origin", remote.GetLocalRemote())
 	if err != nil {
 		log.Fatal(err)
 	}

repository/keyring.go πŸ”—

@@ -2,7 +2,7 @@ package repository
 
 import (
 	"os"
-	"path"
+	"path/filepath"
 
 	"github.com/99designs/keyring"
 )
@@ -38,7 +38,7 @@ func defaultKeyring() (Keyring, error) {
 		ServiceName: "git-bug",
 
 		// Fallback encrypted file
-		FileDir: path.Join(ucd, "git-bug", "keyring"),
+		FileDir: filepath.Join(ucd, "git-bug", "keyring"),
 		// As we write the file in the user's config directory, this file should already be protected by the OS against
 		// other user's access. We actually don't terribly need to protect it further and a password prompt across all
 		// UI's would be a pain. Therefore we use here a constant password so the file will be unreadable by generic file

repository/mock_repo.go πŸ”—

@@ -4,8 +4,12 @@ import (
 	"crypto/sha1"
 	"fmt"
 	"strings"
+	"sync"
 
 	"github.com/99designs/keyring"
+	"github.com/blevesearch/bleve"
+	"github.com/go-git/go-billy/v5"
+	"github.com/go-git/go-billy/v5/memfs"
 
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
@@ -18,15 +22,21 @@ type mockRepoForTest struct {
 	*mockRepoConfig
 	*mockRepoKeyring
 	*mockRepoCommon
+	*mockRepoStorage
+	*mockRepoBleve
 	*mockRepoData
 	*mockRepoClock
 }
 
+func (m *mockRepoForTest) Close() error { return nil }
+
 func NewMockRepoForTest() *mockRepoForTest {
 	return &mockRepoForTest{
 		mockRepoConfig:  NewMockRepoConfig(),
 		mockRepoKeyring: NewMockRepoKeyring(),
 		mockRepoCommon:  NewMockRepoCommon(),
+		mockRepoStorage: NewMockRepoStorage(),
+		mockRepoBleve:   newMockRepoBleve(),
 		mockRepoData:    NewMockRepoData(),
 		mockRepoClock:   NewMockRepoClock(),
 	}
@@ -86,11 +96,6 @@ func NewMockRepoCommon() *mockRepoCommon {
 	return &mockRepoCommon{}
 }
 
-// GetPath returns the path to the repo.
-func (r *mockRepoCommon) GetPath() string {
-	return "~/mockRepo/"
-}
-
 func (r *mockRepoCommon) GetUserName() (string, error) {
 	return "RenΓ© Descartes", nil
 }
@@ -112,6 +117,62 @@ func (r *mockRepoCommon) GetRemotes() (map[string]string, error) {
 	}, nil
 }
 
+var _ RepoStorage = &mockRepoStorage{}
+
+type mockRepoStorage struct {
+	localFs billy.Filesystem
+}
+
+func NewMockRepoStorage() *mockRepoStorage {
+	return &mockRepoStorage{localFs: memfs.New()}
+}
+
+func (m *mockRepoStorage) LocalStorage() billy.Filesystem {
+	return m.localFs
+}
+
+var _ RepoBleve = &mockRepoBleve{}
+
+type mockRepoBleve struct {
+	indexesMutex sync.Mutex
+	indexes      map[string]bleve.Index
+}
+
+func newMockRepoBleve() *mockRepoBleve {
+	return &mockRepoBleve{
+		indexes: make(map[string]bleve.Index),
+	}
+}
+
+func (m *mockRepoBleve) GetBleveIndex(name string) (bleve.Index, error) {
+	m.indexesMutex.Lock()
+	defer m.indexesMutex.Unlock()
+
+	if index, ok := m.indexes[name]; ok {
+		return index, nil
+	}
+
+	mapping := bleve.NewIndexMapping()
+	mapping.DefaultAnalyzer = "en"
+
+	index, err := bleve.NewMemOnly(mapping)
+	if err != nil {
+		return nil, err
+	}
+
+	m.indexes[name] = index
+
+	return index, nil
+}
+
+func (m *mockRepoBleve) ClearBleveIndex(name string) error {
+	m.indexesMutex.Lock()
+	defer m.indexesMutex.Unlock()
+
+	delete(m.indexes, name)
+	return nil
+}
+
 var _ RepoData = &mockRepoData{}
 
 type commit struct {
@@ -314,7 +375,17 @@ func (r *mockRepoData) AddRemote(name string, url string) error {
 	panic("implement me")
 }
 
+func (m mockRepoForTest) GetLocalRemote() string {
+	panic("implement me")
+}
+
+func (m mockRepoForTest) EraseFromDisk() error {
+	// nothing to do
+	return nil
+}
+
 type mockRepoClock struct {
+	mu     sync.Mutex
 	clocks map[string]lamport.Clock
 }
 
@@ -325,6 +396,9 @@ func NewMockRepoClock() *mockRepoClock {
 }
 
 func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
 	if c, ok := r.clocks[name]; ok {
 		return c, nil
 	}

repository/repo.go πŸ”—

@@ -4,6 +4,9 @@ package repository
 import (
 	"errors"
 
+	"github.com/blevesearch/bleve"
+	"github.com/go-git/go-billy/v5"
+
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
@@ -20,6 +23,15 @@ type Repo interface {
 	RepoKeyring
 	RepoCommon
 	RepoData
+	RepoStorage
+	RepoBleve
+
+	Close() error
+}
+
+type RepoCommonStorage interface {
+	RepoCommon
+	RepoStorage
 }
 
 // ClockedRepo is a Repo that also has Lamport clocks
@@ -48,9 +60,6 @@ type RepoKeyring interface {
 
 // RepoCommon represent the common function the we want all the repo to implement
 type RepoCommon 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)
 
@@ -64,6 +73,21 @@ type RepoCommon interface {
 	GetRemotes() (map[string]string, error)
 }
 
+// RepoStorage give access to the filesystem
+type RepoStorage interface {
+	// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug
+	LocalStorage() billy.Filesystem
+}
+
+// RepoBleve give access to Bleve to implement full-text search indexes.
+type RepoBleve interface {
+	// GetBleveIndex return a bleve.Index that can be used to index documents
+	GetBleveIndex(name string) (bleve.Index, error)
+
+	// ClearBleveIndex will wipe the given index
+	ClearBleveIndex(name string) error
+}
+
 // RepoData give access to the git data storage
 type RepoData interface {
 	// FetchRefs fetch git refs from a remote
@@ -145,4 +169,10 @@ type TestedRepo interface {
 type repoTest interface {
 	// AddRemote add a new remote to the repository
 	AddRemote(name string, url string) error
+
+	// GetLocalRemote return the URL to use to add this repo as a local remote
+	GetLocalRemote() string
+
+	// EraseFromDisk delete this repository entirely from the disk
+	EraseFromDisk() error
 }

repository/repo_testing.go πŸ”—

@@ -3,8 +3,6 @@ package repository
 import (
 	"log"
 	"math/rand"
-	"os"
-	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -15,25 +13,13 @@ import (
 func CleanupTestRepos(repos ...Repo) {
 	var firstErr error
 	for _, repo := range repos {
-		path := repo.GetPath()
-		if strings.HasSuffix(path, "/.git") {
-			// for a normal repository (not --bare), we want to remove everything
-			// including the parent directory where files are checked out
-			path = strings.TrimSuffix(path, "/.git")
-
-			// Testing non-bare repo should also check path is
-			// only .git (i.e. ./.git), but doing so, we should
-			// try to remove the current directory and hav some
-			// trouble. In the present case, this case should not
-			// occur.
-			// TODO consider warning or error when path == ".git"
-		}
-		// fmt.Println("Cleaning repo:", path)
-		err := os.RemoveAll(path)
-		if err != nil {
-			log.Println(err)
-			if firstErr == nil {
-				firstErr = err
+		if repo, ok := repo.(TestedRepo); ok {
+			err := repo.EraseFromDisk()
+			if err != nil {
+				log.Println(err)
+				if firstErr == nil {
+					firstErr = err
+				}
 			}
 		}
 	}

termui/bug_table.go πŸ”—

@@ -237,7 +237,11 @@ func (bt *bugTable) disable(g *gocui.Gui) error {
 }
 
 func (bt *bugTable) paginate(max int) error {
-	bt.allIds = bt.repo.QueryBugs(bt.query)
+	var err error
+	bt.allIds, err = bt.repo.QueryBugs(bt.query)
+	if err != nil {
+		return err
+	}
 
 	return bt.doPaginate(max)
 }

util/lamport/persisted_clock.go πŸ”—

@@ -5,30 +5,28 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
-	"path/filepath"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/go-git/go-billy/v5/util"
 )
 
 var ErrClockNotExist = errors.New("clock doesn't exist")
 
 type PersistedClock struct {
 	*MemClock
+	root     billy.Filesystem
 	filePath string
 }
 
 // NewPersistedClock create a new persisted Lamport clock
-func NewPersistedClock(filePath string) (*PersistedClock, error) {
+func NewPersistedClock(root billy.Filesystem, filePath string) (*PersistedClock, error) {
 	clock := &PersistedClock{
 		MemClock: NewMemClock(),
+		root:     root,
 		filePath: filePath,
 	}
 
-	dir := filepath.Dir(filePath)
-	err := os.MkdirAll(dir, 0777)
-	if err != nil {
-		return nil, err
-	}
-
-	err = clock.Write()
+	err := clock.Write()
 	if err != nil {
 		return nil, err
 	}
@@ -37,8 +35,9 @@ func NewPersistedClock(filePath string) (*PersistedClock, error) {
 }
 
 // LoadPersistedClock load a persisted Lamport clock from a file
-func LoadPersistedClock(filePath string) (*PersistedClock, error) {
+func LoadPersistedClock(root billy.Filesystem, filePath string) (*PersistedClock, error) {
 	clock := &PersistedClock{
+		root:     root,
 		filePath: filePath,
 	}
 
@@ -71,13 +70,19 @@ func (pc *PersistedClock) Witness(time Time) error {
 }
 
 func (pc *PersistedClock) read() error {
-	content, err := ioutil.ReadFile(pc.filePath)
+	f, err := pc.root.Open(pc.filePath)
 	if os.IsNotExist(err) {
 		return ErrClockNotExist
 	}
 	if err != nil {
 		return err
 	}
+	defer f.Close()
+
+	content, err := ioutil.ReadAll(f)
+	if err != nil {
+		return err
+	}
 
 	var value uint64
 	n, err := fmt.Sscanf(string(content), "%d", &value)
@@ -96,5 +101,5 @@ func (pc *PersistedClock) read() error {
 
 func (pc *PersistedClock) Write() error {
 	data := []byte(fmt.Sprintf("%d", pc.counter))
-	return ioutil.WriteFile(pc.filePath, data, 0644)
+	return util.WriteFile(pc.root, pc.filePath, data, 0644)
 }

util/lamport/persisted_clock_test.go πŸ”—

@@ -1,18 +1,16 @@
 package lamport
 
 import (
-	"io/ioutil"
-	"path"
 	"testing"
 
+	"github.com/go-git/go-billy/v5/memfs"
 	"github.com/stretchr/testify/require"
 )
 
 func TestPersistedClock(t *testing.T) {
-	dir, err := ioutil.TempDir("", "")
-	require.NoError(t, err)
+	root := memfs.New()
 
-	c, err := NewPersistedClock(path.Join(dir, "test-clock"))
+	c, err := NewPersistedClock(root, "test-clock")
 	require.NoError(t, err)
 
 	testClock(t, c)