Merge branch 'master' into fix/778-git-bug-rm-broken

This commit is contained in:
Steve Moyer 2022-06-06 08:31:42 -04:00
commit 8fc93d8824
No known key found for this signature in database
GPG Key ID: D8D464C9D22FB8E0
22 changed files with 280 additions and 206 deletions

8
.gitignore vendored
View File

@ -1,8 +1,8 @@
git-bug
!/misc/bash_completion/git-bug
!/misc/fish_completion/git-bug
!/misc/powershell_completion/git-bug
!/misc/zsh_completion/git-bug
!/misc/completion/bash/git-bug
!/misc/completion/fish/git-bug
!/misc/completion/powershell/git-bug
!/misc/completion/zsh/git-bug
.gitkeep
dist
coverage.txt

View File

@ -268,20 +268,22 @@ git bug bridge rm [<name>]
## Internals
Interested by how it works ? Have a look at the [data model](doc/model.md) and the [internal bird-view](doc/architecture.md).
Interested in how it works ? Have a look at the [data model](doc/model.md) and the [internal bird-view](doc/architecture.md).
Or maybe you want to [make your own distributed data-structure in git](entity/dag/example_test.go) ?
See also all the [docs](doc).
## Misc
- [Bash completion](misc/bash_completion)
- [Zsh completion](misc/zsh_completion)
- [PowerShell completion](misc/powershell_completion)
- [Bash, Zsh, fish, powershell completion](misc/completion)
- [ManPages](doc/man)
## Planned features
- media embedding
- more bridges
- extendable data model to support arbitrary bug tracker
- webUI that can be used as a public portal to accept user's input
- inflatable raptor
## Contribute

View File

@ -49,13 +49,13 @@ func NewLazyBug(cache *cache.RepoCache, excerpt *cache.BugExcerpt) *lazyBug {
}
func (lb *lazyBug) load() error {
lb.mu.Lock()
defer lb.mu.Unlock()
if lb.snap != nil {
return nil
}
lb.mu.Lock()
defer lb.mu.Unlock()
b, err := lb.cache.ResolveBug(lb.excerpt.Id)
if err != nil {
return err

View File

@ -41,13 +41,13 @@ func NewLazyIdentity(cache *cache.RepoCache, excerpt *cache.IdentityExcerpt) *la
}
func (li *lazyIdentity) load() (*cache.IdentityCache, error) {
li.mu.Lock()
defer li.mu.Unlock()
if li.id != nil {
return li.id, nil
}
li.mu.Lock()
defer li.mu.Unlock()
id, err := li.cache.ResolveIdentity(li.excerpt.Id)
if err != nil {
return nil, fmt.Errorf("cache: missing identity %v", li.excerpt.Id)

View File

@ -7,8 +7,9 @@ import (
"strings"
"time"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/shurcooL/githubv4"
"github.com/MichaelMure/git-bug/bridge/core"
)
var _ Client = &githubv4.Client{}
@ -29,79 +30,69 @@ func newRateLimitHandlerClient(httpClient *http.Client) *rateLimitHandlerClient
return &rateLimitHandlerClient{sc: githubv4.NewClient(httpClient)}
}
type RateLimitingEvent struct {
msg string
}
// mutate calls the github api with a graphql mutation and for each rate limiting event it sends an
// export result.
// mutate calls the github api with a graphql mutation and sends a core.ExportResult for each rate limiting event
func (c *rateLimitHandlerClient) mutate(ctx context.Context, m interface{}, input githubv4.Input, vars map[string]interface{}, out chan<- core.ExportResult) error {
// prepare a closure for the mutation
mutFun := func(ctx context.Context) error {
return c.sc.Mutate(ctx, m, input, vars)
}
limitEvents := make(chan RateLimitingEvent)
defer close(limitEvents)
go func() {
for e := range limitEvents {
select {
case <-ctx.Done():
return
case out <- core.NewExportRateLimiting(e.msg):
}
callback := func(msg string) {
select {
case <-ctx.Done():
case out <- core.NewExportRateLimiting(msg):
}
}()
return c.callAPIAndRetry(mutFun, ctx, limitEvents)
}
return c.callAPIAndRetry(ctx, mutFun, callback)
}
// queryWithLimitEvents calls the github api with a graphql query and it sends rate limiting events
// to a given channel of type RateLimitingEvent.
func (c *rateLimitHandlerClient) queryWithLimitEvents(ctx context.Context, query interface{}, vars map[string]interface{}, limitEvents chan<- RateLimitingEvent) error {
// prepare a closure fot the query
// queryImport calls the github api with a graphql query, and sends an ImportEvent for each rate limiting event
func (c *rateLimitHandlerClient) queryImport(ctx context.Context, query interface{}, vars map[string]interface{}, importEvents chan<- ImportEvent) error {
// prepare a closure for the query
queryFun := func(ctx context.Context) error {
return c.sc.Query(ctx, query, vars)
}
return c.callAPIAndRetry(queryFun, ctx, limitEvents)
}
// queryWithImportEvents calls the github api with a graphql query and it sends rate limiting events
// to a given channel of type ImportEvent.
func (c *rateLimitHandlerClient) queryWithImportEvents(ctx context.Context, query interface{}, vars map[string]interface{}, importEvents chan<- ImportEvent) error {
// forward rate limiting events to channel of import events
limitEvents := make(chan RateLimitingEvent)
defer close(limitEvents)
go func() {
for e := range limitEvents {
select {
case <-ctx.Done():
return
case importEvents <- e:
}
callback := func(msg string) {
select {
case <-ctx.Done():
case importEvents <- RateLimitingEvent{msg}:
}
}()
return c.queryWithLimitEvents(ctx, query, vars, limitEvents)
}
return c.callAPIAndRetry(ctx, queryFun, callback)
}
// queryPrintMsgs calls the github api with a graphql query and it prints for ever rate limiting
// event a message to stdout.
// queryImport calls the github api with a graphql query, and sends a core.ExportResult for each rate limiting event
func (c *rateLimitHandlerClient) queryExport(ctx context.Context, query interface{}, vars map[string]interface{}, out chan<- core.ExportResult) error {
// prepare a closure for the query
queryFun := func(ctx context.Context) error {
return c.sc.Query(ctx, query, vars)
}
callback := func(msg string) {
select {
case <-ctx.Done():
case out <- core.NewExportRateLimiting(msg):
}
}
return c.callAPIAndRetry(ctx, queryFun, callback)
}
// queryPrintMsgs calls the github api with a graphql query, and prints a message to stdout for every rate limiting event .
func (c *rateLimitHandlerClient) queryPrintMsgs(ctx context.Context, query interface{}, vars map[string]interface{}) error {
// print rate limiting events directly to stdout.
limitEvents := make(chan RateLimitingEvent)
defer close(limitEvents)
go func() {
for e := range limitEvents {
fmt.Println(e.msg)
}
}()
return c.queryWithLimitEvents(ctx, query, vars, limitEvents)
// prepare a closure for the query
queryFun := func(ctx context.Context) error {
return c.sc.Query(ctx, query, vars)
}
callback := func(msg string) {
fmt.Println(msg)
}
return c.callAPIAndRetry(ctx, queryFun, callback)
}
// callAPIAndRetry calls the Github GraphQL API (inderectely through callAPIDealWithLimit) and in
// case of error it repeats the request to the Github API. The parameter `apiCall` is intended to be
// a closure containing a query or a mutation to the Github GraphQL API.
func (c *rateLimitHandlerClient) callAPIAndRetry(apiCall func(context.Context) error, ctx context.Context, events chan<- RateLimitingEvent) error {
func (c *rateLimitHandlerClient) callAPIAndRetry(ctx context.Context, apiCall func(context.Context) error, rateLimitEvent func(msg string)) error {
var err error
if err = c.callAPIDealWithLimit(apiCall, ctx, events); err == nil {
if err = c.callAPIDealWithLimit(ctx, apiCall, rateLimitEvent); err == nil {
return nil
}
// failure; the reason may be temporary network problems or internal errors
@ -117,7 +108,7 @@ func (c *rateLimitHandlerClient) callAPIAndRetry(apiCall func(context.Context) e
stop(timer)
return ctx.Err()
case <-timer.C:
err = c.callAPIDealWithLimit(apiCall, ctx, events)
err = c.callAPIDealWithLimit(ctx, apiCall, rateLimitEvent)
if err == nil {
return nil
}
@ -127,10 +118,10 @@ func (c *rateLimitHandlerClient) callAPIAndRetry(apiCall func(context.Context) e
}
// callAPIDealWithLimit calls the Github GraphQL API and if the Github API returns a rate limiting
// error, then it waits until the rate limit is reset and it repeats the request to the API. The
// error, then it waits until the rate limit is reset, and it repeats the request to the API. The
// parameter `apiCall` is intended to be a closure containing a query or a mutation to the Github
// GraphQL API.
func (c *rateLimitHandlerClient) callAPIDealWithLimit(apiCall func(context.Context) error, ctx context.Context, events chan<- RateLimitingEvent) error {
func (c *rateLimitHandlerClient) callAPIDealWithLimit(ctx context.Context, apiCall func(context.Context) error, rateLimitCallback func(msg string)) error {
qctx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
// call the function fun()
@ -155,11 +146,8 @@ func (c *rateLimitHandlerClient) callAPIDealWithLimit(apiCall func(context.Conte
resetTime.String(),
)
// Send message about rate limiting event.
select {
case <-ctx.Done():
return ctx.Err()
case events <- RateLimitingEvent{msg}:
}
rateLimitCallback(msg)
// Pause current goroutine
timer := time.NewTimer(time.Until(resetTime))
select {

View File

@ -486,23 +486,10 @@ func (ge *githubExporter) cacheGithubLabels(ctx context.Context, gc *rateLimitHa
}
q := labelsQuery{}
// When performing the queries we have to forward rate limiting events to the
// current channel of export results.
events := make(chan RateLimitingEvent)
defer close(events)
go func() {
for e := range events {
select {
case <-ctx.Done():
return
case ge.out <- core.NewExportRateLimiting(e.msg):
}
}
}()
hasNextPage := true
for hasNextPage {
if err := gc.queryWithLimitEvents(ctx, &q, variables, events); err != nil {
if err := gc.queryExport(ctx, &q, variables, ge.out); err != nil {
return err
}

View File

@ -0,0 +1,40 @@
package github
import "github.com/shurcooL/githubv4"
type ImportEvent interface {
isImportEvent()
}
type RateLimitingEvent struct {
msg string
}
func (RateLimitingEvent) isImportEvent() {}
type IssueEvent struct {
issue
}
func (IssueEvent) isImportEvent() {}
type IssueEditEvent struct {
issueId githubv4.ID
userContentEdit
}
func (IssueEditEvent) isImportEvent() {}
type TimelineEvent struct {
issueId githubv4.ID
timelineItem
}
func (TimelineEvent) isImportEvent() {}
type CommentEditEvent struct {
commentId githubv4.ID
userContentEdit
}
func (CommentEditEvent) isImportEvent() {}

View File

@ -9,6 +9,7 @@ import (
const (
// These values influence how fast the github graphql rate limit is exhausted.
NumIssues = 40
NumIssueEdits = 100
NumTimelineItems = 100
@ -41,43 +42,6 @@ type importMediator struct {
err error
}
type ImportEvent interface {
isImportEvent()
}
func (RateLimitingEvent) isImportEvent() {}
type IssueEvent struct {
issue
}
func (IssueEvent) isImportEvent() {}
type IssueEditEvent struct {
issueId githubv4.ID
userContentEdit
}
func (IssueEditEvent) isImportEvent() {}
type TimelineEvent struct {
issueId githubv4.ID
timelineItem
}
func (TimelineEvent) isImportEvent() {}
type CommentEditEvent struct {
commentId githubv4.ID
userContentEdit
}
func (CommentEditEvent) isImportEvent() {}
func (mm *importMediator) NextImportEvent() ImportEvent {
return <-mm.importEvents
}
func NewImportMediator(ctx context.Context, client *rateLimitHandlerClient, owner, project string, since time.Time) *importMediator {
mm := importMediator{
gh: client,
@ -87,48 +51,24 @@ func NewImportMediator(ctx context.Context, client *rateLimitHandlerClient, owne
importEvents: make(chan ImportEvent, ChanCapacity),
err: nil,
}
go func() {
mm.fillImportEvents(ctx)
close(mm.importEvents)
}()
go mm.start(ctx)
return &mm
}
type varmap map[string]interface{}
func newIssueVars(owner, project string, since time.Time) varmap {
return varmap{
"owner": githubv4.String(owner),
"name": githubv4.String(project),
"issueSince": githubv4.DateTime{Time: since},
"issueFirst": githubv4.Int(NumIssues),
"issueEditLast": githubv4.Int(NumIssueEdits),
"issueEditBefore": (*githubv4.String)(nil),
"timelineFirst": githubv4.Int(NumTimelineItems),
"timelineAfter": (*githubv4.String)(nil),
"commentEditLast": githubv4.Int(NumCommentEdits),
"commentEditBefore": (*githubv4.String)(nil),
}
func (mm *importMediator) start(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
mm.fillImportEvents(ctx)
// Make sure we cancel everything when we are done, instead of relying on the parent context
// This should unblock pending send to the channel if the capacity was reached and avoid a panic/race when closing.
cancel()
close(mm.importEvents)
}
func newIssueEditVars() varmap {
return varmap{
"issueEditLast": githubv4.Int(NumIssueEdits),
}
}
func newTimelineVars() varmap {
return varmap{
"timelineFirst": githubv4.Int(NumTimelineItems),
"commentEditLast": githubv4.Int(NumCommentEdits),
"commentEditBefore": (*githubv4.String)(nil),
}
}
func newCommentEditVars() varmap {
return varmap{
"commentEditLast": githubv4.Int(NumCommentEdits),
}
// NextImportEvent returns the next ImportEvent, or nil if done.
func (mm *importMediator) NextImportEvent() ImportEvent {
return <-mm.importEvents
}
func (mm *importMediator) Error() error {
@ -138,7 +78,7 @@ func (mm *importMediator) Error() error {
func (mm *importMediator) User(ctx context.Context, loginName string) (*user, error) {
query := userQuery{}
vars := varmap{"login": githubv4.String(loginName)}
if err := mm.gh.queryWithImportEvents(ctx, &query, vars, mm.importEvents); err != nil {
if err := mm.gh.queryImport(ctx, &query, vars, mm.importEvents); err != nil {
return nil, err
}
return &query.User, nil
@ -200,7 +140,7 @@ func (mm *importMediator) queryIssueEdits(ctx context.Context, nid githubv4.ID,
vars["issueEditBefore"] = cursor
}
query := issueEditQuery{}
if err := mm.gh.queryWithImportEvents(ctx, &query, vars, mm.importEvents); err != nil {
if err := mm.gh.queryImport(ctx, &query, vars, mm.importEvents); err != nil {
mm.err = err
return nil, false
}
@ -244,7 +184,7 @@ func (mm *importMediator) queryTimeline(ctx context.Context, nid githubv4.ID, cu
vars["timelineAfter"] = cursor
}
query := timelineQuery{}
if err := mm.gh.queryWithImportEvents(ctx, &query, vars, mm.importEvents); err != nil {
if err := mm.gh.queryImport(ctx, &query, vars, mm.importEvents); err != nil {
mm.err = err
return nil, false
}
@ -294,7 +234,7 @@ func (mm *importMediator) queryCommentEdits(ctx context.Context, nid githubv4.ID
vars["commentEditBefore"] = cursor
}
query := commentEditQuery{}
if err := mm.gh.queryWithImportEvents(ctx, &query, vars, mm.importEvents); err != nil {
if err := mm.gh.queryImport(ctx, &query, vars, mm.importEvents); err != nil {
mm.err = err
return nil, false
}
@ -313,7 +253,7 @@ func (mm *importMediator) queryIssue(ctx context.Context, cursor githubv4.String
vars["issueAfter"] = cursor
}
query := issueQuery{}
if err := mm.gh.queryWithImportEvents(ctx, &query, vars, mm.importEvents); err != nil {
if err := mm.gh.queryImport(ctx, &query, vars, mm.importEvents); err != nil {
mm.err = err
return nil, false
}
@ -334,3 +274,41 @@ func reverse(eds []userContentEdit) chan userContentEdit {
}()
return ret
}
// varmap is a container for Github API's pagination variables
type varmap map[string]interface{}
func newIssueVars(owner, project string, since time.Time) varmap {
return varmap{
"owner": githubv4.String(owner),
"name": githubv4.String(project),
"issueSince": githubv4.DateTime{Time: since},
"issueFirst": githubv4.Int(NumIssues),
"issueEditLast": githubv4.Int(NumIssueEdits),
"issueEditBefore": (*githubv4.String)(nil),
"timelineFirst": githubv4.Int(NumTimelineItems),
"timelineAfter": (*githubv4.String)(nil),
"commentEditLast": githubv4.Int(NumCommentEdits),
"commentEditBefore": (*githubv4.String)(nil),
}
}
func newIssueEditVars() varmap {
return varmap{
"issueEditLast": githubv4.Int(NumIssueEdits),
}
}
func newTimelineVars() varmap {
return varmap{
"timelineFirst": githubv4.Int(NumTimelineItems),
"commentEditLast": githubv4.Int(NumCommentEdits),
"commentEditBefore": (*githubv4.String)(nil),
}
}
func newCommentEditVars() varmap {
return varmap{
"commentEditLast": githubv4.Int(NumCommentEdits),
}
}

View File

@ -14,6 +14,8 @@ import (
"github.com/MichaelMure/git-bug/util/interrupt"
)
const gitBugNamespace = "git-bug"
// Env is the environment of a command
type Env struct {
repo repository.ClockedRepo
@ -68,7 +70,7 @@ func loadRepo(env *Env) func(*cobra.Command, []string) error {
return err
}
env.repo, err = repository.OpenGoGitRepo(cwd, []repository.ClockLoader{bug.ClockLoader})
env.repo, err = repository.OpenGoGitRepo(cwd, gitBugNamespace, []repository.ClockLoader{bug.ClockLoader})
if err == repository.ErrNotARepo {
return fmt.Errorf("%s must be run from within a git repo", rootCommandName)
}

15
doc/README.md Normal file
View File

@ -0,0 +1,15 @@
# Documentation
## For users
- [data model](model.md) describe how the data model works and why.
- [query language](queries.md) describe git-bug's query language.
- [How-to: Read and edit offline your Github/Gitlab/Jira issues with git-bug](howto-github.md)
## For developers
- :exclamation: [data model](model.md) describe how the data model works and why.
- :exclamation: [internal bird-view](architecture.md) gives an overview of the project architecture.
- :exclamation: [Entity/DAG](../entity/dag/example_test.go) explain how to easily make your own distributed entity in git.
- [query language](queries.md) describe git-bug's query language.
- [JIRA bridge de v notes](jira_bridge.md)

View File

@ -3,6 +3,8 @@ Entities data model
If you are not familiar with [git internals](https://git-scm.com/book/en/v1/Git-Internals), you might first want to read about them, as the `git-bug` data model is built on top of them.
In a different format, see how you can easily make your own [distributed data structure](../entity/dag/example_test.go).
## Entities (bug, author, ...) are a series of edit operations
As entities are stored and edited in multiple processes at the same time, it's not possible to store the current state like it would be done in a normal application. If two processes change the same entity and later try to merge the states, we wouldn't know which change takes precedence or how to merge those states.

View File

@ -336,14 +336,17 @@ func Read(repo repository.ClockedRepo, id entity.Id) (*ProjectConfig, error) {
}
func Example_entity() {
const gitBugNamespace = "git-bug"
// Note: this example ignore errors for readability
// Note: variable names get a little confusing as we are simulating both side in the same function
// Let's start by defining two git repository and connecting them as remote
repoRenePath, _ := os.MkdirTemp("", "")
repoIsaacPath, _ := os.MkdirTemp("", "")
repoRene, _ := repository.InitGoGitRepo(repoRenePath)
repoIsaac, _ := repository.InitGoGitRepo(repoIsaacPath)
repoRene, _ := repository.InitGoGitRepo(repoRenePath, gitBugNamespace)
defer repoRene.Close()
repoIsaac, _ := repository.InitGoGitRepo(repoIsaacPath, gitBugNamespace)
defer repoIsaac.Close()
_ = repoRene.AddRemote("origin", repoIsaacPath)
_ = repoIsaac.AddRemote("origin", repoRenePath)

View File

@ -1,5 +1,5 @@
//go:generate go run doc/gen_docs.go
//go:generate go run misc/gen_completion.go
//go:generate go run misc/completion/gen_completion.go
package main

View File

@ -287,6 +287,8 @@ fi
# ex: ts=4 sw=4 et filetype=sh
# Custom bash code to connect the git completion for "git bug" to the
# git-bug completion for "git-bug"
_git_bug() {
local cur prev words cword split

View File

@ -44,13 +44,15 @@ func genBash(root *cobra.Command) error {
if err != nil {
return err
}
f, err := os.Create(filepath.Join(cwd, "misc", "bash_completion", "git-bug"))
f, err := os.Create(filepath.Join(cwd, "misc", "completion", "bash", "git-bug"))
if err != nil {
return err
}
defer f.Close()
const patch = `
# Custom bash code to connect the git completion for "git bug" to the
# git-bug completion for "git-bug"
_git_bug() {
local cur prev words cword split
@ -102,7 +104,7 @@ func genFish(root *cobra.Command) error {
if err != nil {
return err
}
dir := filepath.Join(cwd, "misc", "fish_completion", "git-bug")
dir := filepath.Join(cwd, "misc", "completion", "fish", "git-bug")
return root.GenFishCompletionFile(dir, true)
}
@ -111,7 +113,7 @@ func genPowerShell(root *cobra.Command) error {
if err != nil {
return err
}
path := filepath.Join(cwd, "misc", "powershell_completion", "git-bug")
path := filepath.Join(cwd, "misc", "completion", "powershell", "git-bug")
return root.GenPowerShellCompletionFile(path)
}
@ -120,6 +122,6 @@ func genZsh(root *cobra.Command) error {
if err != nil {
return err
}
path := filepath.Join(cwd, "misc", "zsh_completion", "git-bug")
path := filepath.Join(cwd, "misc", "completion", "zsh", "git-bug")
return root.GenZshCompletionFile(path)
}

View File

@ -11,6 +11,8 @@ import (
// This program will randomly generate a collection of bugs in the repository
// of the current path
func main() {
const gitBugNamespace = "git-bug"
dir, err := os.Getwd()
if err != nil {
panic(err)
@ -20,7 +22,7 @@ func main() {
bug.ClockLoader,
}
repo, err := repository.OpenGoGitRepo(dir, loaders)
repo, err := repository.OpenGoGitRepo(dir, gitBugNamespace, loaders)
if err != nil {
panic(err)
}

View File

@ -26,6 +26,7 @@ import (
)
const clockPath = "clocks"
const indexPath = "indexes"
var _ ClockedRepo = &GoGitRepo{}
var _ TestedRepo = &GoGitRepo{}
@ -49,8 +50,11 @@ type GoGitRepo struct {
localStorage billy.Filesystem
}
// OpenGoGitRepo open an already existing repo at the given path
func OpenGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) {
// OpenGoGitRepo opens an already existing repo at the given path and
// with the specified LocalStorage namespace. Given a repository path
// of "~/myrepo" and a namespace of "git-bug", local storage for the
// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
func OpenGoGitRepo(path, namespace string, clockLoaders []ClockLoader) (*GoGitRepo, error) {
path, err := detectGitPath(path)
if err != nil {
return nil, err
@ -72,7 +76,7 @@ func OpenGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error)
clocks: make(map[string]lamport.Clock),
indexes: make(map[string]bleve.Index),
keyring: k,
localStorage: osfs.New(filepath.Join(path, "git-bug")),
localStorage: osfs.New(filepath.Join(path, namespace)),
}
for _, loader := range clockLoaders {
@ -94,8 +98,11 @@ func OpenGoGitRepo(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) {
// InitGoGitRepo creates a new empty git repo at the given path and
// with the specified LocalStorage namespace. Given a repository path
// of "~/myrepo" and a namespace of "git-bug", local storage for the
// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
func InitGoGitRepo(path, namespace string) (*GoGitRepo, error) {
r, err := gogit.PlainInit(path, false)
if err != nil {
return nil, err
@ -112,12 +119,15 @@ func InitGoGitRepo(path string) (*GoGitRepo, error) {
clocks: make(map[string]lamport.Clock),
indexes: make(map[string]bleve.Index),
keyring: k,
localStorage: osfs.New(filepath.Join(path, ".git", "git-bug")),
localStorage: osfs.New(filepath.Join(path, ".git", namespace)),
}, nil
}
// InitBareGoGitRepo create a new --bare empty git repo at the given path
func InitBareGoGitRepo(path string) (*GoGitRepo, error) {
// InitBareGoGitRepo creates a new --bare empty git repo at the given
// path and with the specified LocalStorage namespace. Given a repository
// path of "~/myrepo" and a namespace of "git-bug", local storage for the
// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
func InitBareGoGitRepo(path, namespace string) (*GoGitRepo, error) {
r, err := gogit.PlainInit(path, true)
if err != nil {
return nil, err
@ -134,7 +144,7 @@ func InitBareGoGitRepo(path string) (*GoGitRepo, error) {
clocks: make(map[string]lamport.Clock),
indexes: make(map[string]bleve.Index),
keyring: k,
localStorage: osfs.New(filepath.Join(path, "git-bug")),
localStorage: osfs.New(filepath.Join(path, namespace)),
}, nil
}
@ -295,7 +305,8 @@ func (repo *GoGitRepo) GetRemotes() (map[string]string, error) {
return result, nil
}
// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug
// LocalStorage returns a billy.Filesystem giving access to
// $RepoPath/.git/$Namespace.
func (repo *GoGitRepo) LocalStorage() billy.Filesystem {
return repo.localStorage
}
@ -309,7 +320,7 @@ func (repo *GoGitRepo) GetBleveIndex(name string) (bleve.Index, error) {
return index, nil
}
path := filepath.Join(repo.path, "git-bug", "indexes", name)
path := filepath.Join(repo.localStorage.Root(), indexPath, name)
index, err := bleve.Open(path)
if err == nil {
@ -340,21 +351,20 @@ func (repo *GoGitRepo) ClearBleveIndex(name string) error {
repo.indexesMutex.Lock()
defer repo.indexesMutex.Unlock()
path := filepath.Join(repo.path, "git-bug", "indexes", name)
err := os.RemoveAll(path)
if err != nil {
return err
}
if index, ok := repo.indexes[name]; ok {
err = index.Close()
err := index.Close()
if err != nil {
return err
}
delete(repo.indexes, name)
}
path := filepath.Join(repo.localStorage.Root(), indexPath, name)
err := os.RemoveAll(path)
if err != nil {
return err
}
return nil
}
@ -569,7 +579,7 @@ func (repo *GoGitRepo) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error)
return repo.StoreSignedCommit(treeHash, nil, parents...)
}
// StoreCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit
// StoreSignedCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit
// will be signed accordingly.
func (repo *GoGitRepo) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
cfg, err := repo.r.Config()
@ -781,7 +791,7 @@ func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) {
result := make(map[string]lamport.Clock)
files, err := ioutil.ReadDir(filepath.Join(repo.path, "git-bug", clockPath))
files, err := ioutil.ReadDir(filepath.Join(repo.localStorage.Root(), clockPath))
if os.IsNotExist(err) {
return nil, nil
}

View File

@ -15,19 +15,25 @@ func TestNewGoGitRepo(t *testing.T) {
// Plain
plainRoot, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(plainRoot)
t.Cleanup(func() {
require.NoError(t, os.RemoveAll(plainRoot))
})
_, err = InitGoGitRepo(plainRoot)
plainRepo, err := InitGoGitRepo(plainRoot, namespace)
require.NoError(t, err)
require.NoError(t, plainRepo.Close())
plainGitDir := filepath.Join(plainRoot, ".git")
// Bare
bareRoot, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(bareRoot)
t.Cleanup(func() {
require.NoError(t, os.RemoveAll(bareRoot))
})
_, err = InitBareGoGitRepo(bareRoot)
bareRepo, err := InitBareGoGitRepo(bareRoot, namespace)
require.NoError(t, err)
require.NoError(t, bareRepo.Close())
bareGitDir := bareRoot
tests := []struct {
@ -52,13 +58,14 @@ func TestNewGoGitRepo(t *testing.T) {
}
for i, tc := range tests {
r, err := OpenGoGitRepo(tc.inPath, nil)
r, err := OpenGoGitRepo(tc.inPath, namespace, 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.path), i)
require.NoError(t, r.Close())
}
}
}
@ -66,3 +73,35 @@ func TestNewGoGitRepo(t *testing.T) {
func TestGoGitRepo(t *testing.T) {
RepoTest(t, CreateGoGitTestRepo, CleanupTestRepos)
}
func TestGoGitRepo_Indexes(t *testing.T) {
plainRoot := t.TempDir()
repo, err := InitGoGitRepo(plainRoot, namespace)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, repo.Close())
})
// Can create indices
indexA, err := repo.GetBleveIndex("a")
require.NoError(t, err)
require.NotZero(t, indexA)
require.FileExists(t, filepath.Join(plainRoot, ".git", namespace, "indexes", "a", "index_meta.json"))
require.FileExists(t, filepath.Join(plainRoot, ".git", namespace, "indexes", "a", "store"))
indexB, err := repo.GetBleveIndex("b")
require.NoError(t, err)
require.NotZero(t, indexB)
require.DirExists(t, filepath.Join(plainRoot, ".git", namespace, "indexes", "b"))
// Can get an existing index
indexA, err = repo.GetBleveIndex("a")
require.NoError(t, err)
require.NotZero(t, indexA)
// Can delete an index
err = repo.ClearBleveIndex("a")
require.NoError(t, err)
require.NoDirExists(t, filepath.Join(plainRoot, ".git", namespace, "indexes", "a"))
}

View File

@ -7,6 +7,8 @@ import (
"github.com/99designs/keyring"
)
const namespace = "git-bug"
// This is intended for testing only
func CreateGoGitTestRepo(bare bool) TestedRepo {
@ -15,7 +17,7 @@ func CreateGoGitTestRepo(bare bool) TestedRepo {
log.Fatal(err)
}
var creator func(string) (*GoGitRepo, error)
var creator func(string, string) (*GoGitRepo, error)
if bare {
creator = InitBareGoGitRepo
@ -23,7 +25,7 @@ func CreateGoGitTestRepo(bare bool) TestedRepo {
creator = InitGoGitRepo
}
repo, err := creator(dir)
repo, err := creator(dir, namespace)
if err != nil {
log.Fatal(err)
}