mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-14 08:45:30 +03:00
Merge branch 'master' into fix/778-git-bug-rm-broken
This commit is contained in:
commit
8fc93d8824
8
.gitignore
vendored
8
.gitignore
vendored
@ -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
|
||||
|
12
README.md
12
README.md
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
40
bridge/github/import_events.go
Normal file
40
bridge/github/import_events.go
Normal 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() {}
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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
15
doc/README.md
Normal 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)
|
@ -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.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user