Merge pull request #89 from MichaelMure/identity

WIP identity in git
This commit is contained in:
Michael Muré 2019-03-01 23:17:57 +01:00 committed by GitHub
commit 7260ca05bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
223 changed files with 5495 additions and 6288 deletions

38
Gopkg.lock generated
View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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,

View File

@ -1,4 +1,4 @@
// Package launchad contains the Launchpad bridge implementation
// Package launchpad contains the Launchpad bridge implementation
package launchpad
import (

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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)
}

View File

@ -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
View 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
}

View File

@ -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 {

View File

@ -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

View 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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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

View 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)
}

View File

@ -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
View 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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
View 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)
}

View File

@ -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
View 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)
}

View File

@ -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")
}

View File

@ -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)

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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")
}

View File

@ -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

View File

@ -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
View File

@ -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
View File

@ -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
View File

@ -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
View 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
View 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]
}

View File

@ -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
View File

@ -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
}

View File

@ -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,
}

View File

@ -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,

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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),

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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.`,

View File

@ -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.`,

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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
}

View File

@ -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

View File

@ -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))
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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
View 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
View 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
View 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
View 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
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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