Merge pull request #933 from zinderic/feat-cleanup-subcommand

Michael MurΓ© created

add cleanup sub-command that remove local bugs and identities

Change summary

cache/bug_subcache.go                 |  1 
cache/identity_subcache.go            |  3 
cache/repo_cache.go                   |  1 
cache/repo_cache_common.go            | 26 +++++++++
cache/repo_cache_test.go              | 33 +++++++++++++
cache/subcache.go                     | 43 +++++++++++++++++
commands/root.go                      |  1 
commands/wipe.go                      | 58 +++++++++++++++++++++++
doc/man/git-bug-wipe.1                | 27 +++++++++++
doc/man/git-bug.1                     |  2 
doc/md/git-bug.md                     |  1 
doc/md/git-bug_wipe.md                | 18 +++++++
entities/bug/bug_actions.go           |  6 ++
entities/identity/identity.go         | 50 --------------------
entities/identity/identity_actions.go | 71 +++++++++++++++++++++++++++++
entities/identity/identity_test.go    |  2 
entities/identity/identity_user.go    |  4 +
entity/dag/entity_actions.go          | 16 ++++++
entity/dag/entity_actions_test.go     | 31 ++++++++++++
go.mod                                |  3 +
go.sum                                |  4 
repository/gogit.go                   | 11 ++--
repository/index_bleve.go             |  7 ++
repository/localstorage_billy.go      | 16 ++++++
repository/mock_repo.go               | 12 +++-
repository/repo.go                    | 10 +++
repository/repo_testing.go            | 37 ++++++++++++++
27 files changed, 424 insertions(+), 70 deletions(-)

Detailed changes

cache/bug_subcache.go πŸ”—

@@ -38,6 +38,7 @@ func NewRepoCacheBug(repo repository.ClockedRepo,
 		ReadWithResolver:    bug.ReadWithResolver,
 		ReadAllWithResolver: bug.ReadAllWithResolver,
 		Remove:              bug.Remove,
+		RemoveAll:           bug.RemoveAll,
 		MergeAll:            bug.MergeAll,
 	}
 

cache/identity_subcache.go πŸ”—

@@ -39,7 +39,8 @@ func NewRepoCacheIdentity(repo repository.ClockedRepo,
 		ReadAllWithResolver: func(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[*identity.Identity] {
 			return identity.ReadAllLocal(repo)
 		},
-		Remove: identity.RemoveIdentity,
+		Remove:    identity.Remove,
+		RemoveAll: identity.RemoveAll,
 		MergeAll: func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult {
 			return identity.MergeAll(repo, remote)
 		},

cache/repo_cache.go πŸ”—

@@ -32,6 +32,7 @@ type cacheMgmt interface {
 	Load() error
 	Build() error
 	SetCacheSize(size int)
+	RemoveAll() error
 	MergeAll(remote string) <-chan entity.MergeResult
 	GetNamespace() string
 	Close() error

cache/repo_cache_common.go πŸ”—

@@ -3,12 +3,12 @@ package cache
 import (
 	"sync"
 
-	"github.com/go-git/go-billy/v5"
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/entities/identity"
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/multierr"
 )
 
 func (c *RepoCache) Name() string {
@@ -56,7 +56,7 @@ func (c *RepoCache) GetRemotes() (map[string]string, error) {
 }
 
 // LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug
-func (c *RepoCache) LocalStorage() billy.Filesystem {
+func (c *RepoCache) LocalStorage() repository.LocalStorage {
 	return c.repo.LocalStorage()
 }
 
@@ -82,6 +82,15 @@ func (c *RepoCache) Fetch(remote string) (string, error) {
 	return c.repo.FetchRefs(remote, prefixes...)
 }
 
+// RemoveAll deletes all entities from the cache and the disk.
+func (c *RepoCache) RemoveAll() error {
+	var errWait multierr.ErrWaitGroup
+	for _, mgmt := range c.subcaches {
+		errWait.Go(mgmt.RemoveAll)
+	}
+	return errWait.Wait()
+}
+
 // MergeAll will merge all the available remote bug and identities
 func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult {
 	out := make(chan entity.MergeResult)
@@ -163,6 +172,19 @@ func (c *RepoCache) SetUserIdentity(i *IdentityCache) error {
 	return nil
 }
 
+func (c *RepoCache) ClearUserIdentity() error {
+	c.muUserIdentity.Lock()
+	defer c.muUserIdentity.Unlock()
+
+	err := identity.ClearUserIdentity(c.repo)
+	if err != nil {
+		return err
+	}
+
+	c.userIdentityId = ""
+	return nil
+}
+
 func (c *RepoCache) GetUserIdentity() (*IdentityCache, error) {
 	c.muUserIdentity.RLock()
 	if c.userIdentityId != "" {

cache/repo_cache_test.go πŸ”—

@@ -135,6 +135,39 @@ func TestCache(t *testing.T) {
 	_, err = cache.Bugs().ResolvePrefix(bug1.Id().String()[:10])
 	require.NoError(t, err)
 
+	require.Len(t, cache.bugs.cached, 1)
+	require.Len(t, cache.bugs.excerpts, 2)
+	require.Len(t, cache.identities.cached, 1)
+	require.Len(t, cache.identities.excerpts, 2)
+	require.Equal(t, uint64(2), indexCount(t, identity.Namespace))
+	require.Equal(t, uint64(2), indexCount(t, bug.Namespace))
+
+	// Remove + RemoveAll
+	err = cache.Identities().Remove(iden1.Id().String()[:10])
+	require.NoError(t, err)
+	err = cache.Bugs().Remove(bug1.Id().String()[:10])
+	require.NoError(t, err)
+	require.Len(t, cache.bugs.cached, 0)
+	require.Len(t, cache.bugs.excerpts, 1)
+	require.Len(t, cache.identities.cached, 0)
+	require.Len(t, cache.identities.excerpts, 1)
+	require.Equal(t, uint64(1), indexCount(t, identity.Namespace))
+	require.Equal(t, uint64(1), indexCount(t, bug.Namespace))
+
+	_, err = cache.Identities().New("RenΓ© Descartes", "rene@descartes.fr")
+	require.NoError(t, err)
+	_, _, err = cache.Bugs().NewRaw(iden2, time.Now().Unix(), "title", "message", nil, nil)
+	require.NoError(t, err)
+
+	err = cache.RemoveAll()
+	require.NoError(t, err)
+	require.Len(t, cache.bugs.cached, 0)
+	require.Len(t, cache.bugs.excerpts, 0)
+	require.Len(t, cache.identities.cached, 0)
+	require.Len(t, cache.identities.excerpts, 0)
+	require.Equal(t, uint64(0), indexCount(t, identity.Namespace))
+	require.Equal(t, uint64(0), indexCount(t, bug.Namespace))
+
 	// Close
 	require.NoError(t, cache.Close())
 	require.Empty(t, cache.bugs.cached)

cache/subcache.go πŸ”—

@@ -34,6 +34,7 @@ type Actions[EntityT entity.Interface] struct {
 	ReadWithResolver    func(repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (EntityT, error)
 	ReadAllWithResolver func(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[EntityT]
 	Remove              func(repo repository.ClockedRepo, id entity.Id) error
+	RemoveAll           func(repo repository.ClockedRepo) error
 	MergeAll            func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult
 }
 
@@ -399,7 +400,49 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Remove(prefix string) error {
 	delete(sc.excerpts, e.Id())
 	sc.lru.Remove(e.Id())
 
+	index, err := sc.repo.GetIndex(sc.namespace)
+	if err != nil {
+		sc.mu.Unlock()
+		return err
+	}
+
+	err = index.Remove(e.Id().String())
+	sc.mu.Unlock()
+	if err != nil {
+		return err
+	}
+
+	return sc.write()
+}
+
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) RemoveAll() error {
+	sc.mu.Lock()
+
+	err := sc.actions.RemoveAll(sc.repo)
+	if err != nil {
+		sc.mu.Unlock()
+		return err
+	}
+
+	for id, _ := range sc.cached {
+		delete(sc.cached, id)
+		sc.lru.Remove(id)
+	}
+	for id, _ := range sc.excerpts {
+		delete(sc.excerpts, id)
+	}
+
+	index, err := sc.repo.GetIndex(sc.namespace)
+	if err != nil {
+		sc.mu.Unlock()
+		return err
+	}
+
+	err = index.Clear()
 	sc.mu.Unlock()
+	if err != nil {
+		return err
+	}
 
 	return sc.write()
 }

commands/root.go πŸ”—

@@ -81,6 +81,7 @@ the same git remote you are already using to collaborate with other people.
 
 	cmd.AddCommand(newCommandsCommand())
 	cmd.AddCommand(newVersionCommand())
+	cmd.AddCommand(newWipeCommand())
 
 	return cmd
 }

commands/wipe.go πŸ”—

@@ -0,0 +1,58 @@
+package commands
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newWipeCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "wipe",
+		Short:   "Wipe git-bug from the git repository",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runWipe(env)
+		},
+	}
+
+	return cmd
+}
+
+func runWipe(env *execenv.Env) error {
+	env.Out.Println("cleaning entities...")
+	err := env.Backend.RemoveAll()
+	if err != nil {
+		_ = env.Backend.Close()
+		return err
+	}
+
+	env.Out.Println("cleaning git config ...")
+	err = env.Backend.ClearUserIdentity()
+	if err != nil {
+		_ = env.Backend.Close()
+		return err
+	}
+	err = env.Backend.LocalConfig().RemoveAll("git-bug")
+	if err != nil {
+		_ = env.Backend.Close()
+		return err
+	}
+
+	storage := env.Backend.LocalStorage()
+
+	err = env.Backend.Close()
+	if err != nil {
+		return err
+	}
+
+	env.Out.Println("cleaning caches ...")
+	err = storage.RemoveAll(".")
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

doc/man/git-bug-wipe.1 πŸ”—

@@ -0,0 +1,27 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+.PP
+git-bug-wipe - Wipe git-bug from the git repository
+
+
+.SH SYNOPSIS
+.PP
+\fBgit-bug wipe [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+Wipe git-bug from the git repository
+
+
+.SH OPTIONS
+.PP
+\fB-h\fP, \fB--help\fP[=false]
+	help for wipe
+
+
+.SH SEE ALSO
+.PP
+\fBgit-bug(1)\fP

doc/man/git-bug.1 πŸ”—

@@ -29,4 +29,4 @@ the same git remote you are already using to collaborate with other people.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug-bridge(1)\fP, \fBgit-bug-bug(1)\fP, \fBgit-bug-commands(1)\fP, \fBgit-bug-label(1)\fP, \fBgit-bug-pull(1)\fP, \fBgit-bug-push(1)\fP, \fBgit-bug-termui(1)\fP, \fBgit-bug-user(1)\fP, \fBgit-bug-version(1)\fP, \fBgit-bug-webui(1)\fP
+\fBgit-bug-bridge(1)\fP, \fBgit-bug-bug(1)\fP, \fBgit-bug-commands(1)\fP, \fBgit-bug-label(1)\fP, \fBgit-bug-pull(1)\fP, \fBgit-bug-push(1)\fP, \fBgit-bug-termui(1)\fP, \fBgit-bug-user(1)\fP, \fBgit-bug-version(1)\fP, \fBgit-bug-webui(1)\fP, \fBgit-bug-wipe(1)\fP

doc/md/git-bug.md πŸ”—

@@ -34,4 +34,5 @@ git-bug [flags]
 * [git-bug user](git-bug_user.md)	 - List identities
 * [git-bug version](git-bug_version.md)	 - Show git-bug version information
 * [git-bug webui](git-bug_webui.md)	 - Launch the web UI
+* [git-bug wipe](git-bug_wipe.md)	 - Wipe git-bug from the git repository
 

doc/md/git-bug_wipe.md πŸ”—

@@ -0,0 +1,18 @@
+## git-bug wipe
+
+Wipe git-bug from the git repository
+
+```
+git-bug wipe [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for wipe
+```
+
+### SEE ALSO
+
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+

entities/bug/bug_actions.go πŸ”—

@@ -37,3 +37,9 @@ func MergeAll(repo repository.ClockedRepo, resolvers entity.Resolvers, remote st
 func Remove(repo repository.ClockedRepo, id entity.Id) error {
 	return dag.Remove(def, repo, id)
 }
+
+// RemoveAll will remove all local bugs.
+// RemoveAll is idempotent.
+func RemoveAll(repo repository.ClockedRepo) error {
+	return dag.RemoveAll(def, repo)
+}

entities/identity/identity.go πŸ”—

@@ -164,56 +164,6 @@ func ListLocalIds(repo repository.Repo) ([]entity.Id, error) {
 	return entity.RefsToIds(refs), nil
 }
 
-// RemoveIdentity will remove a local identity from its entity.Id
-func RemoveIdentity(repo repository.ClockedRepo, id entity.Id) error {
-	var fullMatches []string
-
-	refs, err := repo.ListRefs(identityRefPattern + id.String())
-	if err != nil {
-		return err
-	}
-	if len(refs) > 1 {
-		return entity.NewErrMultipleMatch(Typename, entity.RefsToIds(refs))
-	}
-	if len(refs) == 1 {
-		// we have the identity locally
-		fullMatches = append(fullMatches, refs[0])
-	}
-
-	remotes, err := repo.GetRemotes()
-	if err != nil {
-		return err
-	}
-
-	for remote := range remotes {
-		remotePrefix := fmt.Sprintf(identityRemoteRefPattern+id.String(), remote)
-		remoteRefs, err := repo.ListRefs(remotePrefix)
-		if err != nil {
-			return err
-		}
-		if len(remoteRefs) > 1 {
-			return entity.NewErrMultipleMatch(Typename, entity.RefsToIds(refs))
-		}
-		if len(remoteRefs) == 1 {
-			// found the identity in a remote
-			fullMatches = append(fullMatches, remoteRefs[0])
-		}
-	}
-
-	if len(fullMatches) == 0 {
-		return entity.NewErrNotFound(Typename)
-	}
-
-	for _, ref := range fullMatches {
-		err = repo.RemoveRef(ref)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
 // ReadAllLocal read and parse all local Identity
 func ReadAllLocal(repo repository.ClockedRepo) <-chan entity.StreamedEntity[*Identity] {
 	return readAll(repo, identityRefPattern)

entities/identity/identity_actions.go πŸ”—

@@ -123,3 +123,74 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes
 
 	return out
 }
+
+// Remove will remove a local identity from its entity.Id.
+// It is left as a responsibility to the caller to make sure that this identities is not
+// linked from another entity, otherwise it would break it.
+// Remove is idempotent.
+func Remove(repo repository.ClockedRepo, id entity.Id) error {
+	var fullMatches []string
+
+	refs, err := repo.ListRefs(identityRefPattern + id.String())
+	if err != nil {
+		return err
+	}
+	if len(refs) > 1 {
+		return entity.NewErrMultipleMatch(Typename, entity.RefsToIds(refs))
+	}
+	if len(refs) == 1 {
+		// we have the identity locally
+		fullMatches = append(fullMatches, refs[0])
+	}
+
+	remotes, err := repo.GetRemotes()
+	if err != nil {
+		return err
+	}
+
+	for remote := range remotes {
+		remotePrefix := fmt.Sprintf(identityRemoteRefPattern+id.String(), remote)
+		remoteRefs, err := repo.ListRefs(remotePrefix)
+		if err != nil {
+			return err
+		}
+		if len(remoteRefs) > 1 {
+			return entity.NewErrMultipleMatch(Typename, entity.RefsToIds(refs))
+		}
+		if len(remoteRefs) == 1 {
+			// found the identity in a remote
+			fullMatches = append(fullMatches, remoteRefs[0])
+		}
+	}
+
+	if len(fullMatches) == 0 {
+		return entity.NewErrNotFound(Typename)
+	}
+
+	for _, ref := range fullMatches {
+		err = repo.RemoveRef(ref)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// RemoveAll will remove all local identities.
+// It is left as a responsibility to the caller to make sure that those identities are not
+// linked from another entity, otherwise it would break them.
+// RemoveAll is idempotent.
+func RemoveAll(repo repository.ClockedRepo) error {
+	localIds, err := ListLocalIds(repo)
+	if err != nil {
+		return err
+	}
+	for _, id := range localIds {
+		err = Remove(repo, id)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}

entities/identity/identity_test.go πŸ”—

@@ -275,7 +275,7 @@ func TestIdentityRemove(t *testing.T) {
 	_, err = Fetch(repo, "remoteB")
 	require.NoError(t, err)
 
-	err = RemoveIdentity(repo, rene.Id())
+	err = Remove(repo, rene.Id())
 	require.NoError(t, err)
 
 	_, err = ReadLocal(repo, rene.Id())

entities/identity/identity_user.go πŸ”—

@@ -15,6 +15,10 @@ func SetUserIdentity(repo repository.RepoConfig, identity *Identity) error {
 	return repo.LocalConfig().StoreString(identityConfigKey, identity.Id().String())
 }
 
+func ClearUserIdentity(repo repository.RepoConfig) error {
+	return repo.LocalConfig().RemoveAll(identityConfigKey)
+}
+
 // GetUserIdentity read the current user identity, set with a git config entry
 func GetUserIdentity(repo repository.Repo) (*Identity, error) {
 	id, err := GetUserIdentityId(repo)

entity/dag/entity_actions.go πŸ”—

@@ -258,3 +258,19 @@ func Remove(def Definition, repo repository.ClockedRepo, id entity.Id) error {
 
 	return nil
 }
+
+// RemoveAll delete all Entity matching the Definition.
+// RemoveAll is idempotent.
+func RemoveAll(def Definition, repo repository.ClockedRepo) error {
+	localIds, err := ListLocalIds(def, repo)
+	if err != nil {
+		return err
+	}
+	for _, id := range localIds {
+		err = Remove(def, repo, id)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}

entity/dag/entity_actions_test.go πŸ”—

@@ -406,3 +406,34 @@ func TestRemove(t *testing.T) {
 	err = Remove(def, repoA, e.Id())
 	require.NoError(t, err)
 }
+
+func TestRemoveAll(t *testing.T) {
+	repoA, _, _, id1, _, resolvers, def := makeTestContextRemote(t)
+
+	var ids []entity.Id
+
+	for i := 0; i < 10; i++ {
+		e := New(def)
+		e.Append(newOp1(id1, "foo"))
+		require.NoError(t, e.Commit(repoA))
+		ids = append(ids, e.Id())
+	}
+
+	_, err := Push(def, repoA, "remote")
+	require.NoError(t, err)
+
+	err = RemoveAll(def, repoA)
+	require.NoError(t, err)
+
+	for _, id := range ids {
+		_, err = Read(def, wrapper, repoA, resolvers, id)
+		require.Error(t, err)
+
+		_, err = readRemote(def, wrapper, repoA, resolvers, "remote", id)
+		require.Error(t, err)
+	}
+
+	// Remove is idempotent
+	err = RemoveAll(def, repoA)
+	require.NoError(t, err)
+}

go.mod πŸ”—

@@ -35,6 +35,9 @@ require (
 	golang.org/x/text v0.6.0
 )
 
+// https://github.com/go-git/go-git/pull/659
+replace github.com/go-git/go-git/v5 => github.com/MichaelMure/go-git/v5 v5.1.1-0.20230114115943-17400561a81c
+
 require (
 	github.com/cloudflare/circl v1.3.1 // indirect
 	github.com/lithammer/dedent v1.1.0 // indirect

go.sum πŸ”—

@@ -10,6 +10,8 @@ github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
 github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic=
 github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
+github.com/MichaelMure/go-git/v5 v5.1.1-0.20230114115943-17400561a81c h1:JFFZbyq4cdKo+QrKNxXemMftPy08aS9gELrPTlPTaZU=
+github.com/MichaelMure/go-git/v5 v5.1.1-0.20230114115943-17400561a81c/go.mod h1:BE5hUJ5yaV2YMxhmaP4l6RBQ08kMxKSPD4BlxtH7OjI=
 github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0=
 github.com/MichaelMure/go-term-text v0.3.1/go.mod h1:QgVjAEDUnRMlzpS6ky5CGblux7ebeiLnuy9dAaFZu8o=
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
@@ -123,8 +125,6 @@ github.com/go-git/go-billy/v5 v5.4.0 h1:Vaw7LaSTRJOUric7pe4vnzBSgyuf2KrLsu2Y4ZpQ
 github.com/go-git/go-billy/v5 v5.4.0/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
 github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ=
 github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
-github.com/go-git/go-git/v5 v5.5.2 h1:v8lgZa5k9ylUw+OR/roJHTxR4QItsNFI5nKtAXFuynw=
-github.com/go-git/go-git/v5 v5.5.2/go.mod h1:BE5hUJ5yaV2YMxhmaP4l6RBQ08kMxKSPD4BlxtH7OjI=
 github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
 github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

repository/gogit.go πŸ”—

@@ -13,7 +13,6 @@ import (
 	"time"
 
 	"github.com/ProtonMail/go-crypto/openpgp"
-	"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"
@@ -48,7 +47,7 @@ type GoGitRepo struct {
 	indexes      map[string]Index
 
 	keyring      Keyring
-	localStorage billy.Filesystem
+	localStorage LocalStorage
 }
 
 // OpenGoGitRepo opens an already existing repo at the given path and
@@ -77,7 +76,7 @@ func OpenGoGitRepo(path, namespace string, clockLoaders []ClockLoader) (*GoGitRe
 		clocks:       make(map[string]lamport.Clock),
 		indexes:      make(map[string]Index),
 		keyring:      k,
-		localStorage: osfs.New(filepath.Join(path, namespace)),
+		localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, namespace))},
 	}
 
 	loaderToRun := make([]ClockLoader, 0, len(clockLoaders))
@@ -131,7 +130,7 @@ func InitGoGitRepo(path, namespace string) (*GoGitRepo, error) {
 		clocks:       make(map[string]lamport.Clock),
 		indexes:      make(map[string]Index),
 		keyring:      k,
-		localStorage: osfs.New(filepath.Join(path, ".git", namespace)),
+		localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, ".git", namespace))},
 	}, nil
 }
 
@@ -156,7 +155,7 @@ func InitBareGoGitRepo(path, namespace string) (*GoGitRepo, error) {
 		clocks:       make(map[string]lamport.Clock),
 		indexes:      make(map[string]Index),
 		keyring:      k,
-		localStorage: osfs.New(filepath.Join(path, namespace)),
+		localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, namespace))},
 	}, nil
 }
 
@@ -320,7 +319,7 @@ func (repo *GoGitRepo) GetRemotes() (map[string]string, error) {
 
 // LocalStorage returns a billy.Filesystem giving access to
 // $RepoPath/.git/$Namespace.
-func (repo *GoGitRepo) LocalStorage() billy.Filesystem {
+func (repo *GoGitRepo) LocalStorage() LocalStorage {
 	return repo.localStorage
 }
 

repository/index_bleve.go πŸ”—

@@ -129,6 +129,13 @@ func (b *bleveIndex) DocCount() (uint64, error) {
 	return b.index.DocCount()
 }
 
+func (b *bleveIndex) Remove(id string) error {
+	b.mu.Lock()
+	defer b.mu.Unlock()
+
+	return b.index.Delete(id)
+}
+
 func (b *bleveIndex) Clear() error {
 	b.mu.Lock()
 	defer b.mu.Unlock()

repository/localstorage_billy.go πŸ”—

@@ -0,0 +1,16 @@
+package repository
+
+import (
+	"github.com/go-git/go-billy/v5"
+	"github.com/go-git/go-billy/v5/util"
+)
+
+var _ LocalStorage = &billyLocalStorage{}
+
+type billyLocalStorage struct {
+	billy.Filesystem
+}
+
+func (b billyLocalStorage) RemoveAll(path string) error {
+	return util.RemoveAll(b.Filesystem, path)
+}

repository/mock_repo.go πŸ”—

@@ -9,7 +9,6 @@ import (
 
 	"github.com/99designs/keyring"
 	"github.com/ProtonMail/go-crypto/openpgp"
-	"github.com/go-git/go-billy/v5"
 	"github.com/go-git/go-billy/v5/memfs"
 
 	"github.com/MichaelMure/git-bug/util/lamport"
@@ -123,14 +122,14 @@ func (r *mockRepoCommon) GetRemotes() (map[string]string, error) {
 var _ RepoStorage = &mockRepoStorage{}
 
 type mockRepoStorage struct {
-	localFs billy.Filesystem
+	localFs LocalStorage
 }
 
 func NewMockRepoStorage() *mockRepoStorage {
-	return &mockRepoStorage{localFs: memfs.New()}
+	return &mockRepoStorage{localFs: billyLocalStorage{Filesystem: memfs.New()}}
 }
 
-func (m *mockRepoStorage) LocalStorage() billy.Filesystem {
+func (m *mockRepoStorage) LocalStorage() LocalStorage {
 	return m.localFs
 }
 
@@ -204,6 +203,11 @@ func (m *mockIndex) DocCount() (uint64, error) {
 	return uint64(len(*m)), nil
 }
 
+func (m *mockIndex) Remove(id string) error {
+	delete(*m, id)
+	return nil
+}
+
 func (m *mockIndex) Clear() error {
 	for k, _ := range *m {
 		delete(*m, k)

repository/repo.go πŸ”—

@@ -76,10 +76,15 @@ type RepoCommon interface {
 	GetRemotes() (map[string]string, error)
 }
 
+type LocalStorage interface {
+	billy.Filesystem
+	RemoveAll(path 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
+	LocalStorage() LocalStorage
 }
 
 // RepoIndex gives access to full-text search indexes
@@ -103,6 +108,9 @@ type Index interface {
 	// DocCount returns the number of document in the index.
 	DocCount() (uint64, error)
 
+	// Remove delete one document in the index.
+	Remove(id string) error
+
 	// Clear empty the index.
 	Clear() error
 

repository/repo_testing.go πŸ”—

@@ -2,6 +2,7 @@ package repository
 
 import (
 	"math/rand"
+	"os"
 	"testing"
 
 	"github.com/ProtonMail/go-crypto/openpgp"
@@ -10,8 +11,6 @@ import (
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
-// TODO: add tests for RepoStorage
-
 type RepoCreator func(t testing.TB, bare bool) TestedRepo
 
 // Test suite for a Repo implementation
@@ -32,6 +31,10 @@ func RepoTest(t *testing.T, creator RepoCreator) {
 				RepoConfigTest(t, repo)
 			})
 
+			t.Run("Storage", func(t *testing.T) {
+				RepoStorageTest(t, repo)
+			})
+
 			t.Run("Index", func(t *testing.T) {
 				RepoIndexTest(t, repo)
 			})
@@ -48,6 +51,36 @@ func RepoConfigTest(t *testing.T, repo RepoConfig) {
 	testConfig(t, repo.LocalConfig())
 }
 
+func RepoStorageTest(t *testing.T, repo RepoStorage) {
+	storage := repo.LocalStorage()
+
+	err := storage.MkdirAll("foo/bar", 0755)
+	require.NoError(t, err)
+
+	f, err := storage.Create("foo/bar/foofoo")
+	require.NoError(t, err)
+
+	_, err = f.Write([]byte("hello"))
+	require.NoError(t, err)
+
+	err = f.Close()
+	require.NoError(t, err)
+
+	// remove all
+	err = storage.RemoveAll(".")
+	require.NoError(t, err)
+
+	fi, err := storage.ReadDir(".")
+	// a real FS would remove the root directory with RemoveAll and subsequent call would fail
+	// a memory FS would still have a virtual root and subsequent call would succeed
+	// not ideal, but will do for now
+	if err == nil {
+		require.Empty(t, fi)
+	} else {
+		require.True(t, os.IsNotExist(err))
+	}
+}
+
 func randomHash() Hash {
 	var letterRunes = "abcdef0123456789"
 	b := make([]byte, idLengthSHA256)