mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-15 10:12:06 +03:00
commit
7260ca05bc
38
Gopkg.lock
generated
38
Gopkg.lock
generated
@ -83,14 +83,6 @@
|
||||
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
|
||||
version = "v1.7.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7f89e0c888fb99c61055c646f5678aae645b0b0a1443d9b2dcd9964d850827ce"
|
||||
name = "github.com/go-test/deep"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "6592d9cc0a499ad2d5f574fde80a2b5c5cc3b4f5"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:97df918963298c287643883209a2c3f642e6593379f97ab400c2a2e219ab647d"
|
||||
name = "github.com/golang/protobuf"
|
||||
@ -99,19 +91,6 @@
|
||||
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2e3c336fc7fde5c984d2841455a658a6d626450b1754a854b3b32e7a8f49a07a"
|
||||
name = "github.com/google/go-cmp"
|
||||
packages = [
|
||||
"cmp",
|
||||
"cmp/internal/diff",
|
||||
"cmp/internal/function",
|
||||
"cmp/internal/value",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "3af367b6b30c263d47e8895973edcca9a49cf029"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
|
||||
name = "github.com/gorilla/context"
|
||||
@ -440,20 +419,6 @@
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:225f565dc88a02cebe329d3a49d0ca125789091af952a5cc4fde6312c34ce44d"
|
||||
name = "gotest.tools"
|
||||
packages = [
|
||||
"assert",
|
||||
"assert/cmp",
|
||||
"internal/difflib",
|
||||
"internal/format",
|
||||
"internal/source",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "b6e20af1ed078cd01a6413b734051a292450b4cb"
|
||||
version = "v2.1.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
@ -466,9 +431,9 @@
|
||||
"github.com/cheekybits/genny/generic",
|
||||
"github.com/dustin/go-humanize",
|
||||
"github.com/fatih/color",
|
||||
"github.com/go-test/deep",
|
||||
"github.com/gorilla/mux",
|
||||
"github.com/icrowley/fake",
|
||||
"github.com/mattn/go-runewidth",
|
||||
"github.com/phayes/freeport",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/shurcooL/githubv4",
|
||||
@ -485,7 +450,6 @@
|
||||
"github.com/vektah/gqlparser/ast",
|
||||
"golang.org/x/crypto/ssh/terminal",
|
||||
"golang.org/x/oauth2",
|
||||
"gotest.tools/assert",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
11
Makefile
11
Makefile
@ -19,7 +19,7 @@ debug-build:
|
||||
|
||||
install:
|
||||
go generate
|
||||
go install .
|
||||
go install -ldflags "$(LDFLAGS)" .
|
||||
|
||||
test:
|
||||
go test -bench=. ./...
|
||||
@ -30,14 +30,19 @@ pack-webui:
|
||||
|
||||
# produce a build that will fetch the web UI from the filesystem instead of from the binary
|
||||
debug-webui:
|
||||
go build -tags=debugwebui
|
||||
go build -ldflags "$(LDFLAGS)" -tags=debugwebui
|
||||
|
||||
clean-local-bugs:
|
||||
git for-each-ref refs/bugs/ | cut -f 2 | xargs -r -n 1 git update-ref -d
|
||||
git for-each-ref refs/remotes/origin/bugs/ | cut -f 2 | xargs -r -n 1 git update-ref -d
|
||||
rm -f .git/git-bug/cache
|
||||
rm -f .git/git-bug/bug-cache
|
||||
|
||||
clean-remote-bugs:
|
||||
git ls-remote origin "refs/bugs/*" | cut -f 2 | xargs -r git push origin -d
|
||||
|
||||
clean-local-identities:
|
||||
git for-each-ref refs/identities/ | cut -f 2 | xargs -r -n 1 git update-ref -d
|
||||
git for-each-ref refs/remotes/origin/identities/ | cut -f 2 | xargs -r -n 1 git update-ref -d
|
||||
rm -f .git/git-bug/identity-cache
|
||||
|
||||
.PHONY: build install test pack-webui debug-webui clean-local-bugs clean-remote-bugs
|
||||
|
@ -12,8 +12,10 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrImportNorSupported = errors.New("import is not supported")
|
||||
var ErrExportNorSupported = errors.New("export is not supported")
|
||||
var ErrImportNotSupported = errors.New("import is not supported")
|
||||
var ErrExportNotSupported = errors.New("export is not supported")
|
||||
|
||||
const bridgeConfigKeyPrefix = "git-bug.bridge"
|
||||
|
||||
var bridgeImpl map[string]reflect.Type
|
||||
|
||||
@ -114,12 +116,12 @@ func splitFullName(fullName string) (string, string, error) {
|
||||
// ConfiguredBridges return the list of bridge that are configured for the given
|
||||
// repo
|
||||
func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
|
||||
configs, err := repo.ReadConfigs("git-bug.bridge.")
|
||||
configs, err := repo.ReadConfigs(bridgeConfigKeyPrefix + ".")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't read configured bridges")
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(`git-bug.bridge.([^.]+\.[^.]+)`)
|
||||
re, err := regexp.Compile(bridgeConfigKeyPrefix + `.([^.]+\.[^.]+)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -266,7 +268,7 @@ func (b *Bridge) ensureInit() error {
|
||||
func (b *Bridge) ImportAll() error {
|
||||
importer := b.getImporter()
|
||||
if importer == nil {
|
||||
return ErrImportNorSupported
|
||||
return ErrImportNotSupported
|
||||
}
|
||||
|
||||
err := b.ensureConfig()
|
||||
@ -285,7 +287,7 @@ func (b *Bridge) ImportAll() error {
|
||||
func (b *Bridge) Import(id string) error {
|
||||
importer := b.getImporter()
|
||||
if importer == nil {
|
||||
return ErrImportNorSupported
|
||||
return ErrImportNotSupported
|
||||
}
|
||||
|
||||
err := b.ensureConfig()
|
||||
@ -304,7 +306,7 @@ func (b *Bridge) Import(id string) error {
|
||||
func (b *Bridge) ExportAll() error {
|
||||
exporter := b.getExporter()
|
||||
if exporter == nil {
|
||||
return ErrExportNorSupported
|
||||
return ErrExportNotSupported
|
||||
}
|
||||
|
||||
err := b.ensureConfig()
|
||||
@ -323,7 +325,7 @@ func (b *Bridge) ExportAll() error {
|
||||
func (b *Bridge) Export(id string) error {
|
||||
exporter := b.getExporter()
|
||||
if exporter == nil {
|
||||
return ErrExportNorSupported
|
||||
return ErrExportNotSupported
|
||||
}
|
||||
|
||||
err := b.ensureConfig()
|
||||
|
@ -8,25 +8,26 @@ import (
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
const keyGithubId = "github-id"
|
||||
const keyGithubUrl = "github-url"
|
||||
const keyGithubLogin = "github-login"
|
||||
|
||||
// githubImporter implement the Importer interface
|
||||
type githubImporter struct {
|
||||
client *githubv4.Client
|
||||
conf core.Configuration
|
||||
ghost bug.Person
|
||||
}
|
||||
|
||||
func (gi *githubImporter) Init(conf core.Configuration) error {
|
||||
gi.conf = conf
|
||||
gi.client = buildClient(conf)
|
||||
|
||||
return gi.fetchGhost()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gi *githubImporter) ImportAll(repo *cache.RepoCache) error {
|
||||
@ -69,7 +70,10 @@ func (gi *githubImporter) ImportAll(repo *cache.RepoCache) error {
|
||||
}
|
||||
|
||||
for _, itemEdge := range q.Repository.Issues.Nodes[0].Timeline.Edges {
|
||||
gi.ensureTimelineItem(b, itemEdge.Cursor, itemEdge.Node, variables)
|
||||
err = gi.ensureTimelineItem(repo, b, itemEdge.Cursor, itemEdge.Node, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !issue.Timeline.PageInfo.HasNextPage {
|
||||
@ -104,6 +108,11 @@ func (gi *githubImporter) Import(repo *cache.RepoCache, id string) error {
|
||||
func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline, rootVariables map[string]interface{}) (*cache.BugCache, error) {
|
||||
fmt.Printf("import issue: %s\n", issue.Title)
|
||||
|
||||
author, err := gi.ensurePerson(repo, issue.Author)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := repo.ResolveBugCreateMetadata(keyGithubId, parseId(issue.Id))
|
||||
if err != nil && err != bug.ErrBugNotExist {
|
||||
return nil, err
|
||||
@ -123,7 +132,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
|
||||
if len(issue.UserContentEdits.Nodes) == 0 {
|
||||
if err == bug.ErrBugNotExist {
|
||||
b, err = repo.NewBugRaw(
|
||||
gi.makePerson(issue.Author),
|
||||
author,
|
||||
issue.CreatedAt.Unix(),
|
||||
// Todo: this might not be the initial title, we need to query the
|
||||
// timeline to be sure
|
||||
@ -135,7 +144,6 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
|
||||
keyGithubUrl: issue.Url.String(),
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -161,7 +169,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
|
||||
|
||||
// we create the bug as soon as we have a legit first edition
|
||||
b, err = repo.NewBugRaw(
|
||||
gi.makePerson(issue.Author),
|
||||
author,
|
||||
issue.CreatedAt.Unix(),
|
||||
// Todo: this might not be the initial title, we need to query the
|
||||
// timeline to be sure
|
||||
@ -179,12 +187,12 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
|
||||
continue
|
||||
}
|
||||
|
||||
target, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(issue.Id))
|
||||
target, err := b.ResolveOperationWithMetadata(keyGithubId, parseId(issue.Id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = gi.ensureCommentEdit(b, target, edit)
|
||||
err = gi.ensureCommentEdit(repo, b, target, edit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -194,7 +202,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
|
||||
// if we still didn't get a legit edit, create the bug from the issue data
|
||||
if b == nil {
|
||||
return repo.NewBugRaw(
|
||||
gi.makePerson(issue.Author),
|
||||
author,
|
||||
issue.CreatedAt.Unix(),
|
||||
// Todo: this might not be the initial title, we need to query the
|
||||
// timeline to be sure
|
||||
@ -243,7 +251,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
|
||||
|
||||
// we create the bug as soon as we have a legit first edition
|
||||
b, err = repo.NewBugRaw(
|
||||
gi.makePerson(issue.Author),
|
||||
author,
|
||||
issue.CreatedAt.Unix(),
|
||||
// Todo: this might not be the initial title, we need to query the
|
||||
// timeline to be sure
|
||||
@ -261,12 +269,12 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
|
||||
continue
|
||||
}
|
||||
|
||||
target, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(issue.Id))
|
||||
target, err := b.ResolveOperationWithMetadata(keyGithubId, parseId(issue.Id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = gi.ensureCommentEdit(b, target, edit)
|
||||
err = gi.ensureCommentEdit(repo, b, target, edit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -284,7 +292,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
|
||||
// if we still didn't get a legit edit, create the bug from the issue data
|
||||
if b == nil {
|
||||
return repo.NewBugRaw(
|
||||
gi.makePerson(issue.Author),
|
||||
author,
|
||||
issue.CreatedAt.Unix(),
|
||||
// Todo: this might not be the initial title, we need to query the
|
||||
// timeline to be sure
|
||||
@ -301,21 +309,25 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4.String, item timelineItem, rootVariables map[string]interface{}) error {
|
||||
func (gi *githubImporter) ensureTimelineItem(repo *cache.RepoCache, b *cache.BugCache, cursor githubv4.String, item timelineItem, rootVariables map[string]interface{}) error {
|
||||
fmt.Printf("import %s\n", item.Typename)
|
||||
|
||||
switch item.Typename {
|
||||
case "IssueComment":
|
||||
return gi.ensureComment(b, cursor, item.IssueComment, rootVariables)
|
||||
return gi.ensureComment(repo, b, cursor, item.IssueComment, rootVariables)
|
||||
|
||||
case "LabeledEvent":
|
||||
id := parseId(item.LabeledEvent.Id)
|
||||
_, err := b.ResolveTargetWithMetadata(keyGithubId, id)
|
||||
_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
|
||||
if err != cache.ErrNoMatchingOp {
|
||||
return err
|
||||
}
|
||||
_, err = b.ChangeLabelsRaw(
|
||||
gi.makePerson(item.LabeledEvent.Actor),
|
||||
author, err := gi.ensurePerson(repo, item.LabeledEvent.Actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err = b.ChangeLabelsRaw(
|
||||
author,
|
||||
item.LabeledEvent.CreatedAt.Unix(),
|
||||
[]string{
|
||||
string(item.LabeledEvent.Label.Name),
|
||||
@ -327,12 +339,16 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4.
|
||||
|
||||
case "UnlabeledEvent":
|
||||
id := parseId(item.UnlabeledEvent.Id)
|
||||
_, err := b.ResolveTargetWithMetadata(keyGithubId, id)
|
||||
_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
|
||||
if err != cache.ErrNoMatchingOp {
|
||||
return err
|
||||
}
|
||||
_, err = b.ChangeLabelsRaw(
|
||||
gi.makePerson(item.UnlabeledEvent.Actor),
|
||||
author, err := gi.ensurePerson(repo, item.UnlabeledEvent.Actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err = b.ChangeLabelsRaw(
|
||||
author,
|
||||
item.UnlabeledEvent.CreatedAt.Unix(),
|
||||
nil,
|
||||
[]string{
|
||||
@ -344,40 +360,55 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4.
|
||||
|
||||
case "ClosedEvent":
|
||||
id := parseId(item.ClosedEvent.Id)
|
||||
_, err := b.ResolveTargetWithMetadata(keyGithubId, id)
|
||||
_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
|
||||
if err != cache.ErrNoMatchingOp {
|
||||
return err
|
||||
}
|
||||
return b.CloseRaw(
|
||||
gi.makePerson(item.ClosedEvent.Actor),
|
||||
author, err := gi.ensurePerson(repo, item.ClosedEvent.Actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = b.CloseRaw(
|
||||
author,
|
||||
item.ClosedEvent.CreatedAt.Unix(),
|
||||
map[string]string{keyGithubId: id},
|
||||
)
|
||||
return err
|
||||
|
||||
case "ReopenedEvent":
|
||||
id := parseId(item.ReopenedEvent.Id)
|
||||
_, err := b.ResolveTargetWithMetadata(keyGithubId, id)
|
||||
_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
|
||||
if err != cache.ErrNoMatchingOp {
|
||||
return err
|
||||
}
|
||||
return b.OpenRaw(
|
||||
gi.makePerson(item.ReopenedEvent.Actor),
|
||||
author, err := gi.ensurePerson(repo, item.ReopenedEvent.Actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = b.OpenRaw(
|
||||
author,
|
||||
item.ReopenedEvent.CreatedAt.Unix(),
|
||||
map[string]string{keyGithubId: id},
|
||||
)
|
||||
return err
|
||||
|
||||
case "RenamedTitleEvent":
|
||||
id := parseId(item.RenamedTitleEvent.Id)
|
||||
_, err := b.ResolveTargetWithMetadata(keyGithubId, id)
|
||||
_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
|
||||
if err != cache.ErrNoMatchingOp {
|
||||
return err
|
||||
}
|
||||
return b.SetTitleRaw(
|
||||
gi.makePerson(item.RenamedTitleEvent.Actor),
|
||||
author, err := gi.ensurePerson(repo, item.RenamedTitleEvent.Actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = b.SetTitleRaw(
|
||||
author,
|
||||
item.RenamedTitleEvent.CreatedAt.Unix(),
|
||||
string(item.RenamedTitleEvent.CurrentTitle),
|
||||
map[string]string{keyGithubId: id},
|
||||
)
|
||||
return err
|
||||
|
||||
default:
|
||||
fmt.Println("ignore event ", item.Typename)
|
||||
@ -386,8 +417,14 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.String, comment issueComment, rootVariables map[string]interface{}) error {
|
||||
target, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(comment.Id))
|
||||
func (gi *githubImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache, cursor githubv4.String, comment issueComment, rootVariables map[string]interface{}) error {
|
||||
author, err := gi.ensurePerson(repo, comment.Author)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var target git.Hash
|
||||
target, err = b.ResolveOperationWithMetadata(keyGithubId, parseId(comment.Id))
|
||||
if err != nil && err != cache.ErrNoMatchingOp {
|
||||
// real error
|
||||
return err
|
||||
@ -406,8 +443,8 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin
|
||||
|
||||
if len(comment.UserContentEdits.Nodes) == 0 {
|
||||
if err == cache.ErrNoMatchingOp {
|
||||
err = b.AddCommentRaw(
|
||||
gi.makePerson(comment.Author),
|
||||
op, err := b.AddCommentRaw(
|
||||
author,
|
||||
comment.CreatedAt.Unix(),
|
||||
cleanupText(string(comment.Body)),
|
||||
nil,
|
||||
@ -415,7 +452,11 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin
|
||||
keyGithubId: parseId(comment.Id),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target, err = op.Hash()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -439,8 +480,8 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin
|
||||
continue
|
||||
}
|
||||
|
||||
err = b.AddCommentRaw(
|
||||
gi.makePerson(comment.Author),
|
||||
op, err := b.AddCommentRaw(
|
||||
author,
|
||||
comment.CreatedAt.Unix(),
|
||||
cleanupText(string(*edit.Diff)),
|
||||
nil,
|
||||
@ -452,9 +493,14 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target, err = op.Hash()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := gi.ensureCommentEdit(b, target, edit)
|
||||
err := gi.ensureCommentEdit(repo, b, target, edit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -496,7 +542,7 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin
|
||||
continue
|
||||
}
|
||||
|
||||
err := gi.ensureCommentEdit(b, target, edit)
|
||||
err := gi.ensureCommentEdit(repo, b, target, edit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -514,18 +560,14 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash, edit userContentEdit) error {
|
||||
func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugCache, target git.Hash, edit userContentEdit) error {
|
||||
if edit.Diff == nil {
|
||||
// this happen if the event is older than early 2018, Github doesn't have the data before that.
|
||||
// Best we can do is to ignore the event.
|
||||
return nil
|
||||
}
|
||||
|
||||
if edit.Editor == nil {
|
||||
return fmt.Errorf("no editor")
|
||||
}
|
||||
|
||||
_, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(edit.Id))
|
||||
_, err := b.ResolveOperationWithMetadata(keyGithubId, parseId(edit.Id))
|
||||
if err == nil {
|
||||
// already imported
|
||||
return nil
|
||||
@ -537,14 +579,19 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash,
|
||||
|
||||
fmt.Println("import edition")
|
||||
|
||||
editor, err := gi.ensurePerson(repo, edit.Editor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case edit.DeletedAt != nil:
|
||||
// comment deletion, not supported yet
|
||||
|
||||
case edit.DeletedAt == nil:
|
||||
// comment edition
|
||||
err := b.EditCommentRaw(
|
||||
gi.makePerson(edit.Editor),
|
||||
_, err := b.EditCommentRaw(
|
||||
editor,
|
||||
edit.CreatedAt.Unix(),
|
||||
target,
|
||||
cleanupText(string(*edit.Diff)),
|
||||
@ -560,11 +607,23 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash,
|
||||
return nil
|
||||
}
|
||||
|
||||
// makePerson create a bug.Person from the Github data
|
||||
func (gi *githubImporter) makePerson(actor *actor) bug.Person {
|
||||
// ensurePerson create a bug.Person from the Github data
|
||||
func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*cache.IdentityCache, error) {
|
||||
// When a user has been deleted, Github return a null actor, while displaying a profile named "ghost"
|
||||
// in it's UI. So we need a special case to get it.
|
||||
if actor == nil {
|
||||
return gi.ghost
|
||||
return gi.getGhost(repo)
|
||||
}
|
||||
|
||||
// Look first in the cache
|
||||
i, err := repo.ResolveIdentityImmutableMetadata(keyGithubLogin, string(actor.Login))
|
||||
if err == nil {
|
||||
return i, nil
|
||||
}
|
||||
if _, ok := err.(identity.ErrMultipleMatch); ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var name string
|
||||
var email string
|
||||
|
||||
@ -584,24 +643,36 @@ func (gi *githubImporter) makePerson(actor *actor) bug.Person {
|
||||
case "Bot":
|
||||
}
|
||||
|
||||
return bug.Person{
|
||||
Name: name,
|
||||
Email: email,
|
||||
Login: string(actor.Login),
|
||||
AvatarUrl: string(actor.AvatarUrl),
|
||||
}
|
||||
return repo.NewIdentityRaw(
|
||||
name,
|
||||
email,
|
||||
string(actor.Login),
|
||||
string(actor.AvatarUrl),
|
||||
map[string]string{
|
||||
keyGithubLogin: string(actor.Login),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (gi *githubImporter) fetchGhost() error {
|
||||
func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, error) {
|
||||
// Look first in the cache
|
||||
i, err := repo.ResolveIdentityImmutableMetadata(keyGithubLogin, "ghost")
|
||||
if err == nil {
|
||||
return i, nil
|
||||
}
|
||||
if _, ok := err.(identity.ErrMultipleMatch); ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var q userQuery
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"login": githubv4.String("ghost"),
|
||||
}
|
||||
|
||||
err := gi.client.Query(context.TODO(), &q, variables)
|
||||
err = gi.client.Query(context.TODO(), &q, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var name string
|
||||
@ -609,14 +680,15 @@ func (gi *githubImporter) fetchGhost() error {
|
||||
name = string(*q.User.Name)
|
||||
}
|
||||
|
||||
gi.ghost = bug.Person{
|
||||
Name: name,
|
||||
Login: string(q.User.Login),
|
||||
AvatarUrl: string(q.User.AvatarUrl),
|
||||
Email: string(q.User.Email),
|
||||
}
|
||||
|
||||
return nil
|
||||
return repo.NewIdentityRaw(
|
||||
name,
|
||||
string(q.User.Email),
|
||||
string(q.User.Login),
|
||||
string(q.User.AvatarUrl),
|
||||
map[string]string{
|
||||
keyGithubLogin: string(q.User.Login),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// parseId convert the unusable githubv4.ID (an interface{}) into a string
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -20,14 +21,27 @@ func (li *launchpadImporter) Init(conf core.Configuration) error {
|
||||
}
|
||||
|
||||
const keyLaunchpadID = "launchpad-id"
|
||||
const keyLaunchpadLogin = "launchpad-login"
|
||||
|
||||
func (li *launchpadImporter) makePerson(owner LPPerson) bug.Person {
|
||||
return bug.Person{
|
||||
Name: owner.Name,
|
||||
Email: "",
|
||||
Login: owner.Login,
|
||||
AvatarUrl: "",
|
||||
func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson) (*cache.IdentityCache, error) {
|
||||
// Look first in the cache
|
||||
i, err := repo.ResolveIdentityImmutableMetadata(keyLaunchpadLogin, owner.Login)
|
||||
if err == nil {
|
||||
return i, nil
|
||||
}
|
||||
if _, ok := err.(identity.ErrMultipleMatch); ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return repo.NewIdentityRaw(
|
||||
owner.Name,
|
||||
"",
|
||||
owner.Login,
|
||||
"",
|
||||
map[string]string{
|
||||
keyLaunchpadLogin: owner.Login,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error {
|
||||
@ -53,10 +67,15 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error {
|
||||
return err
|
||||
}
|
||||
|
||||
owner, err := li.ensurePerson(repo, lpBug.Owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err == bug.ErrBugNotExist {
|
||||
createdAt, _ := time.Parse(time.RFC3339, lpBug.CreatedAt)
|
||||
b, err = repo.NewBugRaw(
|
||||
li.makePerson(lpBug.Owner),
|
||||
owner,
|
||||
createdAt.Unix(),
|
||||
lpBug.Title,
|
||||
lpBug.Description,
|
||||
@ -81,7 +100,7 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error {
|
||||
// The Launchpad API returns the bug description as the first
|
||||
// comment, so skip it.
|
||||
for _, lpMessage := range lpBug.Messages[1:] {
|
||||
_, err := b.ResolveTargetWithMetadata(keyLaunchpadID, lpMessage.ID)
|
||||
_, err := b.ResolveOperationWithMetadata(keyLaunchpadID, lpMessage.ID)
|
||||
if err != nil && err != cache.ErrNoMatchingOp {
|
||||
return errors.Wrapf(err, "failed to fetch comments for bug #%s", lpBugID)
|
||||
}
|
||||
@ -94,10 +113,15 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error {
|
||||
continue
|
||||
}
|
||||
|
||||
owner, err := li.ensurePerson(repo, lpMessage.Owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This is a new comment, we can add it.
|
||||
createdAt, _ := time.Parse(time.RFC3339, lpMessage.CreatedAt)
|
||||
err = b.AddCommentRaw(
|
||||
li.makePerson(lpMessage.Owner),
|
||||
_, err = b.AddCommentRaw(
|
||||
owner,
|
||||
createdAt.Unix(),
|
||||
lpMessage.Content,
|
||||
nil,
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Package launchad contains the Launchpad bridge implementation
|
||||
// Package launchpad contains the Launchpad bridge implementation
|
||||
package launchpad
|
||||
|
||||
import (
|
||||
|
48
bug/bug.go
48
bug/bug.go
@ -6,10 +6,12 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/MichaelMure/git-bug/util/lamport"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const bugsRefPattern = "refs/bugs/"
|
||||
@ -113,13 +115,6 @@ func ReadRemoteBug(repo repository.ClockedRepo, remote string, id string) (*Bug,
|
||||
|
||||
// readBug will read and parse a Bug from git
|
||||
func readBug(repo repository.ClockedRepo, ref string) (*Bug, error) {
|
||||
hashes, err := repo.ListCommits(ref)
|
||||
|
||||
// TODO: this is not perfect, it might be a command invoke error
|
||||
if err != nil {
|
||||
return nil, ErrBugNotExist
|
||||
}
|
||||
|
||||
refSplit := strings.Split(ref, "/")
|
||||
id := refSplit[len(refSplit)-1]
|
||||
|
||||
@ -127,6 +122,13 @@ func readBug(repo repository.ClockedRepo, ref string) (*Bug, error) {
|
||||
return nil, fmt.Errorf("invalid ref length")
|
||||
}
|
||||
|
||||
hashes, err := repo.ListCommits(ref)
|
||||
|
||||
// TODO: this is not perfect, it might be a command invoke error
|
||||
if err != nil {
|
||||
return nil, ErrBugNotExist
|
||||
}
|
||||
|
||||
bug := Bug{
|
||||
id: id,
|
||||
}
|
||||
@ -217,6 +219,13 @@ func readBug(repo repository.ClockedRepo, ref string) (*Bug, error) {
|
||||
bug.packs = append(bug.packs, *opp)
|
||||
}
|
||||
|
||||
// Make sure that the identities are properly loaded
|
||||
resolver := identity.NewSimpleResolver(repo)
|
||||
err = bug.EnsureIdentities(resolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &bug, nil
|
||||
}
|
||||
|
||||
@ -312,6 +321,11 @@ func (bug *Bug) Validate() error {
|
||||
return fmt.Errorf("first operation should be a Create op")
|
||||
}
|
||||
|
||||
// The bug ID should be the hash of the first commit
|
||||
if len(bug.packs) > 0 && string(bug.packs[0].commitHash) != bug.id {
|
||||
return fmt.Errorf("bug id should be the first commit hash")
|
||||
}
|
||||
|
||||
// Check that there is no more CreateOp op
|
||||
it := NewOperationIterator(bug)
|
||||
createCount := 0
|
||||
@ -340,7 +354,8 @@ func (bug *Bug) HasPendingOp() bool {
|
||||
|
||||
// Commit write the staging area in Git and move the operations to the packs
|
||||
func (bug *Bug) Commit(repo repository.ClockedRepo) error {
|
||||
if bug.staging.IsEmpty() {
|
||||
|
||||
if !bug.NeedCommit() {
|
||||
return fmt.Errorf("can't commit a bug with no pending operation")
|
||||
}
|
||||
|
||||
@ -450,12 +465,24 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
|
||||
return err
|
||||
}
|
||||
|
||||
bug.staging.commitHash = hash
|
||||
bug.packs = append(bug.packs, bug.staging)
|
||||
bug.staging = OperationPack{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bug *Bug) CommitAsNeeded(repo repository.ClockedRepo) error {
|
||||
if !bug.NeedCommit() {
|
||||
return nil
|
||||
}
|
||||
return bug.Commit(repo)
|
||||
}
|
||||
|
||||
func (bug *Bug) NeedCommit() bool {
|
||||
return !bug.staging.IsEmpty()
|
||||
}
|
||||
|
||||
func makeMediaTree(pack OperationPack) []repository.TreeEntry {
|
||||
var tree []repository.TreeEntry
|
||||
counter := 0
|
||||
@ -504,9 +531,8 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) {
|
||||
}
|
||||
|
||||
ancestor, err := repo.FindCommonAncestor(bug.lastCommit, otherBug.lastCommit)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, errors.Wrap(err, "can't find common ancestor")
|
||||
}
|
||||
|
||||
ancestorIndex := 0
|
||||
|
@ -4,29 +4,68 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Fetch retrieve update from a remote
|
||||
// Note:
|
||||
//
|
||||
// For the actions (fetch/push/pull/merge/commit), this package act as a master for
|
||||
// the identity package and will also drive the needed identity actions. That is,
|
||||
// if bug.Push() is called, identity.Push will also be called to make sure that
|
||||
// the dependant identities are also present and up to date on the remote.
|
||||
//
|
||||
// I'm not entirely sure this is the correct way to do it, as it might introduce
|
||||
// too much complexity and hard coupling, but it does make this package easier
|
||||
// to use.
|
||||
|
||||
// Fetch retrieve updates from a remote
|
||||
// This does not change the local bugs state
|
||||
func Fetch(repo repository.Repo, remote string) (string, error) {
|
||||
stdout, err := identity.Fetch(repo, remote)
|
||||
if err != nil {
|
||||
return stdout, err
|
||||
}
|
||||
|
||||
remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
|
||||
fetchRefSpec := fmt.Sprintf("%s*:%s*", bugsRefPattern, remoteRefSpec)
|
||||
|
||||
return repo.FetchRefs(remote, fetchRefSpec)
|
||||
stdout2, err := repo.FetchRefs(remote, fetchRefSpec)
|
||||
|
||||
return stdout + "\n" + stdout2, err
|
||||
}
|
||||
|
||||
// Push update a remote with the local changes
|
||||
func Push(repo repository.Repo, remote string) (string, error) {
|
||||
return repo.PushRefs(remote, bugsRefPattern+"*")
|
||||
stdout, err := identity.Push(repo, remote)
|
||||
if err != nil {
|
||||
return stdout, err
|
||||
}
|
||||
|
||||
stdout2, err := repo.PushRefs(remote, bugsRefPattern+"*")
|
||||
|
||||
return stdout + "\n" + stdout2, err
|
||||
}
|
||||
|
||||
// Pull will do a Fetch + MergeAll
|
||||
// This function won't give details on the underlying process. If you need more
|
||||
// use Fetch and MergeAll separately.
|
||||
// This function will return an error if a merge fail
|
||||
func Pull(repo repository.ClockedRepo, remote string) error {
|
||||
_, err := Fetch(repo, remote)
|
||||
_, err := identity.Fetch(repo, remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for merge := range identity.MergeAll(repo, remote) {
|
||||
if merge.Err != nil {
|
||||
return merge.Err
|
||||
}
|
||||
if merge.Status == identity.MergeStatusInvalid {
|
||||
return errors.Errorf("merge failure: %s", merge.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = Fetch(repo, remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -35,12 +74,21 @@ func Pull(repo repository.ClockedRepo, remote string) error {
|
||||
if merge.Err != nil {
|
||||
return merge.Err
|
||||
}
|
||||
if merge.Status == MergeStatusInvalid {
|
||||
return errors.Errorf("merge failure: %s", merge.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MergeAll will merge all the available remote bug
|
||||
// MergeAll will merge all the available remote bug:
|
||||
//
|
||||
// - If the remote has new commit, the local bug is updated to match the same history
|
||||
// (fast-forward update)
|
||||
// - if the local bug has new commits but the remote don't, nothing is changed
|
||||
// - if both local and remote bug have new commits (that is, we have a concurrent edition),
|
||||
// new local commits are rewritten at the head of the remote history (that is, a rebase)
|
||||
func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
|
||||
out := make(chan MergeResult)
|
||||
|
||||
|
@ -1,93 +1,31 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createRepo(bare bool) *repository.GitRepo {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// fmt.Println("Creating repo:", dir)
|
||||
|
||||
var creator func(string) (*repository.GitRepo, error)
|
||||
|
||||
if bare {
|
||||
creator = repository.InitBareGitRepo
|
||||
} else {
|
||||
creator = repository.InitGitRepo
|
||||
}
|
||||
|
||||
repo, err := creator(dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := repo.StoreConfig("user.name", "testuser"); err != nil {
|
||||
log.Fatal("failed to set user.name for test repository: ", err)
|
||||
}
|
||||
if err := repo.StoreConfig("user.email", "testuser@example.com"); err != nil {
|
||||
log.Fatal("failed to set user.email for test repository: ", err)
|
||||
}
|
||||
|
||||
return repo
|
||||
}
|
||||
|
||||
func cleanupRepo(repo repository.Repo) error {
|
||||
path := repo.GetPath()
|
||||
// fmt.Println("Cleaning repo:", path)
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
|
||||
func setupRepos(t testing.TB) (repoA, repoB, remote *repository.GitRepo) {
|
||||
repoA = createRepo(false)
|
||||
repoB = createRepo(false)
|
||||
remote = createRepo(true)
|
||||
|
||||
remoteAddr := "file://" + remote.GetPath()
|
||||
|
||||
err := repoA.AddRemote("origin", remoteAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = repoB.AddRemote("origin", remoteAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return repoA, repoB, remote
|
||||
}
|
||||
|
||||
func cleanupRepos(repoA, repoB, remote *repository.GitRepo) {
|
||||
cleanupRepo(repoA)
|
||||
cleanupRepo(repoB)
|
||||
cleanupRepo(remote)
|
||||
}
|
||||
|
||||
func TestPushPull(t *testing.T) {
|
||||
repoA, repoB, remote := setupRepos(t)
|
||||
defer cleanupRepos(repoA, repoB, remote)
|
||||
repoA, repoB, remote := test.SetupReposAndRemote(t)
|
||||
defer test.CleanupRepos(repoA, repoB, remote)
|
||||
|
||||
bug1, _, err := Create(rene, unix, "bug1", "message")
|
||||
assert.Nil(t, err)
|
||||
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
|
||||
bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
|
||||
require.NoError(t, err)
|
||||
err = bug1.Commit(repoA)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// A --> remote --> B
|
||||
_, err = Push(repoA, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Pull(repoB, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
bugs := allBugs(t, ReadAllLocalBugs(repoB))
|
||||
|
||||
@ -96,16 +34,19 @@ func TestPushPull(t *testing.T) {
|
||||
}
|
||||
|
||||
// B --> remote --> A
|
||||
bug2, _, err := Create(rene, unix, "bug2", "message")
|
||||
assert.Nil(t, err)
|
||||
reneB, err := identity.ReadLocal(repoA, reneA.Id())
|
||||
require.NoError(t, err)
|
||||
|
||||
bug2, _, err := Create(reneB, time.Now().Unix(), "bug2", "message")
|
||||
require.NoError(t, err)
|
||||
err = bug2.Commit(repoB)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = Push(repoB, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Pull(repoA, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
bugs = allBugs(t, ReadAllLocalBugs(repoA))
|
||||
|
||||
@ -136,41 +77,46 @@ func BenchmarkRebaseTheirs(b *testing.B) {
|
||||
}
|
||||
|
||||
func _RebaseTheirs(t testing.TB) {
|
||||
repoA, repoB, remote := setupRepos(t)
|
||||
defer cleanupRepos(repoA, repoB, remote)
|
||||
repoA, repoB, remote := test.SetupReposAndRemote(t)
|
||||
defer test.CleanupRepos(repoA, repoB, remote)
|
||||
|
||||
bug1, _, err := Create(rene, unix, "bug1", "message")
|
||||
assert.Nil(t, err)
|
||||
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
|
||||
bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
|
||||
require.NoError(t, err)
|
||||
err = bug1.Commit(repoA)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// A --> remote
|
||||
_, err = Push(repoA, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// remote --> B
|
||||
err = Pull(repoB, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
bug2, err := ReadLocalBug(repoB, bug1.Id())
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug2, rene, unix, "message2")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug2, rene, unix, "message3")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug2, rene, unix, "message4")
|
||||
assert.Nil(t, err)
|
||||
reneB, err := identity.ReadLocal(repoA, reneA.Id())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message2")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message3")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message4")
|
||||
require.NoError(t, err)
|
||||
err = bug2.Commit(repoB)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// B --> remote
|
||||
_, err = Push(repoB, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// remote --> A
|
||||
err = Pull(repoA, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
bugs := allBugs(t, ReadAllLocalBugs(repoB))
|
||||
|
||||
@ -179,7 +125,7 @@ func _RebaseTheirs(t testing.TB) {
|
||||
}
|
||||
|
||||
bug3, err := ReadLocalBug(repoA, bug1.Id())
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
if nbOps(bug3) != 4 {
|
||||
t.Fatal("Unexpected number of operations")
|
||||
@ -197,52 +143,54 @@ func BenchmarkRebaseOurs(b *testing.B) {
|
||||
}
|
||||
|
||||
func _RebaseOurs(t testing.TB) {
|
||||
repoA, repoB, remote := setupRepos(t)
|
||||
defer cleanupRepos(repoA, repoB, remote)
|
||||
repoA, repoB, remote := test.SetupReposAndRemote(t)
|
||||
defer test.CleanupRepos(repoA, repoB, remote)
|
||||
|
||||
bug1, _, err := Create(rene, unix, "bug1", "message")
|
||||
assert.Nil(t, err)
|
||||
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
|
||||
bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
|
||||
require.NoError(t, err)
|
||||
err = bug1.Commit(repoA)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// A --> remote
|
||||
_, err = Push(repoA, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// remote --> B
|
||||
err = Pull(repoB, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug1, rene, unix, "message2")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message3")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message4")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message2")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message3")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message4")
|
||||
require.NoError(t, err)
|
||||
err = bug1.Commit(repoA)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug1, rene, unix, "message5")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message6")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message7")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message5")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message6")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message7")
|
||||
require.NoError(t, err)
|
||||
err = bug1.Commit(repoA)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug1, rene, unix, "message8")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message9")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message10")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message8")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message9")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message10")
|
||||
require.NoError(t, err)
|
||||
err = bug1.Commit(repoA)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// remote --> A
|
||||
err = Pull(repoA, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
bugs := allBugs(t, ReadAllLocalBugs(repoA))
|
||||
|
||||
@ -251,7 +199,7 @@ func _RebaseOurs(t testing.TB) {
|
||||
}
|
||||
|
||||
bug2, err := ReadLocalBug(repoA, bug1.Id())
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
if nbOps(bug2) != 10 {
|
||||
t.Fatal("Unexpected number of operations")
|
||||
@ -278,86 +226,91 @@ func BenchmarkRebaseConflict(b *testing.B) {
|
||||
}
|
||||
|
||||
func _RebaseConflict(t testing.TB) {
|
||||
repoA, repoB, remote := setupRepos(t)
|
||||
defer cleanupRepos(repoA, repoB, remote)
|
||||
repoA, repoB, remote := test.SetupReposAndRemote(t)
|
||||
defer test.CleanupRepos(repoA, repoB, remote)
|
||||
|
||||
bug1, _, err := Create(rene, unix, "bug1", "message")
|
||||
assert.Nil(t, err)
|
||||
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
|
||||
bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
|
||||
require.NoError(t, err)
|
||||
err = bug1.Commit(repoA)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// A --> remote
|
||||
_, err = Push(repoA, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// remote --> B
|
||||
err = Pull(repoB, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug1, rene, unix, "message2")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message3")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message4")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message2")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message3")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message4")
|
||||
require.NoError(t, err)
|
||||
err = bug1.Commit(repoA)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug1, rene, unix, "message5")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message6")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message7")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message5")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message6")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message7")
|
||||
require.NoError(t, err)
|
||||
err = bug1.Commit(repoA)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug1, rene, unix, "message8")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message9")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, rene, unix, "message10")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message8")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message9")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug1, reneA, time.Now().Unix(), "message10")
|
||||
require.NoError(t, err)
|
||||
err = bug1.Commit(repoA)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
bug2, err := ReadLocalBug(repoB, bug1.Id())
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug2, rene, unix, "message11")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug2, rene, unix, "message12")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug2, rene, unix, "message13")
|
||||
assert.Nil(t, err)
|
||||
err = bug2.Commit(repoB)
|
||||
assert.Nil(t, err)
|
||||
reneB, err := identity.ReadLocal(repoA, reneA.Id())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug2, rene, unix, "message14")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug2, rene, unix, "message15")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug2, rene, unix, "message16")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message11")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message12")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message13")
|
||||
require.NoError(t, err)
|
||||
err = bug2.Commit(repoB)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug2, rene, unix, "message17")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug2, rene, unix, "message18")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug2, rene, unix, "message19")
|
||||
assert.Nil(t, err)
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message14")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message15")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message16")
|
||||
require.NoError(t, err)
|
||||
err = bug2.Commit(repoB)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message17")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message18")
|
||||
require.NoError(t, err)
|
||||
_, err = AddComment(bug2, reneB, time.Now().Unix(), "message19")
|
||||
require.NoError(t, err)
|
||||
err = bug2.Commit(repoB)
|
||||
require.NoError(t, err)
|
||||
|
||||
// A --> remote
|
||||
_, err = Push(repoA, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// remote --> B
|
||||
err = Pull(repoB, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
bugs := allBugs(t, ReadAllLocalBugs(repoB))
|
||||
|
||||
@ -366,7 +319,7 @@ func _RebaseConflict(t testing.TB) {
|
||||
}
|
||||
|
||||
bug3, err := ReadLocalBug(repoB, bug1.Id())
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
if nbOps(bug3) != 19 {
|
||||
t.Fatal("Unexpected number of operations")
|
||||
@ -374,11 +327,11 @@ func _RebaseConflict(t testing.TB) {
|
||||
|
||||
// B --> remote
|
||||
_, err = Push(repoB, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// remote --> A
|
||||
err = Pull(repoA, "origin")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
bugs = allBugs(t, ReadAllLocalBugs(repoA))
|
||||
|
||||
@ -387,7 +340,7 @@ func _RebaseConflict(t testing.TB) {
|
||||
}
|
||||
|
||||
bug4, err := ReadLocalBug(repoA, bug1.Id())
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
if nbOps(bug4) != 19 {
|
||||
t.Fatal("Unexpected number of operations")
|
||||
|
@ -1,11 +1,12 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/go-test/deep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBugId(t *testing.T) {
|
||||
@ -13,6 +14,9 @@ func TestBugId(t *testing.T) {
|
||||
|
||||
bug1 := NewBug()
|
||||
|
||||
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
|
||||
|
||||
bug1.Append(createOp)
|
||||
|
||||
err := bug1.Commit(mockRepo)
|
||||
@ -29,6 +33,9 @@ func TestBugValidity(t *testing.T) {
|
||||
|
||||
bug1 := NewBug()
|
||||
|
||||
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
|
||||
|
||||
if bug1.Validate() == nil {
|
||||
t.Fatal("Empty bug should be invalid")
|
||||
}
|
||||
@ -56,9 +63,14 @@ func TestBugValidity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBugSerialisation(t *testing.T) {
|
||||
func TestBugCommitLoad(t *testing.T) {
|
||||
bug1 := NewBug()
|
||||
|
||||
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
|
||||
setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1")
|
||||
addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil)
|
||||
|
||||
bug1.Append(createOp)
|
||||
bug1.Append(setTitleOp)
|
||||
bug1.Append(setTitleOp)
|
||||
@ -70,25 +82,30 @@ func TestBugSerialisation(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
|
||||
bug2, err := ReadLocalBug(repo, bug1.Id())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
equivalentBug(t, bug1, bug2)
|
||||
|
||||
// ignore some fields
|
||||
bug2.packs[0].commitHash = bug1.packs[0].commitHash
|
||||
for i := range bug1.packs[0].Operations {
|
||||
bug2.packs[0].Operations[i].base().hash = bug1.packs[0].Operations[i].base().hash
|
||||
}
|
||||
// add more op
|
||||
|
||||
// check hashes
|
||||
for i := range bug1.packs[0].Operations {
|
||||
if !bug2.packs[0].Operations[i].base().hash.IsValid() {
|
||||
t.Fatal("invalid hash")
|
||||
bug1.Append(setTitleOp)
|
||||
bug1.Append(addCommentOp)
|
||||
|
||||
err = bug1.Commit(repo)
|
||||
assert.Nil(t, err)
|
||||
|
||||
bug3, err := ReadLocalBug(repo, bug1.Id())
|
||||
assert.NoError(t, err)
|
||||
equivalentBug(t, bug1, bug3)
|
||||
}
|
||||
|
||||
func equivalentBug(t *testing.T, expected, actual *Bug) {
|
||||
assert.Equal(t, len(expected.packs), len(actual.packs))
|
||||
|
||||
for i := range expected.packs {
|
||||
for j := range expected.packs[i].Operations {
|
||||
actual.packs[i].Operations[j].base().hash = expected.packs[i].Operations[j].base().hash
|
||||
}
|
||||
}
|
||||
|
||||
deep.CompareUnexportedFields = true
|
||||
if diff := deep.Equal(bug1, bug2); diff != nil {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
// Comment represent a comment in a Bug
|
||||
type Comment struct {
|
||||
Author Person
|
||||
Author identity.Interface
|
||||
Message string
|
||||
Files []git.Hash
|
||||
|
||||
// Creation time of the comment.
|
||||
// Should be used only for human display, never for ordering as we can't rely on it in a distributed system.
|
||||
UnixTime Timestamp
|
||||
UnixTime timestamp.Timestamp
|
||||
}
|
||||
|
||||
// FormatTimeRel format the UnixTime of the comment for human consumption
|
||||
|
27
bug/identity.go
Normal file
27
bug/identity.go
Normal file
@ -0,0 +1,27 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
)
|
||||
|
||||
// EnsureIdentities walk the graph of operations and make sure that all Identity
|
||||
// are properly loaded. That is, it replace all the IdentityStub with the full
|
||||
// Identity, loaded through a Resolver.
|
||||
func (bug *Bug) EnsureIdentities(resolver identity.Resolver) error {
|
||||
it := NewOperationIterator(bug)
|
||||
|
||||
for it.Next() {
|
||||
op := it.Value()
|
||||
base := op.base()
|
||||
|
||||
if stub, ok := base.Author.(*identity.IdentityStub); ok {
|
||||
i, err := resolver.ResolveIdentity(stub.Id())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
base.Author = i
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -28,7 +28,7 @@ func (l *Label) UnmarshalGQL(v interface{}) error {
|
||||
|
||||
// MarshalGQL implements the graphql.Marshaler interface
|
||||
func (l Label) MarshalGQL(w io.Writer) {
|
||||
w.Write([]byte(`"` + l.String() + `"`))
|
||||
_, _ = w.Write([]byte(`"` + l.String() + `"`))
|
||||
}
|
||||
|
||||
func (l Label) Validate() error {
|
||||
|
@ -1,10 +1,13 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/MichaelMure/git-bug/util/text"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
)
|
||||
|
||||
var _ Operation = &AddCommentOperation{}
|
||||
@ -12,9 +15,9 @@ var _ Operation = &AddCommentOperation{}
|
||||
// AddCommentOperation will add a new comment in the bug
|
||||
type AddCommentOperation struct {
|
||||
OpBase
|
||||
Message string `json:"message"`
|
||||
Message string
|
||||
// TODO: change for a map[string]util.hash to store the filename ?
|
||||
Files []git.Hash `json:"files"`
|
||||
Files []git.Hash
|
||||
}
|
||||
|
||||
func (op *AddCommentOperation) base() *OpBase {
|
||||
@ -30,7 +33,7 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
|
||||
Message: op.Message,
|
||||
Author: op.Author,
|
||||
Files: op.Files,
|
||||
UnixTime: Timestamp(op.UnixTime),
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
}
|
||||
|
||||
snapshot.Comments = append(snapshot.Comments, comment)
|
||||
@ -65,10 +68,58 @@ func (op *AddCommentOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *AddCommentOperation) MarshalJSON() ([]byte, error) {
|
||||
base, err := json.Marshal(op.OpBase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// revert back to a flat map to be able to add our own fields
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(base, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data["message"] = op.Message
|
||||
data["files"] = op.Files
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *AddCommentOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Message string `json:"message"`
|
||||
Files []git.Hash `json:"files"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Message = aux.Message
|
||||
op.Files = aux.Files
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *AddCommentOperation) IsAuthored() {}
|
||||
|
||||
func NewAddCommentOp(author Person, unixTime int64, message string, files []git.Hash) *AddCommentOperation {
|
||||
func NewAddCommentOp(author identity.Interface, unixTime int64, message string, files []git.Hash) *AddCommentOperation {
|
||||
return &AddCommentOperation{
|
||||
OpBase: newOpBase(AddCommentOp, author, unixTime),
|
||||
Message: message,
|
||||
@ -82,11 +133,11 @@ type AddCommentTimelineItem struct {
|
||||
}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func AddComment(b Interface, author Person, unixTime int64, message string) (*AddCommentOperation, error) {
|
||||
func AddComment(b Interface, author identity.Interface, unixTime int64, message string) (*AddCommentOperation, error) {
|
||||
return AddCommentWithFiles(b, author, unixTime, message, nil)
|
||||
}
|
||||
|
||||
func AddCommentWithFiles(b Interface, author Person, unixTime int64, message string, files []git.Hash) (*AddCommentOperation, error) {
|
||||
func AddCommentWithFiles(b Interface, author identity.Interface, unixTime int64, message string, files []git.Hash) (*AddCommentOperation, error) {
|
||||
addCommentOp := NewAddCommentOp(author, unixTime, message, files)
|
||||
if err := addCommentOp.Validate(); err != nil {
|
||||
return nil, err
|
||||
|
25
bug/op_add_comment_test.go
Normal file
25
bug/op_add_comment_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddCommentSerialize(t *testing.T) {
|
||||
var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
before := NewAddCommentOp(rene, unix, "message", nil)
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var after AddCommentOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, before, &after)
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/MichaelMure/git-bug/util/text"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
)
|
||||
|
||||
var _ Operation = &CreateOperation{}
|
||||
@ -13,9 +16,9 @@ var _ Operation = &CreateOperation{}
|
||||
// CreateOperation define the initial creation of a bug
|
||||
type CreateOperation struct {
|
||||
OpBase
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Files []git.Hash `json:"files"`
|
||||
Title string
|
||||
Message string
|
||||
Files []git.Hash
|
||||
}
|
||||
|
||||
func (op *CreateOperation) base() *OpBase {
|
||||
@ -32,7 +35,7 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) {
|
||||
comment := Comment{
|
||||
Message: op.Message,
|
||||
Author: op.Author,
|
||||
UnixTime: Timestamp(op.UnixTime),
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
}
|
||||
|
||||
snapshot.Comments = []Comment{comment}
|
||||
@ -81,10 +84,61 @@ func (op *CreateOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *CreateOperation) MarshalJSON() ([]byte, error) {
|
||||
base, err := json.Marshal(op.OpBase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// revert back to a flat map to be able to add our own fields
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(base, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data["title"] = op.Title
|
||||
data["message"] = op.Message
|
||||
data["files"] = op.Files
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *CreateOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Files []git.Hash `json:"files"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Title = aux.Title
|
||||
op.Message = aux.Message
|
||||
op.Files = aux.Files
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *CreateOperation) IsAuthored() {}
|
||||
|
||||
func NewCreateOp(author Person, unixTime int64, title, message string, files []git.Hash) *CreateOperation {
|
||||
func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []git.Hash) *CreateOperation {
|
||||
return &CreateOperation{
|
||||
OpBase: newOpBase(CreateOp, author, unixTime),
|
||||
Title: title,
|
||||
@ -99,11 +153,11 @@ type CreateTimelineItem struct {
|
||||
}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func Create(author Person, unixTime int64, title, message string) (*Bug, *CreateOperation, error) {
|
||||
func Create(author identity.Interface, unixTime int64, title, message string) (*Bug, *CreateOperation, error) {
|
||||
return CreateWithFiles(author, unixTime, title, message, nil)
|
||||
}
|
||||
|
||||
func CreateWithFiles(author Person, unixTime int64, title, message string, files []git.Hash) (*Bug, *CreateOperation, error) {
|
||||
func CreateWithFiles(author identity.Interface, unixTime int64, title, message string, files []git.Hash) (*Bug, *CreateOperation, error) {
|
||||
newBug := NewBug()
|
||||
createOp := NewCreateOp(author, unixTime, title, message, files)
|
||||
|
||||
|
@ -1,20 +1,19 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
snapshot := Snapshot{}
|
||||
|
||||
var rene = Person{
|
||||
Name: "René Descartes",
|
||||
Email: "rene@descartes.fr",
|
||||
}
|
||||
|
||||
rene := identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
|
||||
create := NewCreateOp(rene, unix, "title", "message", nil)
|
||||
@ -22,11 +21,9 @@ func TestCreate(t *testing.T) {
|
||||
create.Apply(&snapshot)
|
||||
|
||||
hash, err := create.Hash()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
comment := Comment{Author: rene, Message: "message", UnixTime: Timestamp(create.UnixTime)}
|
||||
comment := Comment{Author: rene, Message: "message", UnixTime: timestamp.Timestamp(create.UnixTime)}
|
||||
|
||||
expected := Snapshot{
|
||||
Title: "title",
|
||||
@ -42,8 +39,20 @@ func TestCreate(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
deep.CompareUnexportedFields = true
|
||||
if diff := deep.Equal(snapshot, expected); diff != nil {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
assert.Equal(t, expected, snapshot)
|
||||
}
|
||||
|
||||
func TestCreateSerialize(t *testing.T) {
|
||||
var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
before := NewCreateOp(rene, unix, "title", "message", nil)
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var after CreateOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, before, &after)
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/MichaelMure/git-bug/util/text"
|
||||
)
|
||||
@ -12,9 +16,9 @@ var _ Operation = &EditCommentOperation{}
|
||||
// EditCommentOperation will change a comment in the bug
|
||||
type EditCommentOperation struct {
|
||||
OpBase
|
||||
Target git.Hash `json:"target"`
|
||||
Message string `json:"message"`
|
||||
Files []git.Hash `json:"files"`
|
||||
Target git.Hash
|
||||
Message string
|
||||
Files []git.Hash
|
||||
}
|
||||
|
||||
func (op *EditCommentOperation) base() *OpBase {
|
||||
@ -55,7 +59,7 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
|
||||
comment := Comment{
|
||||
Message: op.Message,
|
||||
Files: op.Files,
|
||||
UnixTime: Timestamp(op.UnixTime),
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
}
|
||||
|
||||
switch target.(type) {
|
||||
@ -92,10 +96,61 @@ func (op *EditCommentOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *EditCommentOperation) MarshalJSON() ([]byte, error) {
|
||||
base, err := json.Marshal(op.OpBase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// revert back to a flat map to be able to add our own fields
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(base, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data["target"] = op.Target
|
||||
data["message"] = op.Message
|
||||
data["files"] = op.Files
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *EditCommentOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Target git.Hash `json:"target"`
|
||||
Message string `json:"message"`
|
||||
Files []git.Hash `json:"files"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Target = aux.Target
|
||||
op.Message = aux.Message
|
||||
op.Files = aux.Files
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *EditCommentOperation) IsAuthored() {}
|
||||
|
||||
func NewEditCommentOp(author Person, unixTime int64, target git.Hash, message string, files []git.Hash) *EditCommentOperation {
|
||||
func NewEditCommentOp(author identity.Interface, unixTime int64, target git.Hash, message string, files []git.Hash) *EditCommentOperation {
|
||||
return &EditCommentOperation{
|
||||
OpBase: newOpBase(EditCommentOp, author, unixTime),
|
||||
Target: target,
|
||||
@ -105,11 +160,11 @@ func NewEditCommentOp(author Person, unixTime int64, target git.Hash, message st
|
||||
}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func EditComment(b Interface, author Person, unixTime int64, target git.Hash, message string) (*EditCommentOperation, error) {
|
||||
func EditComment(b Interface, author identity.Interface, unixTime int64, target git.Hash, message string) (*EditCommentOperation, error) {
|
||||
return EditCommentWithFiles(b, author, unixTime, target, message, nil)
|
||||
}
|
||||
|
||||
func EditCommentWithFiles(b Interface, author Person, unixTime int64, target git.Hash, message string, files []git.Hash) (*EditCommentOperation, error) {
|
||||
func EditCommentWithFiles(b Interface, author identity.Interface, unixTime int64, target git.Hash, message string, files []git.Hash) (*EditCommentOperation, error) {
|
||||
editCommentOp := NewEditCommentOp(author, unixTime, target, message, files)
|
||||
if err := editCommentOp.Validate(); err != nil {
|
||||
return nil, err
|
||||
|
@ -1,37 +1,32 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gotest.tools/assert"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEdit(t *testing.T) {
|
||||
snapshot := Snapshot{}
|
||||
|
||||
var rene = Person{
|
||||
Name: "René Descartes",
|
||||
Email: "rene@descartes.fr",
|
||||
}
|
||||
|
||||
rene := identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
|
||||
create := NewCreateOp(rene, unix, "title", "create", nil)
|
||||
create.Apply(&snapshot)
|
||||
|
||||
hash1, err := create.Hash()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
comment := NewAddCommentOp(rene, unix, "comment", nil)
|
||||
comment.Apply(&snapshot)
|
||||
|
||||
hash2, err := comment.Hash()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
edit := NewEditCommentOp(rene, unix, hash1, "create edited", nil)
|
||||
edit.Apply(&snapshot)
|
||||
@ -51,3 +46,18 @@ func TestEdit(t *testing.T) {
|
||||
assert.Equal(t, snapshot.Comments[0].Message, "create edited")
|
||||
assert.Equal(t, snapshot.Comments[1].Message, "comment edited")
|
||||
}
|
||||
|
||||
func TestEditCommentSerialize(t *testing.T) {
|
||||
var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
before := NewEditCommentOp(rene, unix, "target", "message", nil)
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var after EditCommentOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, before, &after)
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -13,8 +17,8 @@ var _ Operation = &LabelChangeOperation{}
|
||||
// LabelChangeOperation define a Bug operation to add or remove labels
|
||||
type LabelChangeOperation struct {
|
||||
OpBase
|
||||
Added []Label `json:"added"`
|
||||
Removed []Label `json:"removed"`
|
||||
Added []Label
|
||||
Removed []Label
|
||||
}
|
||||
|
||||
func (op *LabelChangeOperation) base() *OpBase {
|
||||
@ -65,7 +69,7 @@ AddLoop:
|
||||
item := &LabelChangeTimelineItem{
|
||||
hash: hash,
|
||||
Author: op.Author,
|
||||
UnixTime: Timestamp(op.UnixTime),
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
Added: op.Added,
|
||||
Removed: op.Removed,
|
||||
}
|
||||
@ -97,10 +101,58 @@ func (op *LabelChangeOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *LabelChangeOperation) MarshalJSON() ([]byte, error) {
|
||||
base, err := json.Marshal(op.OpBase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// revert back to a flat map to be able to add our own fields
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(base, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data["added"] = op.Added
|
||||
data["removed"] = op.Removed
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Added []Label `json:"added"`
|
||||
Removed []Label `json:"removed"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Added = aux.Added
|
||||
op.Removed = aux.Removed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *LabelChangeOperation) IsAuthored() {}
|
||||
|
||||
func NewLabelChangeOperation(author Person, unixTime int64, added, removed []Label) *LabelChangeOperation {
|
||||
func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
|
||||
return &LabelChangeOperation{
|
||||
OpBase: newOpBase(LabelChangeOp, author, unixTime),
|
||||
Added: added,
|
||||
@ -110,8 +162,8 @@ func NewLabelChangeOperation(author Person, unixTime int64, added, removed []Lab
|
||||
|
||||
type LabelChangeTimelineItem struct {
|
||||
hash git.Hash
|
||||
Author Person
|
||||
UnixTime Timestamp
|
||||
Author identity.Interface
|
||||
UnixTime timestamp.Timestamp
|
||||
Added []Label
|
||||
Removed []Label
|
||||
}
|
||||
@ -121,7 +173,7 @@ func (l LabelChangeTimelineItem) Hash() git.Hash {
|
||||
}
|
||||
|
||||
// ChangeLabels is a convenience function to apply the operation
|
||||
func ChangeLabels(b Interface, author Person, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
|
||||
func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
|
||||
var added, removed []Label
|
||||
var results []LabelChangeResult
|
||||
|
||||
|
25
bug/op_label_change_test.go
Normal file
25
bug/op_label_change_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLabelChangeSerialize(t *testing.T) {
|
||||
var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
before := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var after LabelChangeOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, before, &after)
|
||||
}
|
@ -1,12 +1,17 @@
|
||||
package bug
|
||||
|
||||
import "github.com/MichaelMure/git-bug/util/git"
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
)
|
||||
|
||||
var _ Operation = &NoOpOperation{}
|
||||
|
||||
// NoOpOperation is an operation that does not change the bug state. It can
|
||||
// however be used to store arbitrary metadata in the bug history, for example
|
||||
// to support a bridge feature
|
||||
// to support a bridge feature.
|
||||
type NoOpOperation struct {
|
||||
OpBase
|
||||
}
|
||||
@ -27,17 +32,57 @@ func (op *NoOpOperation) Validate() error {
|
||||
return opBaseValidate(op, NoOpOp)
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *NoOpOperation) MarshalJSON() ([]byte, error) {
|
||||
base, err := json.Marshal(op.OpBase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// revert back to a flat map to be able to add our own fields
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(base, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *NoOpOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct{}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *NoOpOperation) IsAuthored() {}
|
||||
|
||||
func NewNoOpOp(author Person, unixTime int64) *NoOpOperation {
|
||||
func NewNoOpOp(author identity.Interface, unixTime int64) *NoOpOperation {
|
||||
return &NoOpOperation{
|
||||
OpBase: newOpBase(NoOpOp, author, unixTime),
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func NoOp(b Interface, author Person, unixTime int64, metadata map[string]string) (*NoOpOperation, error) {
|
||||
func NoOp(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*NoOpOperation, error) {
|
||||
op := NewNoOpOp(author, unixTime)
|
||||
|
||||
for key, value := range metadata {
|
||||
|
25
bug/op_noop_test.go
Normal file
25
bug/op_noop_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNoopSerialize(t *testing.T) {
|
||||
var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
before := NewNoOpOp(rene, unix)
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var after NoOpOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, before, &after)
|
||||
}
|
@ -1,13 +1,19 @@
|
||||
package bug
|
||||
|
||||
import "github.com/MichaelMure/git-bug/util/git"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
)
|
||||
|
||||
var _ Operation = &SetMetadataOperation{}
|
||||
|
||||
type SetMetadataOperation struct {
|
||||
OpBase
|
||||
Target git.Hash `json:"target"`
|
||||
NewMetadata map[string]string `json:"new_metadata"`
|
||||
Target git.Hash
|
||||
NewMetadata map[string]string
|
||||
}
|
||||
|
||||
func (op *SetMetadataOperation) base() *OpBase {
|
||||
@ -50,13 +56,65 @@ func (op *SetMetadataOperation) Validate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !op.Target.IsValid() {
|
||||
return fmt.Errorf("target hash is invalid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *SetMetadataOperation) MarshalJSON() ([]byte, error) {
|
||||
base, err := json.Marshal(op.OpBase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// revert back to a flat map to be able to add our own fields
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(base, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data["target"] = op.Target
|
||||
data["new_metadata"] = op.NewMetadata
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *SetMetadataOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Target git.Hash `json:"target"`
|
||||
NewMetadata map[string]string `json:"new_metadata"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Target = aux.Target
|
||||
op.NewMetadata = aux.NewMetadata
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *SetMetadataOperation) IsAuthored() {}
|
||||
|
||||
func NewSetMetadataOp(author Person, unixTime int64, target git.Hash, newMetadata map[string]string) *SetMetadataOperation {
|
||||
func NewSetMetadataOp(author identity.Interface, unixTime int64, target git.Hash, newMetadata map[string]string) *SetMetadataOperation {
|
||||
return &SetMetadataOperation{
|
||||
OpBase: newOpBase(SetMetadataOp, author, unixTime),
|
||||
Target: target,
|
||||
@ -65,7 +123,7 @@ func NewSetMetadataOp(author Person, unixTime int64, target git.Hash, newMetadat
|
||||
}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func SetMetadata(b Interface, author Person, unixTime int64, target git.Hash, newMetadata map[string]string) (*SetMetadataOperation, error) {
|
||||
func SetMetadata(b Interface, author identity.Interface, unixTime int64, target git.Hash, newMetadata map[string]string) (*SetMetadataOperation, error) {
|
||||
SetMetadataOp := NewSetMetadataOp(author, unixTime, target, newMetadata)
|
||||
if err := SetMetadataOp.Validate(); err != nil {
|
||||
return nil, err
|
||||
|
@ -1,20 +1,19 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetMetadata(t *testing.T) {
|
||||
snapshot := Snapshot{}
|
||||
|
||||
var rene = Person{
|
||||
Name: "René Descartes",
|
||||
Email: "rene@descartes.fr",
|
||||
}
|
||||
|
||||
rene := identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
|
||||
create := NewCreateOp(rene, unix, "title", "create", nil)
|
||||
@ -23,9 +22,7 @@ func TestSetMetadata(t *testing.T) {
|
||||
snapshot.Operations = append(snapshot.Operations, create)
|
||||
|
||||
hash1, err := create.Hash()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
comment := NewAddCommentOp(rene, unix, "comment", nil)
|
||||
comment.SetMetadata("key2", "value2")
|
||||
@ -33,9 +30,7 @@ func TestSetMetadata(t *testing.T) {
|
||||
snapshot.Operations = append(snapshot.Operations, comment)
|
||||
|
||||
hash2, err := comment.Hash()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
op1 := NewSetMetadataOp(rene, unix, hash1, map[string]string{
|
||||
"key": "override",
|
||||
@ -96,3 +91,21 @@ func TestSetMetadata(t *testing.T) {
|
||||
assert.Equal(t, commentMetadata["key2"], "value2")
|
||||
assert.Equal(t, commentMetadata["key3"], "value3")
|
||||
}
|
||||
|
||||
func TestSetMetadataSerialize(t *testing.T) {
|
||||
var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
before := NewSetMetadataOp(rene, unix, "message", map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
})
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var after SetMetadataOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, before, &after)
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -10,7 +14,7 @@ var _ Operation = &SetStatusOperation{}
|
||||
// SetStatusOperation will change the status of a bug
|
||||
type SetStatusOperation struct {
|
||||
OpBase
|
||||
Status Status `json:"status"`
|
||||
Status Status
|
||||
}
|
||||
|
||||
func (op *SetStatusOperation) base() *OpBase {
|
||||
@ -34,7 +38,7 @@ func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
|
||||
item := &SetStatusTimelineItem{
|
||||
hash: hash,
|
||||
Author: op.Author,
|
||||
UnixTime: Timestamp(op.UnixTime),
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
Status: op.Status,
|
||||
}
|
||||
|
||||
@ -53,10 +57,55 @@ func (op *SetStatusOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *SetStatusOperation) MarshalJSON() ([]byte, error) {
|
||||
base, err := json.Marshal(op.OpBase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// revert back to a flat map to be able to add our own fields
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(base, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data["status"] = op.Status
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *SetStatusOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Status Status `json:"status"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Status = aux.Status
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *SetStatusOperation) IsAuthored() {}
|
||||
|
||||
func NewSetStatusOp(author Person, unixTime int64, status Status) *SetStatusOperation {
|
||||
func NewSetStatusOp(author identity.Interface, unixTime int64, status Status) *SetStatusOperation {
|
||||
return &SetStatusOperation{
|
||||
OpBase: newOpBase(SetStatusOp, author, unixTime),
|
||||
Status: status,
|
||||
@ -65,8 +114,8 @@ func NewSetStatusOp(author Person, unixTime int64, status Status) *SetStatusOper
|
||||
|
||||
type SetStatusTimelineItem struct {
|
||||
hash git.Hash
|
||||
Author Person
|
||||
UnixTime Timestamp
|
||||
Author identity.Interface
|
||||
UnixTime timestamp.Timestamp
|
||||
Status Status
|
||||
}
|
||||
|
||||
@ -75,7 +124,7 @@ func (s SetStatusTimelineItem) Hash() git.Hash {
|
||||
}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func Open(b Interface, author Person, unixTime int64) (*SetStatusOperation, error) {
|
||||
func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
|
||||
op := NewSetStatusOp(author, unixTime, OpenStatus)
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, err
|
||||
@ -85,7 +134,7 @@ func Open(b Interface, author Person, unixTime int64) (*SetStatusOperation, erro
|
||||
}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func Close(b Interface, author Person, unixTime int64) (*SetStatusOperation, error) {
|
||||
func Close(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
|
||||
op := NewSetStatusOp(author, unixTime, ClosedStatus)
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, err
|
||||
|
25
bug/op_set_status_test.go
Normal file
25
bug/op_set_status_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSetStatusSerialize(t *testing.T) {
|
||||
var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
before := NewSetStatusOp(rene, unix, ClosedStatus)
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var after SetStatusOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, before, &after)
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/MichaelMure/git-bug/util/text"
|
||||
)
|
||||
@ -13,8 +17,8 @@ var _ Operation = &SetTitleOperation{}
|
||||
// SetTitleOperation will change the title of a bug
|
||||
type SetTitleOperation struct {
|
||||
OpBase
|
||||
Title string `json:"title"`
|
||||
Was string `json:"was"`
|
||||
Title string
|
||||
Was string
|
||||
}
|
||||
|
||||
func (op *SetTitleOperation) base() *OpBase {
|
||||
@ -38,7 +42,7 @@ func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
|
||||
item := &SetTitleTimelineItem{
|
||||
hash: hash,
|
||||
Author: op.Author,
|
||||
UnixTime: Timestamp(op.UnixTime),
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
Title: op.Title,
|
||||
Was: op.Was,
|
||||
}
|
||||
@ -74,10 +78,58 @@ func (op *SetTitleOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *SetTitleOperation) MarshalJSON() ([]byte, error) {
|
||||
base, err := json.Marshal(op.OpBase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// revert back to a flat map to be able to add our own fields
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(base, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data["title"] = op.Title
|
||||
data["was"] = op.Was
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
||||
// MarshalJSON
|
||||
func (op *SetTitleOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Title string `json:"title"`
|
||||
Was string `json:"was"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Title = aux.Title
|
||||
op.Was = aux.Was
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *SetTitleOperation) IsAuthored() {}
|
||||
|
||||
func NewSetTitleOp(author Person, unixTime int64, title string, was string) *SetTitleOperation {
|
||||
func NewSetTitleOp(author identity.Interface, unixTime int64, title string, was string) *SetTitleOperation {
|
||||
return &SetTitleOperation{
|
||||
OpBase: newOpBase(SetTitleOp, author, unixTime),
|
||||
Title: title,
|
||||
@ -87,8 +139,8 @@ func NewSetTitleOp(author Person, unixTime int64, title string, was string) *Set
|
||||
|
||||
type SetTitleTimelineItem struct {
|
||||
hash git.Hash
|
||||
Author Person
|
||||
UnixTime Timestamp
|
||||
Author identity.Interface
|
||||
UnixTime timestamp.Timestamp
|
||||
Title string
|
||||
Was string
|
||||
}
|
||||
@ -98,7 +150,7 @@ func (s SetTitleTimelineItem) Hash() git.Hash {
|
||||
}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func SetTitle(b Interface, author Person, unixTime int64, title string) (*SetTitleOperation, error) {
|
||||
func SetTitle(b Interface, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) {
|
||||
it := NewOperationIterator(b)
|
||||
|
||||
var lastTitleOp Operation
|
||||
|
25
bug/op_set_title_test.go
Normal file
25
bug/op_set_title_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSetTitleSerialize(t *testing.T) {
|
||||
var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
before := NewSetTitleOp(rene, unix, "title", "was")
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var after SetTitleOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, before, &after)
|
||||
}
|
@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -76,19 +78,19 @@ func hashOperation(op Operation) (git.Hash, error) {
|
||||
|
||||
// OpBase implement the common code for all operations
|
||||
type OpBase struct {
|
||||
OperationType OperationType `json:"type"`
|
||||
Author Person `json:"author"`
|
||||
UnixTime int64 `json:"timestamp"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
OperationType OperationType
|
||||
Author identity.Interface
|
||||
UnixTime int64
|
||||
Metadata map[string]string
|
||||
// Not serialized. Store the op's hash in memory.
|
||||
hash git.Hash
|
||||
// Not serialized. Store the extra metadata compiled from SetMetadataOperation
|
||||
// in memory.
|
||||
// Not serialized. Store the extra metadata in memory,
|
||||
// compiled from SetMetadataOperation.
|
||||
extraMetadata map[string]string
|
||||
}
|
||||
|
||||
// newOpBase is the constructor for an OpBase
|
||||
func newOpBase(opType OperationType, author Person, unixTime int64) OpBase {
|
||||
func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
|
||||
return OpBase{
|
||||
OperationType: opType,
|
||||
Author: author,
|
||||
@ -96,6 +98,46 @@ func newOpBase(opType OperationType, author Person, unixTime int64) OpBase {
|
||||
}
|
||||
}
|
||||
|
||||
func (op OpBase) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
OperationType OperationType `json:"type"`
|
||||
Author identity.Interface `json:"author"`
|
||||
UnixTime int64 `json:"timestamp"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}{
|
||||
OperationType: op.OperationType,
|
||||
Author: op.Author,
|
||||
UnixTime: op.UnixTime,
|
||||
Metadata: op.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
func (op *OpBase) UnmarshalJSON(data []byte) error {
|
||||
aux := struct {
|
||||
OperationType OperationType `json:"type"`
|
||||
Author json.RawMessage `json:"author"`
|
||||
UnixTime int64 `json:"timestamp"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delegate the decoding of the identity
|
||||
author, err := identity.UnmarshalJSON(aux.Author)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OperationType = aux.OperationType
|
||||
op.Author = author
|
||||
op.UnixTime = aux.UnixTime
|
||||
op.Metadata = aux.Metadata
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Time return the time when the operation was added
|
||||
func (op *OpBase) Time() time.Time {
|
||||
return time.Unix(op.UnixTime, 0)
|
||||
@ -117,14 +159,14 @@ func opBaseValidate(op Operation, opType OperationType) error {
|
||||
return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
|
||||
}
|
||||
|
||||
if _, err := op.Hash(); err != nil {
|
||||
return errors.Wrap(err, "op is not serializable")
|
||||
}
|
||||
|
||||
if op.GetUnixTime() == 0 {
|
||||
return fmt.Errorf("time not set")
|
||||
}
|
||||
|
||||
if op.base().Author == nil {
|
||||
return fmt.Errorf("author not set")
|
||||
}
|
||||
|
||||
if err := op.base().Author.Validate(); err != nil {
|
||||
return errors.Wrap(err, "author")
|
||||
}
|
||||
|
@ -1,29 +1,39 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
rene = Person{
|
||||
Name: "René Descartes",
|
||||
Email: "rene@descartes.fr",
|
||||
func ExampleOperationIterator() {
|
||||
b := NewBug()
|
||||
|
||||
// add operations
|
||||
|
||||
it := NewOperationIterator(b)
|
||||
|
||||
for it.Next() {
|
||||
// do something with each operations
|
||||
_ = it.Value()
|
||||
}
|
||||
|
||||
unix = time.Now().Unix()
|
||||
|
||||
createOp = NewCreateOp(rene, unix, "title", "message", nil)
|
||||
setTitleOp = NewSetTitleOp(rene, unix, "title2", "title1")
|
||||
addCommentOp = NewAddCommentOp(rene, unix, "message2", nil)
|
||||
setStatusOp = NewSetStatusOp(rene, unix, ClosedStatus)
|
||||
labelChangeOp = NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
|
||||
)
|
||||
}
|
||||
|
||||
func TestOpIterator(t *testing.T) {
|
||||
mockRepo := repository.NewMockRepoForTest()
|
||||
|
||||
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
|
||||
createOp := NewCreateOp(rene, unix, "title", "message", nil)
|
||||
setTitleOp := NewSetTitleOp(rene, unix, "title2", "title1")
|
||||
addCommentOp := NewAddCommentOp(rene, unix, "message2", nil)
|
||||
setStatusOp := NewSetStatusOp(rene, unix, ClosedStatus)
|
||||
labelChangeOp := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
|
||||
|
||||
bug1 := NewBug()
|
||||
|
||||
// first pack
|
||||
@ -32,13 +42,15 @@ func TestOpIterator(t *testing.T) {
|
||||
bug1.Append(addCommentOp)
|
||||
bug1.Append(setStatusOp)
|
||||
bug1.Append(labelChangeOp)
|
||||
bug1.Commit(mockRepo)
|
||||
err := bug1.Commit(mockRepo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// second pack
|
||||
bug1.Append(setTitleOp)
|
||||
bug1.Append(setTitleOp)
|
||||
bug1.Append(setTitleOp)
|
||||
bug1.Commit(mockRepo)
|
||||
err = bug1.Commit(mockRepo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// staging
|
||||
bug1.Append(setTitleOp)
|
||||
|
@ -20,7 +20,7 @@ const formatVersion = 1
|
||||
type OperationPack struct {
|
||||
Operations []Operation
|
||||
|
||||
// Private field so not serialized by gob
|
||||
// Private field so not serialized
|
||||
commitHash git.Hash
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ func (opp *OperationPack) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// delegate to specialized unmarshal function
|
||||
op, err := opp.unmarshalOp(raw, t.OperationType)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -73,28 +74,36 @@ func (opp *OperationPack) UnmarshalJSON(data []byte) error {
|
||||
|
||||
func (opp *OperationPack) unmarshalOp(raw []byte, _type OperationType) (Operation, error) {
|
||||
switch _type {
|
||||
case CreateOp:
|
||||
op := &CreateOperation{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
return op, err
|
||||
case SetTitleOp:
|
||||
op := &SetTitleOperation{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
return op, err
|
||||
case AddCommentOp:
|
||||
op := &AddCommentOperation{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
return op, err
|
||||
case SetStatusOp:
|
||||
op := &SetStatusOperation{}
|
||||
case CreateOp:
|
||||
op := &CreateOperation{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
return op, err
|
||||
case EditCommentOp:
|
||||
op := &EditCommentOperation{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
return op, err
|
||||
case LabelChangeOp:
|
||||
op := &LabelChangeOperation{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
return op, err
|
||||
case EditCommentOp:
|
||||
op := &EditCommentOperation{}
|
||||
case NoOpOp:
|
||||
op := &NoOpOperation{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
return op, err
|
||||
case SetMetadataOp:
|
||||
op := &SetMetadataOperation{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
return op, err
|
||||
case SetStatusOp:
|
||||
op := &SetStatusOperation{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
return op, err
|
||||
case SetTitleOp:
|
||||
op := &SetTitleOperation{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
return op, err
|
||||
default:
|
||||
@ -129,7 +138,21 @@ func (opp *OperationPack) Validate() error {
|
||||
|
||||
// Write will serialize and store the OperationPack as a git blob and return
|
||||
// its hash
|
||||
func (opp *OperationPack) Write(repo repository.Repo) (git.Hash, error) {
|
||||
func (opp *OperationPack) Write(repo repository.ClockedRepo) (git.Hash, error) {
|
||||
// make sure we don't write invalid data
|
||||
err := opp.Validate()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "validation error")
|
||||
}
|
||||
|
||||
// First, make sure that all the identities are properly Commit as well
|
||||
for _, op := range opp.Operations {
|
||||
err := op.base().Author.CommitAsNeeded(repo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(opp)
|
||||
|
||||
if err != nil {
|
||||
|
@ -3,51 +3,59 @@ package bug
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/go-test/deep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOperationPackSerialize(t *testing.T) {
|
||||
opp := &OperationPack{}
|
||||
|
||||
rene := identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
|
||||
setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1")
|
||||
addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil)
|
||||
setStatusOp := NewSetStatusOp(rene, time.Now().Unix(), ClosedStatus)
|
||||
labelChangeOp := NewLabelChangeOperation(rene, time.Now().Unix(), []Label{"added"}, []Label{"removed"})
|
||||
|
||||
opp.Append(createOp)
|
||||
opp.Append(setTitleOp)
|
||||
opp.Append(addCommentOp)
|
||||
opp.Append(setStatusOp)
|
||||
opp.Append(labelChangeOp)
|
||||
|
||||
opMeta := NewCreateOp(rene, unix, "title", "message", nil)
|
||||
opMeta := NewSetTitleOp(rene, time.Now().Unix(), "title3", "title2")
|
||||
opMeta.SetMetadata("key", "value")
|
||||
opp.Append(opMeta)
|
||||
|
||||
if len(opMeta.Metadata) != 1 {
|
||||
t.Fatal()
|
||||
}
|
||||
assert.Equal(t, 1, len(opMeta.Metadata))
|
||||
|
||||
opFile := NewCreateOp(rene, unix, "title", "message", []git.Hash{
|
||||
opFile := NewAddCommentOp(rene, time.Now().Unix(), "message", []git.Hash{
|
||||
"abcdef",
|
||||
"ghijkl",
|
||||
})
|
||||
opp.Append(opFile)
|
||||
|
||||
if len(opFile.Files) != 2 {
|
||||
t.Fatal()
|
||||
}
|
||||
assert.Equal(t, 2, len(opFile.Files))
|
||||
|
||||
data, err := json.Marshal(opp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
var opp2 *OperationPack
|
||||
err = json.Unmarshal(data, &opp2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
deep.CompareUnexportedFields = false
|
||||
if diff := deep.Equal(opp, opp2); diff != nil {
|
||||
t.Fatal(diff)
|
||||
ensureHash(t, opp)
|
||||
|
||||
assert.Equal(t, opp, opp2)
|
||||
}
|
||||
|
||||
func ensureHash(t *testing.T, opp *OperationPack) {
|
||||
for _, op := range opp.Operations {
|
||||
_, err := op.Hash()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,19 @@ package bug
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/MichaelMure/git-bug/util/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
unix := time.Now().Unix()
|
||||
|
||||
good := []Operation{
|
||||
NewCreateOp(rene, unix, "title", "message", nil),
|
||||
NewSetTitleOp(rene, unix, "title2", "title1"),
|
||||
@ -25,11 +31,11 @@ func TestValidate(t *testing.T) {
|
||||
|
||||
bad := []Operation{
|
||||
// opbase
|
||||
NewSetStatusOp(Person{Name: "", Email: "rene@descartes.fr"}, unix, ClosedStatus),
|
||||
NewSetStatusOp(Person{Name: "René Descartes\u001b", Email: "rene@descartes.fr"}, unix, ClosedStatus),
|
||||
NewSetStatusOp(Person{Name: "René Descartes", Email: "rene@descartes.fr\u001b"}, unix, ClosedStatus),
|
||||
NewSetStatusOp(Person{Name: "René \nDescartes", Email: "rene@descartes.fr"}, unix, ClosedStatus),
|
||||
NewSetStatusOp(Person{Name: "René Descartes", Email: "rene@\ndescartes.fr"}, unix, ClosedStatus),
|
||||
NewSetStatusOp(identity.NewIdentity("", "rene@descartes.fr"), unix, ClosedStatus),
|
||||
NewSetStatusOp(identity.NewIdentity("René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus),
|
||||
NewSetStatusOp(identity.NewIdentity("René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus),
|
||||
NewSetStatusOp(identity.NewIdentity("René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus),
|
||||
NewSetStatusOp(identity.NewIdentity("René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus),
|
||||
&CreateOperation{OpBase: OpBase{
|
||||
Author: rene,
|
||||
UnixTime: 0,
|
||||
@ -63,7 +69,8 @@ func TestValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMetadata(t *testing.T) {
|
||||
op := NewCreateOp(rene, unix, "title", "message", nil)
|
||||
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
op := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
|
||||
|
||||
op.SetMetadata("key", "value")
|
||||
|
||||
@ -75,11 +82,13 @@ func TestMetadata(t *testing.T) {
|
||||
func TestHash(t *testing.T) {
|
||||
repos := []repository.ClockedRepo{
|
||||
repository.NewMockRepoForTest(),
|
||||
createRepo(false),
|
||||
test.CreateRepo(false),
|
||||
}
|
||||
|
||||
for _, repo := range repos {
|
||||
b, op, err := Create(rene, unix, "title", "message")
|
||||
rene := identity.NewBare("René Descartes", "rene@descartes.fr")
|
||||
|
||||
b, op, err := Create(rene, time.Now().Unix(), "title", "message")
|
||||
require.Nil(t, err)
|
||||
|
||||
h1, err := op.Hash()
|
||||
|
@ -1,95 +0,0 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/text"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Login string `json:"login"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
// GetUser will query the repository for user detail and build the corresponding Person
|
||||
func GetUser(repo repository.Repo) (Person, error) {
|
||||
name, err := repo.GetUserName()
|
||||
if err != nil {
|
||||
return Person{}, err
|
||||
}
|
||||
if name == "" {
|
||||
return Person{}, errors.New("User name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`")
|
||||
}
|
||||
|
||||
email, err := repo.GetUserEmail()
|
||||
if err != nil {
|
||||
return Person{}, err
|
||||
}
|
||||
if email == "" {
|
||||
return Person{}, errors.New("User name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`")
|
||||
}
|
||||
|
||||
return Person{Name: name, Email: email}, nil
|
||||
}
|
||||
|
||||
// Match tell is the Person match the given query string
|
||||
func (p Person) Match(query string) bool {
|
||||
query = strings.ToLower(query)
|
||||
|
||||
return strings.Contains(strings.ToLower(p.Name), query) ||
|
||||
strings.Contains(strings.ToLower(p.Login), query)
|
||||
}
|
||||
|
||||
func (p Person) Validate() error {
|
||||
if text.Empty(p.Name) && text.Empty(p.Login) {
|
||||
return fmt.Errorf("either name or login should be set")
|
||||
}
|
||||
|
||||
if strings.Contains(p.Name, "\n") {
|
||||
return fmt.Errorf("name should be a single line")
|
||||
}
|
||||
|
||||
if !text.Safe(p.Name) {
|
||||
return fmt.Errorf("name is not fully printable")
|
||||
}
|
||||
|
||||
if strings.Contains(p.Login, "\n") {
|
||||
return fmt.Errorf("login should be a single line")
|
||||
}
|
||||
|
||||
if !text.Safe(p.Login) {
|
||||
return fmt.Errorf("login is not fully printable")
|
||||
}
|
||||
|
||||
if strings.Contains(p.Email, "\n") {
|
||||
return fmt.Errorf("email should be a single line")
|
||||
}
|
||||
|
||||
if !text.Safe(p.Email) {
|
||||
return fmt.Errorf("email is not fully printable")
|
||||
}
|
||||
|
||||
if p.AvatarUrl != "" && !text.ValidUrl(p.AvatarUrl) {
|
||||
return fmt.Errorf("avatarUrl is not a valid URL")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Person) DisplayName() string {
|
||||
switch {
|
||||
case p.Name == "" && p.Login != "":
|
||||
return p.Login
|
||||
case p.Name != "" && p.Login == "":
|
||||
return p.Name
|
||||
case p.Name != "" && p.Login != "":
|
||||
return fmt.Sprintf("%s (%s)", p.Name, p.Login)
|
||||
}
|
||||
|
||||
panic("invalid person data")
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
)
|
||||
|
||||
@ -15,7 +16,7 @@ type Snapshot struct {
|
||||
Title string
|
||||
Comments []Comment
|
||||
Labels []Label
|
||||
Author Person
|
||||
Author identity.Interface
|
||||
CreatedAt time.Time
|
||||
|
||||
Timeline []TimelineItem
|
||||
|
@ -3,7 +3,9 @@ package bug
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
)
|
||||
|
||||
type TimelineItem interface {
|
||||
@ -15,20 +17,20 @@ type TimelineItem interface {
|
||||
type CommentHistoryStep struct {
|
||||
// The author of the edition, not necessarily the same as the author of the
|
||||
// original comment
|
||||
Author Person
|
||||
Author identity.Interface
|
||||
// The new message
|
||||
Message string
|
||||
UnixTime Timestamp
|
||||
UnixTime timestamp.Timestamp
|
||||
}
|
||||
|
||||
// CommentTimelineItem is a TimelineItem that holds a Comment and its edition history
|
||||
type CommentTimelineItem struct {
|
||||
hash git.Hash
|
||||
Author Person
|
||||
Author identity.Interface
|
||||
Message string
|
||||
Files []git.Hash
|
||||
CreatedAt Timestamp
|
||||
LastEdit Timestamp
|
||||
CreatedAt timestamp.Timestamp
|
||||
LastEdit timestamp.Timestamp
|
||||
History []CommentHistoryStep
|
||||
}
|
||||
|
||||
|
109
cache/bug_cache.go
vendored
109
cache/bug_cache.go
vendored
@ -9,6 +9,10 @@ import (
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
)
|
||||
|
||||
// BugCache is a wrapper around a Bug. It provide multiple functions:
|
||||
//
|
||||
// 1. Provide a higher level API to use than the raw API from Bug.
|
||||
// 2. Maintain an up to date Snapshot available.
|
||||
type BugCache struct {
|
||||
repoCache *RepoCache
|
||||
bug *bug.WithSnapshot
|
||||
@ -53,8 +57,8 @@ func (e ErrMultipleMatchOp) Error() string {
|
||||
return fmt.Sprintf("Multiple matching operation found:\n%s", strings.Join(casted, "\n"))
|
||||
}
|
||||
|
||||
// ResolveTargetWithMetadata will find an operation that has the matching metadata
|
||||
func (c *BugCache) ResolveTargetWithMetadata(key string, value string) (git.Hash, error) {
|
||||
// ResolveOperationWithMetadata will find an operation that has the matching metadata
|
||||
func (c *BugCache) ResolveOperationWithMetadata(key string, value string) (git.Hash, error) {
|
||||
// preallocate but empty
|
||||
matching := make([]git.Hash, 0, 5)
|
||||
|
||||
@ -82,45 +86,45 @@ func (c *BugCache) ResolveTargetWithMetadata(key string, value string) (git.Hash
|
||||
return matching[0], nil
|
||||
}
|
||||
|
||||
func (c *BugCache) AddComment(message string) error {
|
||||
func (c *BugCache) AddComment(message string) (*bug.AddCommentOperation, error) {
|
||||
return c.AddCommentWithFiles(message, nil)
|
||||
}
|
||||
|
||||
func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error {
|
||||
author, err := bug.GetUser(c.repoCache.repo)
|
||||
func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) (*bug.AddCommentOperation, error) {
|
||||
author, err := c.repoCache.GetUserIdentity()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.AddCommentRaw(author, time.Now().Unix(), message, files, nil)
|
||||
}
|
||||
|
||||
func (c *BugCache) AddCommentRaw(author bug.Person, unixTime int64, message string, files []git.Hash, metadata map[string]string) error {
|
||||
op, err := bug.AddCommentWithFiles(c.bug, author, unixTime, message, files)
|
||||
func (c *BugCache) AddCommentRaw(author *IdentityCache, unixTime int64, message string, files []git.Hash, metadata map[string]string) (*bug.AddCommentOperation, error) {
|
||||
op, err := bug.AddCommentWithFiles(c.bug, author.Identity, unixTime, message, files)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
return c.notifyUpdated()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelChangeResult, error) {
|
||||
author, err := bug.GetUser(c.repoCache.repo)
|
||||
func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelChangeResult, *bug.LabelChangeOperation, error) {
|
||||
author, err := c.repoCache.GetUserIdentity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return c.ChangeLabelsRaw(author, time.Now().Unix(), added, removed, nil)
|
||||
}
|
||||
|
||||
func (c *BugCache) ChangeLabelsRaw(author bug.Person, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, error) {
|
||||
changes, op, err := bug.ChangeLabels(c.bug, author, unixTime, added, removed)
|
||||
func (c *BugCache) ChangeLabelsRaw(author *IdentityCache, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, *bug.LabelChangeOperation, error) {
|
||||
changes, op, err := bug.ChangeLabels(c.bug, author.Identity, unixTime, added, removed)
|
||||
if err != nil {
|
||||
return changes, err
|
||||
return changes, nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
@ -129,107 +133,112 @@ func (c *BugCache) ChangeLabelsRaw(author bug.Person, unixTime int64, added []st
|
||||
|
||||
err = c.notifyUpdated()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return changes, nil
|
||||
return changes, op, nil
|
||||
}
|
||||
|
||||
func (c *BugCache) Open() error {
|
||||
author, err := bug.GetUser(c.repoCache.repo)
|
||||
func (c *BugCache) Open() (*bug.SetStatusOperation, error) {
|
||||
author, err := c.repoCache.GetUserIdentity()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.OpenRaw(author, time.Now().Unix(), nil)
|
||||
}
|
||||
|
||||
func (c *BugCache) OpenRaw(author bug.Person, unixTime int64, metadata map[string]string) error {
|
||||
op, err := bug.Open(c.bug, author, unixTime)
|
||||
func (c *BugCache) OpenRaw(author *IdentityCache, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) {
|
||||
op, err := bug.Open(c.bug, author.Identity, unixTime)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
return c.notifyUpdated()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
func (c *BugCache) Close() error {
|
||||
author, err := bug.GetUser(c.repoCache.repo)
|
||||
func (c *BugCache) Close() (*bug.SetStatusOperation, error) {
|
||||
author, err := c.repoCache.GetUserIdentity()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.CloseRaw(author, time.Now().Unix(), nil)
|
||||
}
|
||||
|
||||
func (c *BugCache) CloseRaw(author bug.Person, unixTime int64, metadata map[string]string) error {
|
||||
op, err := bug.Close(c.bug, author, unixTime)
|
||||
func (c *BugCache) CloseRaw(author *IdentityCache, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) {
|
||||
op, err := bug.Close(c.bug, author.Identity, unixTime)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
return c.notifyUpdated()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
func (c *BugCache) SetTitle(title string) error {
|
||||
author, err := bug.GetUser(c.repoCache.repo)
|
||||
func (c *BugCache) SetTitle(title string) (*bug.SetTitleOperation, error) {
|
||||
author, err := c.repoCache.GetUserIdentity()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.SetTitleRaw(author, time.Now().Unix(), title, nil)
|
||||
}
|
||||
|
||||
func (c *BugCache) SetTitleRaw(author bug.Person, unixTime int64, title string, metadata map[string]string) error {
|
||||
op, err := bug.SetTitle(c.bug, author, unixTime, title)
|
||||
func (c *BugCache) SetTitleRaw(author *IdentityCache, unixTime int64, title string, metadata map[string]string) (*bug.SetTitleOperation, error) {
|
||||
op, err := bug.SetTitle(c.bug, author.Identity, unixTime, title)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
return c.notifyUpdated()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
func (c *BugCache) EditComment(target git.Hash, message string) error {
|
||||
author, err := bug.GetUser(c.repoCache.repo)
|
||||
func (c *BugCache) EditComment(target git.Hash, message string) (*bug.EditCommentOperation, error) {
|
||||
author, err := c.repoCache.GetUserIdentity()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.EditCommentRaw(author, time.Now().Unix(), target, message, nil)
|
||||
}
|
||||
|
||||
func (c *BugCache) EditCommentRaw(author bug.Person, unixTime int64, target git.Hash, message string, metadata map[string]string) error {
|
||||
op, err := bug.EditComment(c.bug, author, unixTime, target, message)
|
||||
func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target git.Hash, message string, metadata map[string]string) (*bug.EditCommentOperation, error) {
|
||||
op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
return c.notifyUpdated()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
func (c *BugCache) Commit() error {
|
||||
return c.bug.Commit(c.repoCache.repo)
|
||||
err := c.bug.Commit(c.repoCache.repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.notifyUpdated()
|
||||
}
|
||||
|
||||
func (c *BugCache) CommitAsNeeded() error {
|
||||
if c.bug.HasPendingOp() {
|
||||
return c.bug.Commit(c.repoCache.repo)
|
||||
err := c.bug.CommitAsNeeded(c.repoCache.repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return c.notifyUpdated()
|
||||
}
|
||||
|
41
cache/bug_excerpt.go
vendored
41
cache/bug_excerpt.go
vendored
@ -4,9 +4,15 @@ import (
|
||||
"encoding/gob"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/lamport"
|
||||
)
|
||||
|
||||
// Package initialisation used to register the type for (de)serialization
|
||||
func init() {
|
||||
gob.Register(BugExcerpt{})
|
||||
}
|
||||
|
||||
// BugExcerpt hold a subset of the bug values to be able to sort and filter bugs
|
||||
// efficiently without having to read and compile each raw bugs.
|
||||
type BugExcerpt struct {
|
||||
@ -18,29 +24,52 @@ type BugExcerpt struct {
|
||||
EditUnixTime int64
|
||||
|
||||
Status bug.Status
|
||||
Author bug.Person
|
||||
Labels []bug.Label
|
||||
|
||||
// If author is identity.Bare, LegacyAuthor is set
|
||||
// If author is identity.Identity, AuthorId is set and data is deported
|
||||
// in a IdentityExcerpt
|
||||
LegacyAuthor LegacyAuthorExcerpt
|
||||
AuthorId string
|
||||
|
||||
CreateMetadata map[string]string
|
||||
}
|
||||
|
||||
// identity.Bare data are directly embedded in the bug excerpt
|
||||
type LegacyAuthorExcerpt struct {
|
||||
Name string
|
||||
Login string
|
||||
}
|
||||
|
||||
func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
|
||||
return &BugExcerpt{
|
||||
e := &BugExcerpt{
|
||||
Id: b.Id(),
|
||||
CreateLamportTime: b.CreateLamportTime(),
|
||||
EditLamportTime: b.EditLamportTime(),
|
||||
CreateUnixTime: b.FirstOp().GetUnixTime(),
|
||||
EditUnixTime: snap.LastEditUnix(),
|
||||
Status: snap.Status,
|
||||
Author: snap.Author,
|
||||
Labels: snap.Labels,
|
||||
CreateMetadata: b.FirstOp().AllMetadata(),
|
||||
}
|
||||
|
||||
switch snap.Author.(type) {
|
||||
case *identity.Identity:
|
||||
e.AuthorId = snap.Author.Id()
|
||||
case *identity.Bare:
|
||||
e.LegacyAuthor = LegacyAuthorExcerpt{
|
||||
Login: snap.Author.Login(),
|
||||
Name: snap.Author.Name(),
|
||||
}
|
||||
default:
|
||||
panic("unhandled identity type")
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// Package initialisation used to register the type for (de)serialization
|
||||
func init() {
|
||||
gob.Register(BugExcerpt{})
|
||||
func (b *BugExcerpt) HumanId() string {
|
||||
return bug.FormatHumanID(b.Id)
|
||||
}
|
||||
|
||||
/*
|
||||
|
49
cache/filter.go
vendored
49
cache/filter.go
vendored
@ -1,11 +1,13 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
)
|
||||
|
||||
// Filter is a functor that match a subset of bugs
|
||||
type Filter func(excerpt *BugExcerpt) bool
|
||||
// Filter is a predicate that match a subset of bugs
|
||||
type Filter func(repoCache *RepoCache, excerpt *BugExcerpt) bool
|
||||
|
||||
// StatusFilter return a Filter that match a bug status
|
||||
func StatusFilter(query string) (Filter, error) {
|
||||
@ -14,21 +16,36 @@ func StatusFilter(query string) (Filter, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(excerpt *BugExcerpt) bool {
|
||||
return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
|
||||
return excerpt.Status == status
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AuthorFilter return a Filter that match a bug author
|
||||
func AuthorFilter(query string) Filter {
|
||||
return func(excerpt *BugExcerpt) bool {
|
||||
return excerpt.Author.Match(query)
|
||||
return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
|
||||
query = strings.ToLower(query)
|
||||
|
||||
// Normal identity
|
||||
if excerpt.AuthorId != "" {
|
||||
author, ok := repoCache.identitiesExcerpts[excerpt.AuthorId]
|
||||
if !ok {
|
||||
panic("missing identity in the cache")
|
||||
}
|
||||
|
||||
return strings.Contains(strings.ToLower(author.Name), query) ||
|
||||
strings.Contains(strings.ToLower(author.Login), query)
|
||||
}
|
||||
|
||||
// Legacy identity support
|
||||
return strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Name), query) ||
|
||||
strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Login), query)
|
||||
}
|
||||
}
|
||||
|
||||
// LabelFilter return a Filter that match a label
|
||||
func LabelFilter(label string) Filter {
|
||||
return func(excerpt *BugExcerpt) bool {
|
||||
return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
|
||||
for _, l := range excerpt.Labels {
|
||||
if string(l) == label {
|
||||
return true
|
||||
@ -40,7 +57,7 @@ func LabelFilter(label string) Filter {
|
||||
|
||||
// NoLabelFilter return a Filter that match the absence of labels
|
||||
func NoLabelFilter() Filter {
|
||||
return func(excerpt *BugExcerpt) bool {
|
||||
return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
|
||||
return len(excerpt.Labels) == 0
|
||||
}
|
||||
}
|
||||
@ -54,20 +71,20 @@ type Filters struct {
|
||||
}
|
||||
|
||||
// Match check if a bug match the set of filters
|
||||
func (f *Filters) Match(excerpt *BugExcerpt) bool {
|
||||
if match := f.orMatch(f.Status, excerpt); !match {
|
||||
func (f *Filters) Match(repoCache *RepoCache, excerpt *BugExcerpt) bool {
|
||||
if match := f.orMatch(f.Status, repoCache, excerpt); !match {
|
||||
return false
|
||||
}
|
||||
|
||||
if match := f.orMatch(f.Author, excerpt); !match {
|
||||
if match := f.orMatch(f.Author, repoCache, excerpt); !match {
|
||||
return false
|
||||
}
|
||||
|
||||
if match := f.orMatch(f.Label, excerpt); !match {
|
||||
if match := f.orMatch(f.Label, repoCache, excerpt); !match {
|
||||
return false
|
||||
}
|
||||
|
||||
if match := f.andMatch(f.NoFilters, excerpt); !match {
|
||||
if match := f.andMatch(f.NoFilters, repoCache, excerpt); !match {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -75,28 +92,28 @@ func (f *Filters) Match(excerpt *BugExcerpt) bool {
|
||||
}
|
||||
|
||||
// Check if any of the filters provided match the bug
|
||||
func (*Filters) orMatch(filters []Filter, excerpt *BugExcerpt) bool {
|
||||
func (*Filters) orMatch(filters []Filter, repoCache *RepoCache, excerpt *BugExcerpt) bool {
|
||||
if len(filters) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
match := false
|
||||
for _, f := range filters {
|
||||
match = match || f(excerpt)
|
||||
match = match || f(repoCache, excerpt)
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
// Check if all of the filters provided match the bug
|
||||
func (*Filters) andMatch(filters []Filter, excerpt *BugExcerpt) bool {
|
||||
func (*Filters) andMatch(filters []Filter, repoCache *RepoCache, excerpt *BugExcerpt) bool {
|
||||
if len(filters) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
match := true
|
||||
for _, f := range filters {
|
||||
match = match && f(excerpt)
|
||||
match = match && f(repoCache, excerpt)
|
||||
}
|
||||
|
||||
return match
|
||||
|
43
cache/identity_cache.go
vendored
Normal file
43
cache/identity_cache.go
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
)
|
||||
|
||||
// IdentityCache is a wrapper around an Identity for caching.
|
||||
type IdentityCache struct {
|
||||
*identity.Identity
|
||||
repoCache *RepoCache
|
||||
}
|
||||
|
||||
func NewIdentityCache(repoCache *RepoCache, id *identity.Identity) *IdentityCache {
|
||||
return &IdentityCache{
|
||||
Identity: id,
|
||||
repoCache: repoCache,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IdentityCache) notifyUpdated() error {
|
||||
return i.repoCache.identityUpdated(i.Identity.Id())
|
||||
}
|
||||
|
||||
func (i *IdentityCache) AddVersion(version *identity.Version) error {
|
||||
i.Identity.AddVersion(version)
|
||||
return i.notifyUpdated()
|
||||
}
|
||||
|
||||
func (i *IdentityCache) Commit() error {
|
||||
err := i.Identity.Commit(i.repoCache.repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.notifyUpdated()
|
||||
}
|
||||
|
||||
func (i *IdentityCache) CommitAsNeeded() error {
|
||||
err := i.Identity.CommitAsNeeded(i.repoCache.repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.notifyUpdated()
|
||||
}
|
70
cache/identity_excerpt.go
vendored
Normal file
70
cache/identity_excerpt.go
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
)
|
||||
|
||||
// Package initialisation used to register the type for (de)serialization
|
||||
func init() {
|
||||
gob.Register(IdentityExcerpt{})
|
||||
}
|
||||
|
||||
// IdentityExcerpt hold a subset of the identity values to be able to sort and
|
||||
// filter identities efficiently without having to read and compile each raw
|
||||
// identity.
|
||||
type IdentityExcerpt struct {
|
||||
Id string
|
||||
|
||||
Name string
|
||||
Login string
|
||||
ImmutableMetadata map[string]string
|
||||
}
|
||||
|
||||
func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt {
|
||||
return &IdentityExcerpt{
|
||||
Id: i.Id(),
|
||||
Name: i.Name(),
|
||||
Login: i.Login(),
|
||||
ImmutableMetadata: i.ImmutableMetadata(),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IdentityExcerpt) HumanId() string {
|
||||
return identity.FormatHumanID(i.Id)
|
||||
}
|
||||
|
||||
// DisplayName return a non-empty string to display, representing the
|
||||
// identity, based on the non-empty values.
|
||||
func (i *IdentityExcerpt) DisplayName() string {
|
||||
switch {
|
||||
case i.Name == "" && i.Login != "":
|
||||
return i.Login
|
||||
case i.Name != "" && i.Login == "":
|
||||
return i.Name
|
||||
case i.Name != "" && i.Login != "":
|
||||
return fmt.Sprintf("%s (%s)", i.Name, i.Login)
|
||||
}
|
||||
|
||||
panic("invalid person data")
|
||||
}
|
||||
|
||||
/*
|
||||
* Sorting
|
||||
*/
|
||||
|
||||
type IdentityById []*IdentityExcerpt
|
||||
|
||||
func (b IdentityById) Len() int {
|
||||
return len(b)
|
||||
}
|
||||
|
||||
func (b IdentityById) Less(i, j int) bool {
|
||||
return b[i].Id < b[j].Id
|
||||
}
|
||||
|
||||
func (b IdentityById) Swap(i, j int) {
|
||||
b[i], b[j] = b[j], b[i]
|
||||
}
|
1
cache/multi_repo_cache.go
vendored
1
cache/multi_repo_cache.go
vendored
@ -8,6 +8,7 @@ import (
|
||||
|
||||
const lockfile = "lock"
|
||||
|
||||
// MultiRepoCache is the root cache, holding multiple RepoCache.
|
||||
type MultiRepoCache struct {
|
||||
repos map[string]*RepoCache
|
||||
}
|
||||
|
402
cache/repo_cache.go
vendored
402
cache/repo_cache.go
vendored
@ -14,27 +14,64 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/git"
|
||||
"github.com/MichaelMure/git-bug/util/process"
|
||||
)
|
||||
|
||||
const cacheFile = "cache"
|
||||
const formatVersion = 1
|
||||
const bugCacheFile = "bug-cache"
|
||||
const identityCacheFile = "identity-cache"
|
||||
|
||||
// 1: original format
|
||||
// 2: added cache for identities with a reference in the bug cache
|
||||
const formatVersion = 2
|
||||
|
||||
type ErrInvalidCacheFormat struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e ErrInvalidCacheFormat) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// RepoCache is a cache for a Repository. This cache has multiple functions:
|
||||
//
|
||||
// 1. After being loaded, a Bug is kept in memory in the cache, allowing for fast
|
||||
// access later.
|
||||
// 2. The cache maintain on memory and on disk a pre-digested excerpt for each bug,
|
||||
// allowing for fast querying the whole set of bugs without having to load
|
||||
// them individually.
|
||||
// 3. The cache guarantee that a single instance of a Bug is loaded at once, avoiding
|
||||
// loss of data that we could have with multiple copies in the same process.
|
||||
// 4. The same way, the cache maintain in memory a single copy of the loaded identities.
|
||||
//
|
||||
// The cache also protect the on-disk data by locking the git repository for its
|
||||
// own usage, by writing a lock file. Of course, normal git operations are not
|
||||
// affected, only git-bug related one.
|
||||
type RepoCache struct {
|
||||
// the underlying repo
|
||||
repo repository.ClockedRepo
|
||||
|
||||
// excerpt of bugs data for all bugs
|
||||
excerpts map[string]*BugExcerpt
|
||||
bugExcerpts map[string]*BugExcerpt
|
||||
// bug loaded in memory
|
||||
bugs map[string]*BugCache
|
||||
|
||||
// excerpt of identities data for all identities
|
||||
identitiesExcerpts map[string]*IdentityExcerpt
|
||||
// identities loaded in memory
|
||||
identities map[string]*IdentityCache
|
||||
|
||||
// the user identity's id, if known
|
||||
userIdentityId string
|
||||
}
|
||||
|
||||
func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
|
||||
c := &RepoCache{
|
||||
repo: r,
|
||||
bugs: make(map[string]*BugCache),
|
||||
repo: r,
|
||||
bugs: make(map[string]*BugCache),
|
||||
identities: make(map[string]*IdentityCache),
|
||||
}
|
||||
|
||||
err := c.lock()
|
||||
@ -46,6 +83,9 @@ func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
|
||||
if err == nil {
|
||||
return c, nil
|
||||
}
|
||||
if _, ok := err.(ErrInvalidCacheFormat); ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.buildCache()
|
||||
if err != nil {
|
||||
@ -125,14 +165,38 @@ func (c *RepoCache) bugUpdated(id string) error {
|
||||
panic("missing bug in the cache")
|
||||
}
|
||||
|
||||
c.excerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
|
||||
c.bugExcerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
|
||||
|
||||
return c.write()
|
||||
// we only need to write the bug cache
|
||||
return c.writeBugCache()
|
||||
}
|
||||
|
||||
// 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 string) error {
|
||||
i, ok := c.identities[id]
|
||||
if !ok {
|
||||
panic("missing identity in the cache")
|
||||
}
|
||||
|
||||
c.identitiesExcerpts[id] = NewIdentityExcerpt(i.Identity)
|
||||
|
||||
// we only need to write the identity cache
|
||||
return c.writeIdentityCache()
|
||||
}
|
||||
|
||||
// load will try to read from the disk all the cache files
|
||||
func (c *RepoCache) load() error {
|
||||
err := c.loadBugCache()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.loadIdentityCache()
|
||||
}
|
||||
|
||||
// load will try to read from the disk the bug cache file
|
||||
func (c *RepoCache) load() error {
|
||||
f, err := os.Open(cacheFilePath(c.repo))
|
||||
func (c *RepoCache) loadBugCache() error {
|
||||
f, err := os.Open(bugCacheFilePath(c.repo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -149,16 +213,56 @@ func (c *RepoCache) load() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if aux.Version != 1 {
|
||||
return fmt.Errorf("unknown cache format version %v", aux.Version)
|
||||
if aux.Version != 2 {
|
||||
return ErrInvalidCacheFormat{
|
||||
message: fmt.Sprintf("unknown cache format version %v", aux.Version),
|
||||
}
|
||||
}
|
||||
|
||||
c.excerpts = aux.Excerpts
|
||||
c.bugExcerpts = aux.Excerpts
|
||||
return nil
|
||||
}
|
||||
|
||||
// write will serialize on disk the bug cache file
|
||||
// load will try to read from the disk the identity cache file
|
||||
func (c *RepoCache) loadIdentityCache() error {
|
||||
f, err := os.Open(identityCacheFilePath(c.repo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decoder := gob.NewDecoder(f)
|
||||
|
||||
aux := struct {
|
||||
Version uint
|
||||
Excerpts map[string]*IdentityExcerpt
|
||||
}{}
|
||||
|
||||
err = decoder.Decode(&aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if aux.Version != 2 {
|
||||
return ErrInvalidCacheFormat{
|
||||
message: fmt.Sprintf("unknown cache format version %v", aux.Version),
|
||||
}
|
||||
}
|
||||
|
||||
c.identitiesExcerpts = aux.Excerpts
|
||||
return nil
|
||||
}
|
||||
|
||||
// write will serialize on disk all the cache files
|
||||
func (c *RepoCache) write() error {
|
||||
err := c.writeBugCache()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.writeIdentityCache()
|
||||
}
|
||||
|
||||
// write will serialize on disk the bug cache file
|
||||
func (c *RepoCache) writeBugCache() error {
|
||||
var data bytes.Buffer
|
||||
|
||||
aux := struct {
|
||||
@ -166,7 +270,7 @@ func (c *RepoCache) write() error {
|
||||
Excerpts map[string]*BugExcerpt
|
||||
}{
|
||||
Version: formatVersion,
|
||||
Excerpts: c.excerpts,
|
||||
Excerpts: c.bugExcerpts,
|
||||
}
|
||||
|
||||
encoder := gob.NewEncoder(&data)
|
||||
@ -176,7 +280,7 @@ func (c *RepoCache) write() error {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(cacheFilePath(c.repo))
|
||||
f, err := os.Create(bugCacheFilePath(c.repo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -189,14 +293,66 @@ func (c *RepoCache) write() error {
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func cacheFilePath(repo repository.Repo) string {
|
||||
return path.Join(repo.GetPath(), ".git", "git-bug", cacheFile)
|
||||
// write will serialize on disk the identity cache file
|
||||
func (c *RepoCache) writeIdentityCache() error {
|
||||
var data bytes.Buffer
|
||||
|
||||
aux := struct {
|
||||
Version uint
|
||||
Excerpts map[string]*IdentityExcerpt
|
||||
}{
|
||||
Version: formatVersion,
|
||||
Excerpts: c.identitiesExcerpts,
|
||||
}
|
||||
|
||||
encoder := gob.NewEncoder(&data)
|
||||
|
||||
err := encoder.Encode(aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(identityCacheFilePath(c.repo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write(data.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func bugCacheFilePath(repo repository.Repo) string {
|
||||
return path.Join(repo.GetPath(), ".git", "git-bug", bugCacheFile)
|
||||
}
|
||||
|
||||
func identityCacheFilePath(repo repository.Repo) string {
|
||||
return path.Join(repo.GetPath(), ".git", "git-bug", identityCacheFile)
|
||||
}
|
||||
|
||||
func (c *RepoCache) buildCache() error {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Building identity cache... ")
|
||||
|
||||
c.identitiesExcerpts = make(map[string]*IdentityExcerpt)
|
||||
|
||||
allIdentities := identity.ReadAllLocalIdentities(c.repo)
|
||||
|
||||
for i := range allIdentities {
|
||||
if i.Err != nil {
|
||||
return i.Err
|
||||
}
|
||||
|
||||
c.identitiesExcerpts[i.Identity.Id()] = NewIdentityExcerpt(i.Identity)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Done.")
|
||||
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Building bug cache... ")
|
||||
|
||||
c.excerpts = make(map[string]*BugExcerpt)
|
||||
c.bugExcerpts = make(map[string]*BugExcerpt)
|
||||
|
||||
allBugs := bug.ReadAllLocalBugs(c.repo)
|
||||
|
||||
@ -206,7 +362,7 @@ func (c *RepoCache) buildCache() error {
|
||||
}
|
||||
|
||||
snap := b.Bug.Compile()
|
||||
c.excerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
|
||||
c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Done.")
|
||||
@ -231,13 +387,23 @@ func (c *RepoCache) ResolveBug(id string) (*BugCache, error) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// ResolveBugExcerpt retrieve a BugExcerpt matching the exact given id
|
||||
func (c *RepoCache) ResolveBugExcerpt(id string) (*BugExcerpt, error) {
|
||||
e, ok := c.bugExcerpts[id]
|
||||
if !ok {
|
||||
return nil, bug.ErrBugNotExist
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple
|
||||
// bugs match.
|
||||
func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
|
||||
// preallocate but empty
|
||||
matching := make([]string, 0, 5)
|
||||
|
||||
for id := range c.excerpts {
|
||||
for id := range c.bugExcerpts {
|
||||
if strings.HasPrefix(id, prefix) {
|
||||
matching = append(matching, id)
|
||||
}
|
||||
@ -261,7 +427,7 @@ func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCach
|
||||
// preallocate but empty
|
||||
matching := make([]string, 0, 5)
|
||||
|
||||
for id, excerpt := range c.excerpts {
|
||||
for id, excerpt := range c.bugExcerpts {
|
||||
if excerpt.CreateMetadata[key] == value {
|
||||
matching = append(matching, id)
|
||||
}
|
||||
@ -278,6 +444,7 @@ func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCach
|
||||
return c.ResolveBug(matching[0])
|
||||
}
|
||||
|
||||
// QueryBugs return the id of all Bug matching the given Query
|
||||
func (c *RepoCache) QueryBugs(query *Query) []string {
|
||||
if query == nil {
|
||||
return c.AllBugsIds()
|
||||
@ -285,8 +452,8 @@ func (c *RepoCache) QueryBugs(query *Query) []string {
|
||||
|
||||
var filtered []*BugExcerpt
|
||||
|
||||
for _, excerpt := range c.excerpts {
|
||||
if query.Match(excerpt) {
|
||||
for _, excerpt := range c.bugExcerpts {
|
||||
if query.Match(c, excerpt) {
|
||||
filtered = append(filtered, excerpt)
|
||||
}
|
||||
}
|
||||
@ -321,10 +488,10 @@ func (c *RepoCache) QueryBugs(query *Query) []string {
|
||||
|
||||
// AllBugsIds return all known bug ids
|
||||
func (c *RepoCache) AllBugsIds() []string {
|
||||
result := make([]string, len(c.excerpts))
|
||||
result := make([]string, len(c.bugExcerpts))
|
||||
|
||||
i := 0
|
||||
for _, excerpt := range c.excerpts {
|
||||
for _, excerpt := range c.bugExcerpts {
|
||||
result[i] = excerpt.Id
|
||||
i++
|
||||
}
|
||||
@ -332,11 +499,6 @@ func (c *RepoCache) AllBugsIds() []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// ClearAllBugs clear all bugs kept in memory
|
||||
func (c *RepoCache) ClearAllBugs() {
|
||||
c.bugs = make(map[string]*BugCache)
|
||||
}
|
||||
|
||||
// ValidLabels list valid labels
|
||||
//
|
||||
// Note: in the future, a proper label policy could be implemented where valid
|
||||
@ -345,7 +507,7 @@ func (c *RepoCache) ClearAllBugs() {
|
||||
func (c *RepoCache) ValidLabels() []bug.Label {
|
||||
set := map[bug.Label]interface{}{}
|
||||
|
||||
for _, excerpt := range c.excerpts {
|
||||
for _, excerpt := range c.bugExcerpts {
|
||||
for _, l := range excerpt.Labels {
|
||||
set[l] = nil
|
||||
}
|
||||
@ -376,7 +538,7 @@ func (c *RepoCache) NewBug(title string, message string) (*BugCache, error) {
|
||||
// NewBugWithFiles create a new bug with attached files for the message
|
||||
// The new bug is written in the repository (commit)
|
||||
func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Hash) (*BugCache, error) {
|
||||
author, err := bug.GetUser(c.repo)
|
||||
author, err := c.GetUserIdentity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -387,8 +549,8 @@ func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Ha
|
||||
// NewBugWithFilesMeta create a new bug with attached files for the message, as
|
||||
// well as metadata for the Create operation.
|
||||
// The new bug is written in the repository (commit)
|
||||
func (c *RepoCache) NewBugRaw(author bug.Person, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) {
|
||||
b, op, err := bug.CreateWithFiles(author, unixTime, title, message, files)
|
||||
func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) {
|
||||
b, op, err := bug.CreateWithFiles(author.Identity, unixTime, title, message, files)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -402,9 +564,14 @@ func (c *RepoCache) NewBugRaw(author bug.Person, unixTime int64, title string, m
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, has := c.bugs[b.Id()]; has {
|
||||
return nil, fmt.Errorf("bug %s already exist in the cache", b.Id())
|
||||
}
|
||||
|
||||
cached := NewBugCache(c, b)
|
||||
c.bugs[b.Id()] = cached
|
||||
|
||||
// force the write of the excerpt
|
||||
err = c.bugUpdated(b.Id())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -421,6 +588,8 @@ func (c *RepoCache) Fetch(remote string) (string, error) {
|
||||
|
||||
// MergeAll will merge all the available remote bug
|
||||
func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
|
||||
// TODO: add identities
|
||||
|
||||
out := make(chan bug.MergeResult)
|
||||
|
||||
// Intercept merge results to update the cache properly
|
||||
@ -441,7 +610,7 @@ func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
|
||||
case bug.MergeStatusNew, bug.MergeStatusUpdated:
|
||||
b := result.Bug
|
||||
snap := b.Compile()
|
||||
c.excerpts[id] = NewBugExcerpt(b, &snap)
|
||||
c.bugExcerpts[id] = NewBugExcerpt(b, &snap)
|
||||
}
|
||||
}
|
||||
|
||||
@ -524,3 +693,166 @@ func repoIsAvailable(repo repository.Repo) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveIdentity retrieve an identity matching the exact given id
|
||||
func (c *RepoCache) ResolveIdentity(id string) (*IdentityCache, error) {
|
||||
cached, ok := c.identities[id]
|
||||
if ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
i, err := identity.ReadLocal(c.repo, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cached = NewIdentityCache(c, i)
|
||||
c.identities[id] = cached
|
||||
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// ResolveIdentityExcerpt retrieve a IdentityExcerpt matching the exact given id
|
||||
func (c *RepoCache) ResolveIdentityExcerpt(id string) (*IdentityExcerpt, error) {
|
||||
e, ok := c.identitiesExcerpts[id]
|
||||
if !ok {
|
||||
return nil, identity.ErrIdentityNotExist
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// ResolveIdentityPrefix retrieve an Identity matching an id prefix.
|
||||
// It fails if multiple identities match.
|
||||
func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) {
|
||||
// preallocate but empty
|
||||
matching := make([]string, 0, 5)
|
||||
|
||||
for id := range c.identitiesExcerpts {
|
||||
if strings.HasPrefix(id, prefix) {
|
||||
matching = append(matching, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matching) > 1 {
|
||||
return nil, identity.ErrMultipleMatch{Matching: matching}
|
||||
}
|
||||
|
||||
if len(matching) == 0 {
|
||||
return nil, identity.ErrIdentityNotExist
|
||||
}
|
||||
|
||||
return c.ResolveIdentity(matching[0])
|
||||
}
|
||||
|
||||
// ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on
|
||||
// one of it's version. If multiple version have the same key, the first defined take precedence.
|
||||
func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*IdentityCache, error) {
|
||||
// preallocate but empty
|
||||
matching := make([]string, 0, 5)
|
||||
|
||||
for id, i := range c.identitiesExcerpts {
|
||||
if i.ImmutableMetadata[key] == value {
|
||||
matching = append(matching, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matching) > 1 {
|
||||
return nil, identity.ErrMultipleMatch{Matching: matching}
|
||||
}
|
||||
|
||||
if len(matching) == 0 {
|
||||
return nil, identity.ErrIdentityNotExist
|
||||
}
|
||||
|
||||
return c.ResolveIdentity(matching[0])
|
||||
}
|
||||
|
||||
// AllIdentityIds return all known identity ids
|
||||
func (c *RepoCache) AllIdentityIds() []string {
|
||||
result := make([]string, len(c.identitiesExcerpts))
|
||||
|
||||
i := 0
|
||||
for _, excerpt := range c.identitiesExcerpts {
|
||||
result[i] = excerpt.Id
|
||||
i++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *RepoCache) SetUserIdentity(i *IdentityCache) error {
|
||||
err := identity.SetUserIdentity(c.repo, i.Identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure that everything is fine
|
||||
if _, ok := c.identities[i.Id()]; !ok {
|
||||
panic("SetUserIdentity while the identity is not from the cache, something is wrong")
|
||||
}
|
||||
|
||||
c.userIdentityId = i.Id()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RepoCache) GetUserIdentity() (*IdentityCache, error) {
|
||||
if c.userIdentityId != "" {
|
||||
i, ok := c.identities[c.userIdentityId]
|
||||
if ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
i, err := identity.GetUserIdentity(c.repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cached := NewIdentityCache(c, i)
|
||||
c.identities[i.Id()] = cached
|
||||
c.userIdentityId = i.Id()
|
||||
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// NewIdentity create a new identity
|
||||
// The new identity is written in the repository (commit)
|
||||
func (c *RepoCache) NewIdentity(name string, email string) (*IdentityCache, error) {
|
||||
return c.NewIdentityRaw(name, email, "", "", nil)
|
||||
}
|
||||
|
||||
// NewIdentityFull create a new identity
|
||||
// The new identity is written in the repository (commit)
|
||||
func (c *RepoCache) NewIdentityFull(name string, email string, login string, avatarUrl string) (*IdentityCache, error) {
|
||||
return c.NewIdentityRaw(name, email, login, avatarUrl, nil)
|
||||
}
|
||||
|
||||
func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) {
|
||||
i := identity.NewIdentityFull(name, email, login, avatarUrl)
|
||||
|
||||
for key, value := range metadata {
|
||||
i.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
err := i.Commit(c.repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, has := c.identities[i.Id()]; has {
|
||||
return nil, fmt.Errorf("identity %s already exist in the cache", i.Id())
|
||||
}
|
||||
|
||||
cached := NewIdentityCache(c, i)
|
||||
c.identities[i.Id()] = cached
|
||||
|
||||
// force the write of the excerpt
|
||||
err = c.identityUpdated(i.Id())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cached, nil
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ func runAddBug(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Create a new bug",
|
||||
Short: "Create a new bug.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runAddBug,
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ func runBridge(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var bridgeCmd = &cobra.Command{
|
||||
Use: "bridge",
|
||||
Short: "Configure and use bridges to other bug trackers",
|
||||
Short: "Configure and use bridges to other bug trackers.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runBridge,
|
||||
Args: cobra.NoArgs,
|
||||
|
@ -91,7 +91,7 @@ func promptName() (string, error) {
|
||||
|
||||
var bridgeConfigureCmd = &cobra.Command{
|
||||
Use: "configure",
|
||||
Short: "Configure a new bridge",
|
||||
Short: "Configure a new bridge.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runBridgeConfigure,
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ func runBridgePull(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var bridgePullCmd = &cobra.Command{
|
||||
Use: "pull [<name>]",
|
||||
Short: "Pull updates",
|
||||
Short: "Pull updates.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runBridgePull,
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ func runBridgeRm(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var bridgeRmCmd = &cobra.Command{
|
||||
Use: "rm name <name>",
|
||||
Short: "Delete a configured bridge",
|
||||
Short: "Delete a configured bridge.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runBridgeRm,
|
||||
Args: cobra.ExactArgs(1),
|
||||
|
@ -61,7 +61,7 @@ func runCommands(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var commandsCmd = &cobra.Command{
|
||||
Use: "commands [<option>...]",
|
||||
Short: "Display available commands",
|
||||
Short: "Display available commands.",
|
||||
RunE: runCommands,
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ func commentsTextOutput(comments []bug.Comment) {
|
||||
|
||||
var commentCmd = &cobra.Command{
|
||||
Use: "comment [<id>]",
|
||||
Short: "Display or add comments",
|
||||
Short: "Display or add comments.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runComment,
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ func runCommentAdd(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = b.AddComment(commentAddMessage)
|
||||
_, err = b.AddComment(commentAddMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -56,7 +56,7 @@ func runCommentAdd(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var commentAddCmd = &cobra.Command{
|
||||
Use: "add [<id>]",
|
||||
Short: "Add a new comment",
|
||||
Short: "Add a new comment.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runCommentAdd,
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ func runDeselect(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var deselectCmd = &cobra.Command{
|
||||
Use: "deselect",
|
||||
Short: "Clear the implicitly selected bug",
|
||||
Short: "Clear the implicitly selected bug.",
|
||||
Example: `git bug select 2f15
|
||||
git bug comment
|
||||
git bug status
|
||||
|
@ -33,7 +33,7 @@ func runLabel(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var labelCmd = &cobra.Command{
|
||||
Use: "label [<id>]",
|
||||
Short: "Display, add or remove labels",
|
||||
Short: "Display, add or remove labels.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runLabel,
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ func runLabelAdd(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
changes, err := b.ChangeLabels(args, nil)
|
||||
changes, _, err := b.ChangeLabels(args, nil)
|
||||
|
||||
for _, change := range changes {
|
||||
fmt.Println(change)
|
||||
@ -37,7 +37,7 @@ func runLabelAdd(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var labelAddCmd = &cobra.Command{
|
||||
Use: "add [<id>] <label>[...]",
|
||||
Short: "Add a label",
|
||||
Short: "Add a label.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runLabelAdd,
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ func runLabelRm(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
changes, err := b.ChangeLabels(nil, args)
|
||||
changes, _, err := b.ChangeLabels(nil, args)
|
||||
|
||||
for _, change := range changes {
|
||||
fmt.Println(change)
|
||||
@ -37,7 +37,7 @@ func runLabelRm(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var labelRmCmd = &cobra.Command{
|
||||
Use: "rm [<id>] <label>[...]",
|
||||
Short: "Remove a label",
|
||||
Short: "Remove a label.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runLabelRm,
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ func runLsLabel(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var lsLabelCmd = &cobra.Command{
|
||||
Use: "ls-label",
|
||||
Short: "List valid labels",
|
||||
Short: "List valid labels.",
|
||||
Long: `List valid labels.
|
||||
|
||||
Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.`,
|
||||
|
@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/colors"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
"github.com/spf13/cobra"
|
||||
@ -52,7 +52,7 @@ func runLsBug(cmd *cobra.Command, args []string) error {
|
||||
|
||||
snapshot := b.Snapshot()
|
||||
|
||||
var author bug.Person
|
||||
var author identity.Interface
|
||||
|
||||
if len(snapshot.Comments) > 0 {
|
||||
create := snapshot.Comments[0]
|
||||
@ -131,7 +131,7 @@ func lsQueryFromFlags() (*cache.Query, error) {
|
||||
|
||||
var lsCmd = &cobra.Command{
|
||||
Use: "ls [<query>]",
|
||||
Short: "List bugs",
|
||||
Short: "List bugs.",
|
||||
Long: `Display a summary of each bugs.
|
||||
|
||||
You can pass an additional query to filter and order the list. This query can be expressed either with a simple query language or with flags.`,
|
||||
|
@ -54,7 +54,7 @@ func runPull(cmd *cobra.Command, args []string) error {
|
||||
// showCmd defines the "push" subcommand.
|
||||
var pullCmd = &cobra.Command{
|
||||
Use: "pull [<remote>]",
|
||||
Short: "Pull bugs update from a git remote",
|
||||
Short: "Pull bugs update from a git remote.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runPull,
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ func runPush(cmd *cobra.Command, args []string) error {
|
||||
// showCmd defines the "push" subcommand.
|
||||
var pushCmd = &cobra.Command{
|
||||
Use: "push [<remote>]",
|
||||
Short: "Push bugs update to a git remote",
|
||||
Short: "Push bugs update to a git remote.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runPush,
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -18,7 +19,7 @@ var repo repository.ClockedRepo
|
||||
// RootCmd represents the base command when called without any subcommands
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: rootCommandName,
|
||||
Short: "A bug tracker embedded in Git",
|
||||
Short: "A bug tracker embedded in Git.",
|
||||
Long: `git-bug is a bug tracker embedded in git.
|
||||
|
||||
git-bug use git objects to store the bug tracking separated from the files
|
||||
@ -53,6 +54,7 @@ func Execute() {
|
||||
}
|
||||
}
|
||||
|
||||
// loadRepo is a pre-run function that load the repository for use in a command
|
||||
func loadRepo(cmd *cobra.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
@ -70,3 +72,24 @@ func loadRepo(cmd *cobra.Command, args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadRepoEnsureUser is the same as loadRepo, but also ensure that the user has configured
|
||||
// an identity. Use this pre-run function when an error after using the configured user won't
|
||||
// do.
|
||||
func loadRepoEnsureUser(cmd *cobra.Command, args []string) error {
|
||||
err := loadRepo(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set, err := identity.IsUserIdentitySet(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !set {
|
||||
return identity.ErrNoIdentitySet
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ func runSelect(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var selectCmd = &cobra.Command{
|
||||
Use: "select <id>",
|
||||
Short: "Select a bug for implicit use in future commands",
|
||||
Short: "Select a bug for implicit use in future commands.",
|
||||
Example: `git bug select 2f15
|
||||
git bug comment
|
||||
git bug status
|
||||
|
@ -1,113 +1,80 @@
|
||||
package _select
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSelect(t *testing.T) {
|
||||
repo, err := cache.NewRepoCache(createRepo())
|
||||
checkErr(t, err)
|
||||
repo := test.CreateRepo(false)
|
||||
|
||||
_, _, err = ResolveBug(repo, []string{})
|
||||
if err != ErrNoValidId {
|
||||
t.Fatal("expected no valid id error, got", err)
|
||||
}
|
||||
repoCache, err := cache.NewRepoCache(repo)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Select(repo, "invalid")
|
||||
checkErr(t, err)
|
||||
_, _, err = ResolveBug(repoCache, []string{})
|
||||
require.Equal(t, ErrNoValidId, err)
|
||||
|
||||
_, _, err = ResolveBug(repo, []string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid bug error")
|
||||
}
|
||||
err = Select(repoCache, "invalid")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Resolve without a pattern should fail when no bug is selected
|
||||
_, _, err = ResolveBug(repoCache, []string{})
|
||||
require.Error(t, err)
|
||||
|
||||
// generate a bunch of bugs
|
||||
|
||||
rene, err := repoCache.NewIdentity("René Descartes", "rene@descartes.fr")
|
||||
require.NoError(t, err)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err := repo.NewBug("title", "message")
|
||||
checkErr(t, err)
|
||||
_, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// two more for testing
|
||||
b1, err := repo.NewBug("title", "message")
|
||||
checkErr(t, err)
|
||||
b2, err := repo.NewBug("title", "message")
|
||||
checkErr(t, err)
|
||||
// and two more for testing
|
||||
b1, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
|
||||
require.NoError(t, err)
|
||||
b2, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Select(repo, b1.Id())
|
||||
checkErr(t, err)
|
||||
err = Select(repoCache, b1.Id())
|
||||
require.NoError(t, err)
|
||||
|
||||
// normal select without args
|
||||
b3, _, err := ResolveBug(repo, []string{})
|
||||
checkErr(t, err)
|
||||
if b3.Id() != b1.Id() {
|
||||
t.Fatal("incorrect bug returned")
|
||||
}
|
||||
b3, _, err := ResolveBug(repoCache, []string{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, b1.Id(), b3.Id())
|
||||
|
||||
// override selection with same id
|
||||
b4, _, err := ResolveBug(repo, []string{b1.Id()})
|
||||
checkErr(t, err)
|
||||
if b4.Id() != b1.Id() {
|
||||
t.Fatal("incorrect bug returned")
|
||||
}
|
||||
b4, _, err := ResolveBug(repoCache, []string{b1.Id()})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, b1.Id(), b4.Id())
|
||||
|
||||
// override selection with a prefix
|
||||
b5, _, err := ResolveBug(repo, []string{b1.HumanId()})
|
||||
checkErr(t, err)
|
||||
if b5.Id() != b1.Id() {
|
||||
t.Fatal("incorrect bug returned")
|
||||
}
|
||||
b5, _, err := ResolveBug(repoCache, []string{b1.HumanId()})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, b1.Id(), b5.Id())
|
||||
|
||||
// args that shouldn't override
|
||||
b6, _, err := ResolveBug(repo, []string{"arg"})
|
||||
checkErr(t, err)
|
||||
if b6.Id() != b1.Id() {
|
||||
t.Fatal("incorrect bug returned")
|
||||
}
|
||||
b6, _, err := ResolveBug(repoCache, []string{"arg"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, b1.Id(), b6.Id())
|
||||
|
||||
// override with a different id
|
||||
b7, _, err := ResolveBug(repo, []string{b2.Id()})
|
||||
checkErr(t, err)
|
||||
if b7.Id() != b2.Id() {
|
||||
t.Fatal("incorrect bug returned")
|
||||
}
|
||||
b7, _, err := ResolveBug(repoCache, []string{b2.Id()})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, b2.Id(), b7.Id())
|
||||
|
||||
err = Clear(repo)
|
||||
checkErr(t, err)
|
||||
err = Clear(repoCache)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = ResolveBug(repo, []string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid bug error")
|
||||
}
|
||||
}
|
||||
|
||||
func createRepo() *repository.GitRepo {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
repo, err := repository.InitGitRepo(dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := repo.StoreConfig("user.name", "testuser"); err != nil {
|
||||
log.Fatal("failed to set user.name for test repository: ", err)
|
||||
}
|
||||
if err := repo.StoreConfig("user.email", "testuser@example.com"); err != nil {
|
||||
log.Fatal("failed to set user.email for test repository: ", err)
|
||||
}
|
||||
|
||||
return repo
|
||||
}
|
||||
|
||||
func checkErr(t testing.TB, err error) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Resolve without a pattern should error again after clearing the selected bug
|
||||
_, _, err = ResolveBug(repoCache, []string{})
|
||||
require.Error(t, err)
|
||||
|
||||
require.NoError(t, test.CleanupRepo(repo))
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ func runShowBug(cmd *cobra.Command, args []string) error {
|
||||
case "author":
|
||||
fmt.Printf("%s\n", firstComment.Author.DisplayName())
|
||||
case "authorEmail":
|
||||
fmt.Printf("%s\n", firstComment.Author.Email)
|
||||
fmt.Printf("%s\n", firstComment.Author.Email())
|
||||
case "createTime":
|
||||
fmt.Printf("%s\n", firstComment.FormatTime())
|
||||
case "id":
|
||||
@ -93,7 +93,7 @@ func runShowBug(cmd *cobra.Command, args []string) error {
|
||||
indent,
|
||||
i,
|
||||
comment.Author.DisplayName(),
|
||||
comment.Author.Email,
|
||||
comment.Author.Email(),
|
||||
)
|
||||
|
||||
if comment.Message == "" {
|
||||
@ -113,7 +113,7 @@ func runShowBug(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var showCmd = &cobra.Command{
|
||||
Use: "show [<id>]",
|
||||
Short: "Display the details of a bug",
|
||||
Short: "Display the details of a bug.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runShowBug,
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ func runStatus(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status [<id>]",
|
||||
Short: "Display or change a bug status",
|
||||
Short: "Display or change a bug status.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runStatus,
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ func runStatusClose(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.Close()
|
||||
_, err = b.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -30,7 +30,7 @@ func runStatusClose(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var closeCmd = &cobra.Command{
|
||||
Use: "close [<id>]",
|
||||
Short: "Mark a bug as closed",
|
||||
Short: "Mark a bug as closed.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runStatusClose,
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ func runStatusOpen(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.Open()
|
||||
_, err = b.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -30,7 +30,7 @@ func runStatusOpen(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var openCmd = &cobra.Command{
|
||||
Use: "open [<id>]",
|
||||
Short: "Mark a bug as open",
|
||||
Short: "Mark a bug as open.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runStatusOpen,
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ func runTermUI(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var termUICmd = &cobra.Command{
|
||||
Use: "termui",
|
||||
Short: "Launch the terminal UI",
|
||||
PreRunE: loadRepo,
|
||||
Short: "Launch the terminal UI.",
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runTermUI,
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ func runTitle(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var titleCmd = &cobra.Command{
|
||||
Use: "title [<id>]",
|
||||
Short: "Display or change a title",
|
||||
Short: "Display or change a title.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runTitle,
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func runTitleEdit(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("No change, aborting.")
|
||||
}
|
||||
|
||||
err = b.SetTitle(titleEditTitle)
|
||||
_, err = b.SetTitle(titleEditTitle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -54,7 +54,7 @@ func runTitleEdit(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var titleEditCmd = &cobra.Command{
|
||||
Use: "edit [<id>]",
|
||||
Short: "Edit a title",
|
||||
Short: "Edit a title.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runTitleEdit,
|
||||
}
|
||||
|
61
commands/user.go
Normal file
61
commands/user.go
Normal file
@ -0,0 +1,61 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func runUser(cmd *cobra.Command, args []string) error {
|
||||
backend, err := cache.NewRepoCache(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
if len(args) > 1 {
|
||||
return errors.New("only one identity can be displayed at a time")
|
||||
}
|
||||
|
||||
var id *cache.IdentityCache
|
||||
if len(args) == 1 {
|
||||
id, err = backend.ResolveIdentityPrefix(args[0])
|
||||
} else {
|
||||
id, err = backend.GetUserIdentity()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Id: %s\n", id.Id())
|
||||
fmt.Printf("Name: %s\n", id.Name())
|
||||
fmt.Printf("Login: %s\n", id.Login())
|
||||
fmt.Printf("Email: %s\n", id.Email())
|
||||
fmt.Printf("Last modification: %s (lamport %d)\n",
|
||||
id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200"),
|
||||
id.LastModificationLamport())
|
||||
fmt.Println("Metadata:")
|
||||
for key, value := range id.ImmutableMetadata() {
|
||||
fmt.Printf(" %s --> %s\n", key, value)
|
||||
}
|
||||
// fmt.Printf("Protected: %v\n", id.IsProtected())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var userCmd = &cobra.Command{
|
||||
Use: "user [<id>]",
|
||||
Short: "Display or change the user identity.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runUser,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(userCmd)
|
||||
userCmd.Flags().SortFlags = false
|
||||
}
|
48
commands/user_adopt.go
Normal file
48
commands/user_adopt.go
Normal file
@ -0,0 +1,48 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func runUserAdopt(cmd *cobra.Command, args []string) error {
|
||||
backend, err := cache.NewRepoCache(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
prefix := args[0]
|
||||
|
||||
i, err := backend.ResolveIdentityPrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = backend.SetUserIdentity(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Your identity is now: %s\n", i.DisplayName())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var userAdoptCmd = &cobra.Command{
|
||||
Use: "adopt <id>",
|
||||
Short: "Adopt an existing identity as your own.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runUserAdopt,
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
func init() {
|
||||
userCmd.AddCommand(userAdoptCmd)
|
||||
userAdoptCmd.Flags().SortFlags = false
|
||||
}
|
81
commands/user_create.go
Normal file
81
commands/user_create.go
Normal file
@ -0,0 +1,81 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/input"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func runUserCreate(cmd *cobra.Command, args []string) error {
|
||||
backend, err := cache.NewRepoCache(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Before creating a new identity, please be aware that "+
|
||||
"you can also use an already existing one using \"git bug user adopt\". As an example, "+
|
||||
"you can do that if your identity has already been created by an importer.\n\n")
|
||||
|
||||
preName, err := backend.GetUserName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name, err := input.PromptValueRequired("Name", preName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preEmail, err := backend.GetUserEmail()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
email, err := input.PromptValueRequired("Email", preEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
login, err := input.PromptValue("Avatar URL", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := backend.NewIdentityRaw(name, email, "", login, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = id.CommitAsNeeded()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = backend.SetUserIdentity(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stderr)
|
||||
fmt.Println(id.Id())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var userCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new identity.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runUserCreate,
|
||||
}
|
||||
|
||||
func init() {
|
||||
userCmd.AddCommand(userCreateCmd)
|
||||
userCreateCmd.Flags().SortFlags = false
|
||||
}
|
45
commands/user_ls.go
Normal file
45
commands/user_ls.go
Normal file
@ -0,0 +1,45 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/util/colors"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func runUserLs(cmd *cobra.Command, args []string) error {
|
||||
backend, err := cache.NewRepoCache(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
for _, id := range backend.AllIdentityIds() {
|
||||
i, err := backend.ResolveIdentityExcerpt(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s\n",
|
||||
colors.Cyan(i.HumanId()),
|
||||
i.DisplayName(),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var userLsCmd = &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List identities.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runUserLs,
|
||||
}
|
||||
|
||||
func init() {
|
||||
userCmd.AddCommand(userLsCmd)
|
||||
userLsCmd.Flags().SortFlags = false
|
||||
}
|
@ -41,7 +41,7 @@ func runVersionCmd(cmd *cobra.Command, args []string) {
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show git-bug version information",
|
||||
Short: "Show git-bug version information.",
|
||||
Run: runVersionCmd,
|
||||
}
|
||||
|
||||
|
@ -224,7 +224,7 @@ func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Requ
|
||||
|
||||
var webUICmd = &cobra.Command{
|
||||
Use: "webui",
|
||||
Short: "Launch the web UI",
|
||||
Short: "Launch the web UI.",
|
||||
PreRunE: loadRepo,
|
||||
RunE: runWebUI,
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/MichaelMure/git-bug/commands"
|
||||
"github.com/spf13/cobra/doc"
|
||||
@ -14,7 +15,7 @@ import (
|
||||
|
||||
func main() {
|
||||
cwd, _ := os.Getwd()
|
||||
filepath := path.Join(cwd, "doc", "man")
|
||||
dir := path.Join(cwd, "doc", "man")
|
||||
|
||||
header := &doc.GenManHeader{
|
||||
Title: "GIT-BUG",
|
||||
@ -24,7 +25,17 @@ func main() {
|
||||
|
||||
fmt.Println("Generating manpage ...")
|
||||
|
||||
err := doc.GenManTree(commands.RootCmd, header, filepath)
|
||||
files, err := filepath.Glob(dir + "/*.1")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if err := os.Remove(f); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = doc.GenManTree(commands.RootCmd, header, dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -4,20 +4,32 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/MichaelMure/git-bug/commands"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/MichaelMure/git-bug/commands"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cwd, _ := os.Getwd()
|
||||
filepath := path.Join(cwd, "doc", "md")
|
||||
dir := path.Join(cwd, "doc", "md")
|
||||
|
||||
fmt.Println("Generating Markdown documentation ...")
|
||||
|
||||
err := doc.GenMarkdownTree(commands.RootCmd, filepath)
|
||||
files, err := filepath.Glob(dir + "/*.md")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if err := os.Remove(f); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = doc.GenMarkdownTree(commands.RootCmd, dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-add \- Create a new bug
|
||||
git\-bug\-add \- Create a new bug.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-add \- Create a new bug
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Create a new bug
|
||||
Create a new bug.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,29 +0,0 @@
|
||||
.TH "GIT-BUG" "1" "Sep 2018" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-bridge\-bridge \- Configure and use bridges to other bug trackers
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBgit\-bug bridge bridge [flags]\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Configure and use bridges to other bug trackers
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for bridge
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBgit\-bug\-bridge(1)\fP
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-bridge\-configure \- Configure a new bridge
|
||||
git\-bug\-bridge\-configure \- Configure a new bridge.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-bridge\-configure \- Configure a new bridge
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Configure a new bridge
|
||||
Configure a new bridge.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-bridge\-pull \- Pull updates
|
||||
git\-bug\-bridge\-pull \- Pull updates.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-bridge\-pull \- Pull updates
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Pull updates
|
||||
Pull updates.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-bridge\-rm \- Delete a configured bridge
|
||||
git\-bug\-bridge\-rm \- Delete a configured bridge.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-bridge\-rm \- Delete a configured bridge
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Delete a configured bridge
|
||||
Delete a configured bridge.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-bridge \- Configure and use bridges to other bug trackers
|
||||
git\-bug\-bridge \- Configure and use bridges to other bug trackers.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-bridge \- Configure and use bridges to other bug trackers
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Configure and use bridges to other bug trackers
|
||||
Configure and use bridges to other bug trackers.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-commands \- Display available commands
|
||||
git\-bug\-commands \- Display available commands.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-commands \- Display available commands
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Display available commands
|
||||
Display available commands.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-comment\-add \- Add a new comment
|
||||
git\-bug\-comment\-add \- Add a new comment.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-comment\-add \- Add a new comment
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Add a new comment
|
||||
Add a new comment.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-comment \- Display or add comments
|
||||
git\-bug\-comment \- Display or add comments.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-comment \- Display or add comments
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Display or add comments
|
||||
Display or add comments.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-deselect \- Clear the implicitly selected bug
|
||||
git\-bug\-deselect \- Clear the implicitly selected bug.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-deselect \- Clear the implicitly selected bug
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Clear the implicitly selected bug
|
||||
Clear the implicitly selected bug.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-label\-add \- Add a label
|
||||
git\-bug\-label\-add \- Add a label.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-label\-add \- Add a label
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Add a label
|
||||
Add a label.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-label\-rm \- Remove a label
|
||||
git\-bug\-label\-rm \- Remove a label.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-label\-rm \- Remove a label
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Remove a label
|
||||
Remove a label.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-label \- Display, add or remove labels
|
||||
git\-bug\-label \- Display, add or remove labels.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-label \- Display, add or remove labels
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Display, add or remove labels
|
||||
Display, add or remove labels.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
29
doc/man/git-bug-ls-id.1
Normal file
29
doc/man/git-bug-ls-id.1
Normal file
@ -0,0 +1,29 @@
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-ls\-id \- List Bug Id
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBgit\-bug ls\-id [<prefix>] [flags]\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
List Bug Id
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for ls\-id
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBgit\-bug(1)\fP
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-ls\-label \- List valid labels
|
||||
git\-bug\-ls\-label \- List valid labels.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-ls \- List bugs
|
||||
git\-bug\-ls \- List bugs.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-pull \- Pull bugs update from a git remote
|
||||
git\-bug\-pull \- Pull bugs update from a git remote.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-pull \- Pull bugs update from a git remote
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Pull bugs update from a git remote
|
||||
Pull bugs update from a git remote.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-push \- Push bugs update to a git remote
|
||||
git\-bug\-push \- Push bugs update to a git remote.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-push \- Push bugs update to a git remote
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Push bugs update to a git remote
|
||||
Push bugs update to a git remote.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-select \- Select a bug for implicit use in future commands
|
||||
git\-bug\-select \- Select a bug for implicit use in future commands.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-select \- Select a bug for implicit use in future commands
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Select a bug for implicit use in future commands
|
||||
Select a bug for implicit use in future commands.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-show \- Display the details of a bug
|
||||
git\-bug\-show \- Display the details of a bug.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-show \- Display the details of a bug
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Display the details of a bug
|
||||
Display the details of a bug.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-status\-close \- Mark a bug as closed
|
||||
git\-bug\-status\-close \- Mark a bug as closed.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-status\-close \- Mark a bug as closed
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Mark a bug as closed
|
||||
Mark a bug as closed.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
@ -1,11 +1,11 @@
|
||||
.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" ""
|
||||
.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
git\-bug\-status\-open \- Mark a bug as open
|
||||
git\-bug\-status\-open \- Mark a bug as open.
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -15,7 +15,7 @@ git\-bug\-status\-open \- Mark a bug as open
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
Mark a bug as open
|
||||
Mark a bug as open.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user