From feab9412dffe5772048aad29893c4cb01d566387 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Wed, 21 Nov 2018 18:56:12 +0100 Subject: WIP identity in git --- Gopkg.lock | 1 + bridge/core/bridge.go | 6 +- bug/bug_actions.go | 5 + bug/bug_actions_test.go | 2 +- bug/comment.go | 3 +- bug/op_add_comment.go | 8 +- bug/op_create.go | 8 +- bug/op_create_test.go | 6 +- bug/op_edit_comment.go | 8 +- bug/op_edit_comment_test.go | 6 +- bug/op_label_change.go | 8 +- bug/op_noop.go | 9 +- bug/op_set_metadata.go | 9 +- bug/op_set_metadata_test.go | 6 +- bug/op_set_status.go | 9 +- bug/op_set_title.go | 8 +- bug/operation.go | 50 ++- bug/operation_iterator_test.go | 7 +- bug/operation_pack.go | 31 +- bug/operation_test.go | 11 +- bug/person.go | 95 ----- bug/snapshot.go | 3 +- bug/timeline.go | 5 +- cache/bug_cache.go | 26 +- cache/bug_excerpt.go | 4 +- cache/repo_cache.go | 5 +- commands/id.go | 35 ++ graphql/bug.graphql | 122 ------ graphql/gqlgen.yml | 15 +- graphql/graph/gen_graph.go | 658 ++++++++++++++++++--------------- graphql/graphql_test.go | 148 ++++++++ graphql/operations.graphql | 100 ----- graphql/resolvers/person.go | 37 -- graphql/resolvers/root.go | 4 - graphql/root.graphql | 38 -- graphql/schema/bug.graphql | 108 ++++++ graphql/schema/identity.graphql | 13 + graphql/schema/operations.graphql | 100 +++++ graphql/schema/root.graphql | 38 ++ graphql/schema/timeline.graphql | 86 +++++ graphql/timeline.graphql | 86 ----- identity/bare.go | 144 ++++++++ identity/identity.go | 285 ++++++++++++++ identity/identity_test.go | 145 ++++++++ identity/interface.go | 30 ++ identity/key.go | 7 + identity/version.go | 105 ++++++ misc/random_bugs/create_random_bugs.go | 26 +- termui/bug_table.go | 3 +- tests/graphql_test.go | 148 -------- tests/read_bugs_test.go | 52 +-- util/test/repo.go | 52 +++ 52 files changed, 1827 insertions(+), 1097 deletions(-) delete mode 100644 bug/person.go create mode 100644 commands/id.go delete mode 100644 graphql/bug.graphql create mode 100644 graphql/graphql_test.go delete mode 100644 graphql/operations.graphql delete mode 100644 graphql/resolvers/person.go delete mode 100644 graphql/root.graphql create mode 100644 graphql/schema/bug.graphql create mode 100644 graphql/schema/identity.graphql create mode 100644 graphql/schema/operations.graphql create mode 100644 graphql/schema/root.graphql create mode 100644 graphql/schema/timeline.graphql delete mode 100644 graphql/timeline.graphql create mode 100644 identity/bare.go create mode 100644 identity/identity.go create mode 100644 identity/identity_test.go create mode 100644 identity/interface.go create mode 100644 identity/key.go create mode 100644 identity/version.go delete mode 100644 tests/graphql_test.go create mode 100644 util/test/repo.go diff --git a/Gopkg.lock b/Gopkg.lock index 0ea5b61b..a208c91a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -469,6 +469,7 @@ "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", diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go index 91ed5bfb..96646edb 100644 --- a/bridge/core/bridge.go +++ b/bridge/core/bridge.go @@ -15,6 +15,8 @@ import ( var ErrImportNorSupported = errors.New("import is not supported") var ErrExportNorSupported = errors.New("export is not supported") +const bridgeConfigKeyPrefix = "git-bug.bridge" + var bridgeImpl map[string]reflect.Type // Bridge is a wrapper around a BridgeImpl that will bind low-level @@ -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) } diff --git a/bug/bug_actions.go b/bug/bug_actions.go index 487ba25e..a21db826 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -35,6 +35,11 @@ func Pull(repo repository.ClockedRepo, remote string) error { if merge.Err != nil { return merge.Err } + if merge.Status == MergeStatusInvalid { + // Not awesome: simply output the merge failure here as this function + // is only used in tests for now. + fmt.Println(merge) + } } return nil diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go index a60e5c68..4327ae58 100644 --- a/bug/bug_actions_test.go +++ b/bug/bug_actions_test.go @@ -50,7 +50,7 @@ func cleanupRepo(repo repository.Repo) error { func setupRepos(t testing.TB) (repoA, repoB, remote *repository.GitRepo) { repoA = createRepo(false) repoB = createRepo(false) - remote = createRepo(true) + remote = createRepo(false) remoteAddr := "file://" + remote.GetPath() diff --git a/bug/comment.go b/bug/comment.go index 67936634..84d34299 100644 --- a/bug/comment.go +++ b/bug/comment.go @@ -1,13 +1,14 @@ package bug import ( + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/git" "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 diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index 2d6fb21a..23a10419 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -3,6 +3,8 @@ package bug import ( "fmt" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/util/text" ) @@ -68,7 +70,7 @@ func (op *AddCommentOperation) Validate() error { // 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 +84,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 diff --git a/bug/op_create.go b/bug/op_create.go index 3816d8b7..01b2bf03 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/util/text" ) @@ -84,7 +86,7 @@ func (op *CreateOperation) Validate() error { // 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 +101,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) diff --git a/bug/op_create_test.go b/bug/op_create_test.go index d74051ec..227dea27 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -4,16 +4,14 @@ import ( "testing" "time" + "github.com/MichaelMure/git-bug/identity" "github.com/go-test/deep" ) func TestCreate(t *testing.T) { snapshot := Snapshot{} - var rene = Person{ - Name: "René Descartes", - Email: "rene@descartes.fr", - } + var rene = identity.NewBare("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index bc87310a..9e0afc02 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -3,6 +3,8 @@ package bug import ( "fmt" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/util/text" ) @@ -95,7 +97,7 @@ func (op *EditCommentOperation) Validate() error { // 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 +107,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 diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index 71a7dda2..ba9bc9d5 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -4,16 +4,14 @@ import ( "testing" "time" + "github.com/MichaelMure/git-bug/identity" "gotest.tools/assert" ) func TestEdit(t *testing.T) { snapshot := Snapshot{} - var rene = Person{ - Name: "René Descartes", - Email: "rene@descartes.fr", - } + var rene = identity.NewBare("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() diff --git a/bug/op_label_change.go b/bug/op_label_change.go index d7aab06b..5d0b6a78 100644 --- a/bug/op_label_change.go +++ b/bug/op_label_change.go @@ -4,6 +4,8 @@ import ( "fmt" "sort" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/util/git" "github.com/pkg/errors" ) @@ -100,7 +102,7 @@ func (op *LabelChangeOperation) Validate() error { // 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,7 +112,7 @@ func NewLabelChangeOperation(author Person, unixTime int64, added, removed []Lab type LabelChangeTimelineItem struct { hash git.Hash - Author Person + Author identity.Interface UnixTime Timestamp Added []Label Removed []Label @@ -121,7 +123,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 diff --git a/bug/op_noop.go b/bug/op_noop.go index ac898dde..410799b3 100644 --- a/bug/op_noop.go +++ b/bug/op_noop.go @@ -1,6 +1,9 @@ package bug -import "github.com/MichaelMure/git-bug/util/git" +import ( + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/util/git" +) var _ Operation = &NoOpOperation{} @@ -30,14 +33,14 @@ func (op *NoOpOperation) Validate() error { // 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 { diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go index aac81f3b..e18f1cb6 100644 --- a/bug/op_set_metadata.go +++ b/bug/op_set_metadata.go @@ -1,6 +1,9 @@ package bug -import "github.com/MichaelMure/git-bug/util/git" +import ( + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/util/git" +) var _ Operation = &SetMetadataOperation{} @@ -56,7 +59,7 @@ func (op *SetMetadataOperation) Validate() error { // 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 +68,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 diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index 068e2bb0..c6f5c3c1 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -4,16 +4,14 @@ import ( "testing" "time" + "github.com/MichaelMure/git-bug/identity" "github.com/stretchr/testify/assert" ) func TestSetMetadata(t *testing.T) { snapshot := Snapshot{} - var rene = Person{ - Name: "René Descartes", - Email: "rene@descartes.fr", - } + var rene = identity.NewBare("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() diff --git a/bug/op_set_status.go b/bug/op_set_status.go index 54f476cb..9fc64e52 100644 --- a/bug/op_set_status.go +++ b/bug/op_set_status.go @@ -1,6 +1,7 @@ package bug import ( + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/git" "github.com/pkg/errors" ) @@ -56,7 +57,7 @@ func (op *SetStatusOperation) Validate() error { // 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,7 +66,7 @@ func NewSetStatusOp(author Person, unixTime int64, status Status) *SetStatusOper type SetStatusTimelineItem struct { hash git.Hash - Author Person + Author identity.Interface UnixTime Timestamp Status Status } @@ -75,7 +76,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 +86,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 diff --git a/bug/op_set_title.go b/bug/op_set_title.go index b631ca18..3b253c06 100644 --- a/bug/op_set_title.go +++ b/bug/op_set_title.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/util/text" ) @@ -77,7 +79,7 @@ func (op *SetTitleOperation) Validate() error { // 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,7 +89,7 @@ func NewSetTitleOp(author Person, unixTime int64, title string, was string) *Set type SetTitleTimelineItem struct { hash git.Hash - Author Person + Author identity.Interface UnixTime Timestamp Title string Was string @@ -98,7 +100,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 diff --git a/bug/operation.go b/bug/operation.go index 592b5616..8dec5644 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -6,6 +6,8 @@ import ( "fmt" "time" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/util/git" "github.com/pkg/errors" ) @@ -74,21 +76,23 @@ func hashOperation(op Operation) (git.Hash, error) { return base.hash, nil } +// TODO: serialization with identity + // 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 +100,34 @@ func newOpBase(opType OperationType, author Person, unixTime int64) OpBase { } } +type opBaseJson struct { + OperationType OperationType `json:"type"` + UnixTime int64 `json:"timestamp"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +func (op *OpBase) MarshalJSON() ([]byte, error) { + return json.Marshal(opBaseJson{ + OperationType: op.OperationType, + UnixTime: op.UnixTime, + Metadata: op.Metadata, + }) +} + +func (op *OpBase) UnmarshalJSON(data []byte) error { + aux := opBaseJson{} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + op.OperationType = aux.OperationType + 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) @@ -125,6 +157,10 @@ func opBaseValidate(op Operation, opType OperationType) error { 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") } diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go index 506cc94f..6b32cfc4 100644 --- a/bug/operation_iterator_test.go +++ b/bug/operation_iterator_test.go @@ -1,17 +1,14 @@ package bug import ( + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "testing" "time" ) var ( - rene = Person{ - Name: "René Descartes", - Email: "rene@descartes.fr", - } - + rene = identity.NewBare("René Descartes", "rene@descartes.fr") unix = time.Now().Unix() createOp = NewCreateOp(rene, unix, "title", "message", nil) diff --git a/bug/operation_pack.go b/bug/operation_pack.go index f33d94bf..fc395d90 100644 --- a/bug/operation_pack.go +++ b/bug/operation_pack.go @@ -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 AddCommentOp: + op := &AddCommentOperation{} + err := json.Unmarshal(raw, &op) + return op, err case CreateOp: op := &CreateOperation{} err := json.Unmarshal(raw, &op) return op, err - case SetTitleOp: - op := &SetTitleOperation{} + case EditCommentOp: + op := &EditCommentOperation{} err := json.Unmarshal(raw, &op) return op, err - case AddCommentOp: - op := &AddCommentOperation{} + case LabelChangeOp: + op := &LabelChangeOperation{} err := json.Unmarshal(raw, &op) return op, err - case SetStatusOp: - op := &SetStatusOperation{} + case NoOpOp: + op := &NoOpOperation{} err := json.Unmarshal(raw, &op) return op, err - case LabelChangeOp: - op := &LabelChangeOperation{} + case SetMetadataOp: + op := &SetMetadataOperation{} err := json.Unmarshal(raw, &op) return op, err - case EditCommentOp: - op := &EditCommentOperation{} + 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: diff --git a/bug/operation_test.go b/bug/operation_test.go index 255d6d98..0e2afc6c 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -3,6 +3,7 @@ package bug import ( "testing" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" "github.com/stretchr/testify/require" @@ -25,11 +26,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.NewBare("", "rene@descartes.fr"), unix, ClosedStatus), + NewSetStatusOp(identity.NewBare("René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus), + NewSetStatusOp(identity.NewBare("René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus), + NewSetStatusOp(identity.NewBare("René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus), + NewSetStatusOp(identity.NewBare("René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus), &CreateOperation{OpBase: OpBase{ Author: rene, UnixTime: 0, diff --git a/bug/person.go b/bug/person.go deleted file mode 100644 index 449e2262..00000000 --- a/bug/person.go +++ /dev/null @@ -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") -} diff --git a/bug/snapshot.go b/bug/snapshot.go index 72e673d4..46618ebd 100644 --- a/bug/snapshot.go +++ b/bug/snapshot.go @@ -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 diff --git a/bug/timeline.go b/bug/timeline.go index a84c45e4..306ffa9e 100644 --- a/bug/timeline.go +++ b/bug/timeline.go @@ -3,6 +3,7 @@ package bug import ( "strings" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/git" ) @@ -15,7 +16,7 @@ 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 @@ -24,7 +25,7 @@ type CommentHistoryStep struct { // 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 diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 52e9eafb..25ff000c 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -5,6 +5,8 @@ import ( "strings" "time" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/util/git" ) @@ -87,7 +89,7 @@ func (c *BugCache) AddComment(message string) error { } func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error { - author, err := bug.GetUser(c.repoCache.repo) + author, err := identity.GetIdentity(c.repoCache.repo) if err != nil { return err } @@ -95,7 +97,7 @@ func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error { 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 { +func (c *BugCache) AddCommentRaw(author *identity.Identity, unixTime int64, message string, files []git.Hash, metadata map[string]string) error { op, err := bug.AddCommentWithFiles(c.bug, author, unixTime, message, files) if err != nil { return err @@ -109,7 +111,7 @@ func (c *BugCache) AddCommentRaw(author bug.Person, unixTime int64, message stri } func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelChangeResult, error) { - author, err := bug.GetUser(c.repoCache.repo) + author, err := identity.GetIdentity(c.repoCache.repo) if err != nil { return nil, err } @@ -117,7 +119,7 @@ func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelCh 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) { +func (c *BugCache) ChangeLabelsRaw(author *identity.Identity, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, error) { changes, op, err := bug.ChangeLabels(c.bug, author, unixTime, added, removed) if err != nil { return changes, err @@ -136,7 +138,7 @@ func (c *BugCache) ChangeLabelsRaw(author bug.Person, unixTime int64, added []st } func (c *BugCache) Open() error { - author, err := bug.GetUser(c.repoCache.repo) + author, err := identity.GetIdentity(c.repoCache.repo) if err != nil { return err } @@ -144,7 +146,7 @@ func (c *BugCache) Open() error { return c.OpenRaw(author, time.Now().Unix(), nil) } -func (c *BugCache) OpenRaw(author bug.Person, unixTime int64, metadata map[string]string) error { +func (c *BugCache) OpenRaw(author *identity.Identity, unixTime int64, metadata map[string]string) error { op, err := bug.Open(c.bug, author, unixTime) if err != nil { return err @@ -158,7 +160,7 @@ func (c *BugCache) OpenRaw(author bug.Person, unixTime int64, metadata map[strin } func (c *BugCache) Close() error { - author, err := bug.GetUser(c.repoCache.repo) + author, err := identity.GetIdentity(c.repoCache.repo) if err != nil { return err } @@ -166,7 +168,7 @@ func (c *BugCache) Close() error { return c.CloseRaw(author, time.Now().Unix(), nil) } -func (c *BugCache) CloseRaw(author bug.Person, unixTime int64, metadata map[string]string) error { +func (c *BugCache) CloseRaw(author *identity.Identity, unixTime int64, metadata map[string]string) error { op, err := bug.Close(c.bug, author, unixTime) if err != nil { return err @@ -180,7 +182,7 @@ func (c *BugCache) CloseRaw(author bug.Person, unixTime int64, metadata map[stri } func (c *BugCache) SetTitle(title string) error { - author, err := bug.GetUser(c.repoCache.repo) + author, err := identity.GetIdentity(c.repoCache.repo) if err != nil { return err } @@ -188,7 +190,7 @@ func (c *BugCache) SetTitle(title string) error { 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 { +func (c *BugCache) SetTitleRaw(author *identity.Identity, unixTime int64, title string, metadata map[string]string) error { op, err := bug.SetTitle(c.bug, author, unixTime, title) if err != nil { return err @@ -202,7 +204,7 @@ func (c *BugCache) SetTitleRaw(author bug.Person, unixTime int64, title string, } func (c *BugCache) EditComment(target git.Hash, message string) error { - author, err := bug.GetUser(c.repoCache.repo) + author, err := identity.GetIdentity(c.repoCache.repo) if err != nil { return err } @@ -210,7 +212,7 @@ func (c *BugCache) EditComment(target git.Hash, message string) error { 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 { +func (c *BugCache) EditCommentRaw(author *identity.Identity, unixTime int64, target git.Hash, message string, metadata map[string]string) error { op, err := bug.EditComment(c.bug, author, unixTime, target, message) if err != nil { return err diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index 7a11fa82..77c18175 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -3,6 +3,8 @@ package cache import ( "encoding/gob" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -18,7 +20,7 @@ type BugExcerpt struct { EditUnixTime int64 Status bug.Status - Author bug.Person + Author *identity.Identity Labels []bug.Label CreateMetadata map[string]string diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 286e27a5..a149fd73 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -14,6 +14,7 @@ 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" @@ -376,7 +377,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 := identity.GetIdentity(c.repo) if err != nil { return nil, err } @@ -387,7 +388,7 @@ 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) { +func (c *RepoCache) NewBugRaw(author *identity.Identity, 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) if err != nil { return nil, err diff --git a/commands/id.go b/commands/id.go new file mode 100644 index 00000000..485c5457 --- /dev/null +++ b/commands/id.go @@ -0,0 +1,35 @@ +package commands + +import ( + "fmt" + + "github.com/MichaelMure/git-bug/identity" + "github.com/spf13/cobra" +) + +func runId(cmd *cobra.Command, args []string) error { + id, err := identity.GetIdentity(repo) + if err != nil { + return err + } + + fmt.Printf("Id: %s\n", id.Id()) + fmt.Printf("Identity: %s\n", id.DisplayName()) + fmt.Printf("Name: %s\n", id.Name()) + fmt.Printf("Login: %s\n", id.Login()) + fmt.Printf("Email: %s\n", id.Email()) + fmt.Printf("Protected: %v\n", id.IsProtected()) + + return nil +} + +var idCmd = &cobra.Command{ + Use: "id", + Short: "Display or change the user identity", + PreRunE: loadRepo, + RunE: runId, +} + +func init() { + RootCmd.AddCommand(idCmd) +} diff --git a/graphql/bug.graphql b/graphql/bug.graphql deleted file mode 100644 index 27bbba99..00000000 --- a/graphql/bug.graphql +++ /dev/null @@ -1,122 +0,0 @@ -"""Represents an person""" -type Person { - """The name of the person, if known.""" - name: String - """The email of the person, if known.""" - email: String - """The login of the person, if known.""" - login: String - """A string containing the either the name of the person, its login or both""" - displayName: String! - """An url to an avatar""" - avatarUrl: String -} - -"""Represents a comment on a bug.""" -type Comment implements Authored { - """The author of this comment.""" - author: Person! - - """The message of this comment.""" - message: String! - - """All media's hash referenced in this comment""" - files: [Hash!]! -} - -type CommentConnection { - edges: [CommentEdge!]! - nodes: [Comment!]! - pageInfo: PageInfo! - totalCount: Int! -} - -type CommentEdge { - cursor: String! - node: Comment! -} - -enum Status { - OPEN - CLOSED -} - -type Bug { - id: String! - humanId: String! - status: Status! - title: String! - labels: [Label!]! - author: Person! - createdAt: Time! - lastEdit: Time! - - comments( - """Returns the elements in the list that come after the specified cursor.""" - after: String - """Returns the elements in the list that come before the specified cursor.""" - before: String - """Returns the first _n_ elements from the list.""" - first: Int - """Returns the last _n_ elements from the list.""" - last: Int - ): CommentConnection! - - timeline( - """Returns the elements in the list that come after the specified cursor.""" - after: String - """Returns the elements in the list that come before the specified cursor.""" - before: String - """Returns the first _n_ elements from the list.""" - first: Int - """Returns the last _n_ elements from the list.""" - last: Int - ): TimelineItemConnection! - - operations( - """Returns the elements in the list that come after the specified cursor.""" - after: String - """Returns the elements in the list that come before the specified cursor.""" - before: String - """Returns the first _n_ elements from the list.""" - first: Int - """Returns the last _n_ elements from the list.""" - last: Int - ): OperationConnection! -} - -"""The connection type for Bug.""" -type BugConnection { - """A list of edges.""" - edges: [BugEdge!]! - nodes: [Bug!]! - """Information to aid in pagination.""" - pageInfo: PageInfo! - """Identifies the total count of items in the connection.""" - totalCount: Int! -} - -"""An edge in a connection.""" -type BugEdge { - """A cursor for use in pagination.""" - cursor: String! - """The item at the end of the edge.""" - node: Bug! -} - -type Repository { - allBugs( - """Returns the elements in the list that come after the specified cursor.""" - after: String - """Returns the elements in the list that come before the specified cursor.""" - before: String - """Returns the first _n_ elements from the list.""" - first: Int - """Returns the last _n_ elements from the list.""" - last: Int - """A query to select and order bugs""" - query: String - ): BugConnection! - bug(prefix: String!): Bug -} - diff --git a/graphql/gqlgen.yml b/graphql/gqlgen.yml index b6dc3ae5..019f3444 100644 --- a/graphql/gqlgen.yml +++ b/graphql/gqlgen.yml @@ -1,4 +1,4 @@ -schema: "*.graphql" +schema: "schema/*.graphql" exec: filename: graph/gen_graph.go model: @@ -13,17 +13,8 @@ models: model: github.com/MichaelMure/git-bug/bug.Snapshot Comment: model: github.com/MichaelMure/git-bug/bug.Comment - Person: - model: github.com/MichaelMure/git-bug/bug.Person - fields: - name: - resolver: true - email: - resolver: true - login: - resolver: true - avatarUrl: - resolver: true + Identity: + model: github.com/MichaelMure/git-bug/identity.Identity Label: model: github.com/MichaelMure/git-bug/bug.Label Hash: diff --git a/graphql/graph/gen_graph.go b/graphql/graph/gen_graph.go index e7d09ef4..cc714ecc 100644 --- a/graphql/graph/gen_graph.go +++ b/graphql/graph/gen_graph.go @@ -15,6 +15,7 @@ import ( "github.com/99designs/gqlgen/graphql/introspection" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/graphql/models" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/git" "github.com/vektah/gqlparser" "github.com/vektah/gqlparser/ast" @@ -46,7 +47,6 @@ type ResolverRoot interface { LabelChangeOperation() LabelChangeOperationResolver LabelChangeTimelineItem() LabelChangeTimelineItemResolver Mutation() MutationResolver - Person() PersonResolver Query() QueryResolver Repository() RepositoryResolver SetStatusOperation() SetStatusOperationResolver @@ -158,6 +158,14 @@ type ComplexityRoot struct { Files func(childComplexity int) int } + Identity struct { + Name func(childComplexity int) int + Email func(childComplexity int) int + Login func(childComplexity int) int + DisplayName func(childComplexity int) int + AvatarUrl func(childComplexity int) int + } + LabelChangeOperation struct { Hash func(childComplexity int) int Author func(childComplexity int) int @@ -203,14 +211,6 @@ type ComplexityRoot struct { EndCursor func(childComplexity int) int } - Person struct { - Name func(childComplexity int) int - Email func(childComplexity int) int - Login func(childComplexity int) int - DisplayName func(childComplexity int) int - AvatarUrl func(childComplexity int) int - } - Query struct { DefaultRepository func(childComplexity int) int Repository func(childComplexity int, id string) int @@ -307,13 +307,6 @@ type MutationResolver interface { SetTitle(ctx context.Context, repoRef *string, prefix string, title string) (bug.Snapshot, error) Commit(ctx context.Context, repoRef *string, prefix string) (bug.Snapshot, error) } -type PersonResolver interface { - Name(ctx context.Context, obj *bug.Person) (*string, error) - Email(ctx context.Context, obj *bug.Person) (*string, error) - Login(ctx context.Context, obj *bug.Person) (*string, error) - - AvatarURL(ctx context.Context, obj *bug.Person) (*string, error) -} type QueryResolver interface { DefaultRepository(ctx context.Context) (*models.Repository, error) Repository(ctx context.Context, id string) (*models.Repository, error) @@ -1453,6 +1446,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.EditCommentOperation.Files(childComplexity), true + case "Identity.name": + if e.complexity.Identity.Name == nil { + break + } + + return e.complexity.Identity.Name(childComplexity), true + + case "Identity.email": + if e.complexity.Identity.Email == nil { + break + } + + return e.complexity.Identity.Email(childComplexity), true + + case "Identity.login": + if e.complexity.Identity.Login == nil { + break + } + + return e.complexity.Identity.Login(childComplexity), true + + case "Identity.displayName": + if e.complexity.Identity.DisplayName == nil { + break + } + + return e.complexity.Identity.DisplayName(childComplexity), true + + case "Identity.avatarUrl": + if e.complexity.Identity.AvatarUrl == nil { + break + } + + return e.complexity.Identity.AvatarUrl(childComplexity), true + case "LabelChangeOperation.hash": if e.complexity.LabelChangeOperation.Hash == nil { break @@ -1677,41 +1705,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PageInfo.EndCursor(childComplexity), true - case "Person.name": - if e.complexity.Person.Name == nil { - break - } - - return e.complexity.Person.Name(childComplexity), true - - case "Person.email": - if e.complexity.Person.Email == nil { - break - } - - return e.complexity.Person.Email(childComplexity), true - - case "Person.login": - if e.complexity.Person.Login == nil { - break - } - - return e.complexity.Person.Login(childComplexity), true - - case "Person.displayName": - if e.complexity.Person.DisplayName == nil { - break - } - - return e.complexity.Person.DisplayName(childComplexity), true - - case "Person.avatarUrl": - if e.complexity.Person.AvatarUrl == nil { - break - } - - return e.complexity.Person.AvatarUrl(childComplexity), true - case "Query.defaultRepository": if e.complexity.Query.DefaultRepository == nil { break @@ -2072,11 +2065,18 @@ func (ec *executionContext) _AddCommentOperation_author(ctx context.Context, fie } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -2296,11 +2296,18 @@ func (ec *executionContext) _AddCommentTimelineItem_author(ctx context.Context, } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -2800,11 +2807,18 @@ func (ec *executionContext) _Bug_author(ctx context.Context, field graphql.Colle } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -3334,11 +3348,18 @@ func (ec *executionContext) _Comment_author(ctx context.Context, field graphql.C } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -3916,11 +3937,18 @@ func (ec *executionContext) _CreateOperation_author(ctx context.Context, field g } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -4167,11 +4195,18 @@ func (ec *executionContext) _CreateTimelineItem_author(ctx context.Context, fiel } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -4513,11 +4548,18 @@ func (ec *executionContext) _EditCommentOperation_author(ctx context.Context, fi } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -4637,6 +4679,167 @@ func (ec *executionContext) _EditCommentOperation_files(ctx context.Context, fie return arr1 } +var identityImplementors = []string{"Identity"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj *identity.Identity) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, identityImplementors) + + out := graphql.NewOrderedMap(len(fields)) + invalid := false + for i, field := range fields { + out.Keys[i] = field.Alias + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Identity") + case "name": + out.Values[i] = ec._Identity_name(ctx, field, obj) + case "email": + out.Values[i] = ec._Identity_email(ctx, field, obj) + case "login": + out.Values[i] = ec._Identity_login(ctx, field, obj) + case "displayName": + out.Values[i] = ec._Identity_displayName(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "avatarUrl": + out.Values[i] = ec._Identity_avatarUrl(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + if invalid { + return graphql.Null + } + return out +} + +// nolint: vetshadow +func (ec *executionContext) _Identity_name(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Identity", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return graphql.MarshalString(res) +} + +// nolint: vetshadow +func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Identity", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Email(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return graphql.MarshalString(res) +} + +// nolint: vetshadow +func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Identity", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Login(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return graphql.MarshalString(res) +} + +// nolint: vetshadow +func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Identity", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.DisplayName(), nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return graphql.MarshalString(res) +} + +// nolint: vetshadow +func (ec *executionContext) _Identity_avatarUrl(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Identity", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.AvatarURL(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return graphql.MarshalString(res) +} + var labelChangeOperationImplementors = []string{"LabelChangeOperation", "Operation", "Authored"} // nolint: gocyclo, errcheck, gas, goconst @@ -4740,11 +4943,18 @@ func (ec *executionContext) _LabelChangeOperation_author(ctx context.Context, fi } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -4949,11 +5159,18 @@ func (ec *executionContext) _LabelChangeTimelineItem_author(ctx context.Context, } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -5820,200 +6037,6 @@ func (ec *executionContext) _PageInfo_endCursor(ctx context.Context, field graph return graphql.MarshalString(res) } -var personImplementors = []string{"Person"} - -// nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _Person(ctx context.Context, sel ast.SelectionSet, obj *bug.Person) graphql.Marshaler { - fields := graphql.CollectFields(ctx, sel, personImplementors) - - var wg sync.WaitGroup - out := graphql.NewOrderedMap(len(fields)) - invalid := false - for i, field := range fields { - out.Keys[i] = field.Alias - - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("Person") - case "name": - wg.Add(1) - go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Person_name(ctx, field, obj) - wg.Done() - }(i, field) - case "email": - wg.Add(1) - go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Person_email(ctx, field, obj) - wg.Done() - }(i, field) - case "login": - wg.Add(1) - go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Person_login(ctx, field, obj) - wg.Done() - }(i, field) - case "displayName": - out.Values[i] = ec._Person_displayName(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalid = true - } - case "avatarUrl": - wg.Add(1) - go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Person_avatarUrl(ctx, field, obj) - wg.Done() - }(i, field) - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - wg.Wait() - if invalid { - return graphql.Null - } - return out -} - -// nolint: vetshadow -func (ec *executionContext) _Person_name(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler { - ctx = ec.Tracer.StartFieldExecution(ctx, field) - defer func() { ec.Tracer.EndFieldExecution(ctx) }() - rctx := &graphql.ResolverContext{ - Object: "Person", - Args: nil, - Field: field, - } - ctx = graphql.WithResolverContext(ctx, rctx) - ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) - resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Person().Name(rctx, obj) - }) - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - rctx.Result = res - ctx = ec.Tracer.StartFieldChildExecution(ctx) - - if res == nil { - return graphql.Null - } - return graphql.MarshalString(*res) -} - -// nolint: vetshadow -func (ec *executionContext) _Person_email(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler { - ctx = ec.Tracer.StartFieldExecution(ctx, field) - defer func() { ec.Tracer.EndFieldExecution(ctx) }() - rctx := &graphql.ResolverContext{ - Object: "Person", - Args: nil, - Field: field, - } - ctx = graphql.WithResolverContext(ctx, rctx) - ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) - resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Person().Email(rctx, obj) - }) - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - rctx.Result = res - ctx = ec.Tracer.StartFieldChildExecution(ctx) - - if res == nil { - return graphql.Null - } - return graphql.MarshalString(*res) -} - -// nolint: vetshadow -func (ec *executionContext) _Person_login(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler { - ctx = ec.Tracer.StartFieldExecution(ctx, field) - defer func() { ec.Tracer.EndFieldExecution(ctx) }() - rctx := &graphql.ResolverContext{ - Object: "Person", - Args: nil, - Field: field, - } - ctx = graphql.WithResolverContext(ctx, rctx) - ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) - resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Person().Login(rctx, obj) - }) - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - rctx.Result = res - ctx = ec.Tracer.StartFieldChildExecution(ctx) - - if res == nil { - return graphql.Null - } - return graphql.MarshalString(*res) -} - -// nolint: vetshadow -func (ec *executionContext) _Person_displayName(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler { - ctx = ec.Tracer.StartFieldExecution(ctx, field) - defer func() { ec.Tracer.EndFieldExecution(ctx) }() - rctx := &graphql.ResolverContext{ - Object: "Person", - Args: nil, - Field: field, - } - ctx = graphql.WithResolverContext(ctx, rctx) - ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) - resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.DisplayName(), nil - }) - if resTmp == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - rctx.Result = res - ctx = ec.Tracer.StartFieldChildExecution(ctx) - return graphql.MarshalString(res) -} - -// nolint: vetshadow -func (ec *executionContext) _Person_avatarUrl(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler { - ctx = ec.Tracer.StartFieldExecution(ctx, field) - defer func() { ec.Tracer.EndFieldExecution(ctx) }() - rctx := &graphql.ResolverContext{ - Object: "Person", - Args: nil, - Field: field, - } - ctx = graphql.WithResolverContext(ctx, rctx) - ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) - resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Person().AvatarURL(rctx, obj) - }) - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - rctx.Result = res - ctx = ec.Tracer.StartFieldChildExecution(ctx) - - if res == nil { - return graphql.Null - } - return graphql.MarshalString(*res) -} - var queryImplementors = []string{"Query"} // nolint: gocyclo, errcheck, gas, goconst @@ -6400,11 +6423,18 @@ func (ec *executionContext) _SetStatusOperation_author(ctx context.Context, fiel } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -6563,11 +6593,18 @@ func (ec *executionContext) _SetStatusTimelineItem_author(ctx context.Context, f } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -6727,11 +6764,18 @@ func (ec *executionContext) _SetTitleOperation_author(ctx context.Context, field } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -6918,11 +6962,18 @@ func (ec *executionContext) _SetTitleTimelineItem_author(ctx context.Context, fi } return graphql.Null } - res := resTmp.(bug.Person) + res := resTmp.(*identity.Identity) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec._Person(ctx, field.Selections, &res) + if res == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res) } // nolint: vetshadow @@ -8862,24 +8913,10 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er } var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "bug.graphql", Input: `"""Represents an person""" -type Person { - """The name of the person, if known.""" - name: String - """The email of the person, if known.""" - email: String - """The login of the person, if known.""" - login: String - """A string containing the either the name of the person, its login or both""" - displayName: String! - """An url to an avatar""" - avatarUrl: String -} - -"""Represents a comment on a bug.""" + &ast.Source{Name: "schema/bug.graphql", Input: `"""Represents a comment on a bug.""" type Comment implements Authored { """The author of this comment.""" - author: Person! + author: Identity! """The message of this comment.""" message: String! @@ -8911,7 +8948,7 @@ type Bug { status: Status! title: String! labels: [Label!]! - author: Person! + author: Identity! createdAt: Time! lastEdit: Time! @@ -8985,12 +9022,25 @@ type Repository { } `}, - &ast.Source{Name: "operations.graphql", Input: `"""An operation applied to a bug.""" + &ast.Source{Name: "schema/identity.graphql", Input: `"""Represents an identity""" +type Identity { + """The name of the person, if known.""" + name: String + """The email of the person, if known.""" + email: String + """The login of the person, if known.""" + login: String + """A string containing the either the name of the person, its login or both""" + displayName: String! + """An url to an avatar""" + avatarUrl: String +}`}, + &ast.Source{Name: "schema/operations.graphql", Input: `"""An operation applied to a bug.""" interface Operation { """The hash of the operation""" hash: Hash! """The operations author.""" - author: Person! + author: Identity! """The datetime when this operation was issued.""" date: Time! } @@ -9017,7 +9067,7 @@ type CreateOperation implements Operation & Authored { """The hash of the operation""" hash: Hash! """The author of this object.""" - author: Person! + author: Identity! """The datetime when this operation was issued.""" date: Time! @@ -9030,7 +9080,7 @@ type SetTitleOperation implements Operation & Authored { """The hash of the operation""" hash: Hash! """The author of this object.""" - author: Person! + author: Identity! """The datetime when this operation was issued.""" date: Time! @@ -9042,7 +9092,7 @@ type AddCommentOperation implements Operation & Authored { """The hash of the operation""" hash: Hash! """The author of this object.""" - author: Person! + author: Identity! """The datetime when this operation was issued.""" date: Time! @@ -9054,7 +9104,7 @@ type EditCommentOperation implements Operation & Authored { """The hash of the operation""" hash: Hash! """The author of this object.""" - author: Person! + author: Identity! """The datetime when this operation was issued.""" date: Time! @@ -9067,7 +9117,7 @@ type SetStatusOperation implements Operation & Authored { """The hash of the operation""" hash: Hash! """The author of this object.""" - author: Person! + author: Identity! """The datetime when this operation was issued.""" date: Time! @@ -9078,14 +9128,14 @@ type LabelChangeOperation implements Operation & Authored { """The hash of the operation""" hash: Hash! """The author of this object.""" - author: Person! + author: Identity! """The datetime when this operation was issued.""" date: Time! added: [Label!]! removed: [Label!]! }`}, - &ast.Source{Name: "root.graphql", Input: `scalar Time + &ast.Source{Name: "schema/root.graphql", Input: `scalar Time scalar Label scalar Hash @@ -9104,7 +9154,7 @@ type PageInfo { """An object that has an author.""" interface Authored { """The author of this object.""" - author: Person! + author: Identity! } type Query { @@ -9123,7 +9173,7 @@ type Mutation { commit(repoRef: String, prefix: String!): Bug! }`}, - &ast.Source{Name: "timeline.graphql", Input: `"""An item in the timeline of events""" + &ast.Source{Name: "schema/timeline.graphql", Input: `"""An item in the timeline of events""" interface TimelineItem { """The hash of the source operation""" hash: Hash! @@ -9157,7 +9207,7 @@ type TimelineItemEdge { type CreateTimelineItem implements TimelineItem { """The hash of the source operation""" hash: Hash! - author: Person! + author: Identity! message: String! messageIsEmpty: Boolean! files: [Hash!]! @@ -9171,7 +9221,7 @@ type CreateTimelineItem implements TimelineItem { type AddCommentTimelineItem implements TimelineItem { """The hash of the source operation""" hash: Hash! - author: Person! + author: Identity! message: String! messageIsEmpty: Boolean! files: [Hash!]! @@ -9185,7 +9235,7 @@ type AddCommentTimelineItem implements TimelineItem { type LabelChangeTimelineItem implements TimelineItem { """The hash of the source operation""" hash: Hash! - author: Person! + author: Identity! date: Time! added: [Label!]! removed: [Label!]! @@ -9195,7 +9245,7 @@ type LabelChangeTimelineItem implements TimelineItem { type SetStatusTimelineItem implements TimelineItem { """The hash of the source operation""" hash: Hash! - author: Person! + author: Identity! date: Time! status: Status! } @@ -9204,7 +9254,7 @@ type SetStatusTimelineItem implements TimelineItem { type SetTitleTimelineItem implements TimelineItem { """The hash of the source operation""" hash: Hash! - author: Person! + author: Identity! date: Time! title: String! was: String! diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go new file mode 100644 index 00000000..90381987 --- /dev/null +++ b/graphql/graphql_test.go @@ -0,0 +1,148 @@ +package graphql + +import ( + "net/http/httptest" + "testing" + + "github.com/MichaelMure/git-bug/graphql/models" + "github.com/MichaelMure/git-bug/util/test" + "github.com/vektah/gqlgen/client" +) + +func TestQueries(t *testing.T) { + repo := test.CreateFilledRepo(10) + + handler, err := NewHandler(repo) + if err != nil { + t.Fatal(err) + } + + srv := httptest.NewServer(handler) + c := client.New(srv.URL) + + query := ` + query { + defaultRepository { + allBugs(first: 2) { + pageInfo { + endCursor + hasNextPage + startCursor + hasPreviousPage + } + nodes{ + author { + name + email + avatarUrl + } + + createdAt + humanId + id + lastEdit + status + title + + comments(first: 2) { + pageInfo { + endCursor + hasNextPage + startCursor + hasPreviousPage + } + nodes { + files + message + } + } + + operations(first: 20) { + pageInfo { + endCursor + hasNextPage + startCursor + hasPreviousPage + } + nodes { + author { + name + email + avatarUrl + } + date + ... on CreateOperation { + title + message + files + } + ... on SetTitleOperation { + title + was + } + ... on AddCommentOperation { + files + message + } + ... on SetStatusOperation { + status + } + ... on LabelChangeOperation { + added + removed + } + } + } + } + } + } + }` + + type Person struct { + Name string `json:"name"` + Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` + } + + var resp struct { + DefaultRepository struct { + AllBugs struct { + PageInfo models.PageInfo + Nodes []struct { + Author Person + CreatedAt string `json:"createdAt"` + HumanId string `json:"humanId"` + Id string + LastEdit string `json:"lastEdit"` + Status string + Title string + + Comments struct { + PageInfo models.PageInfo + Nodes []struct { + Files []string + Message string + } + } + + Operations struct { + PageInfo models.PageInfo + Nodes []struct { + Author Person + Date string + Title string + Files []string + Message string + Was string + Status string + Added []string + Removed []string + } + } + } + } + } + } + + c.MustPost(query, &resp) +} diff --git a/graphql/operations.graphql b/graphql/operations.graphql deleted file mode 100644 index 420a9e12..00000000 --- a/graphql/operations.graphql +++ /dev/null @@ -1,100 +0,0 @@ -"""An operation applied to a bug.""" -interface Operation { - """The hash of the operation""" - hash: Hash! - """The operations author.""" - author: Person! - """The datetime when this operation was issued.""" - date: Time! -} - -# Connection - -"""The connection type for an Operation""" -type OperationConnection { - edges: [OperationEdge!]! - nodes: [Operation!]! - pageInfo: PageInfo! - totalCount: Int! -} - -"""Represent an Operation""" -type OperationEdge { - cursor: String! - node: Operation! -} - -# Operations - -type CreateOperation implements Operation & Authored { - """The hash of the operation""" - hash: Hash! - """The author of this object.""" - author: Person! - """The datetime when this operation was issued.""" - date: Time! - - title: String! - message: String! - files: [Hash!]! -} - -type SetTitleOperation implements Operation & Authored { - """The hash of the operation""" - hash: Hash! - """The author of this object.""" - author: Person! - """The datetime when this operation was issued.""" - date: Time! - - title: String! - was: String! -} - -type AddCommentOperation implements Operation & Authored { - """The hash of the operation""" - hash: Hash! - """The author of this object.""" - author: Person! - """The datetime when this operation was issued.""" - date: Time! - - message: String! - files: [Hash!]! -} - -type EditCommentOperation implements Operation & Authored { - """The hash of the operation""" - hash: Hash! - """The author of this object.""" - author: Person! - """The datetime when this operation was issued.""" - date: Time! - - target: Hash! - message: String! - files: [Hash!]! -} - -type SetStatusOperation implements Operation & Authored { - """The hash of the operation""" - hash: Hash! - """The author of this object.""" - author: Person! - """The datetime when this operation was issued.""" - date: Time! - - status: Status! -} - -type LabelChangeOperation implements Operation & Authored { - """The hash of the operation""" - hash: Hash! - """The author of this object.""" - author: Person! - """The datetime when this operation was issued.""" - date: Time! - - added: [Label!]! - removed: [Label!]! -} \ No newline at end of file diff --git a/graphql/resolvers/person.go b/graphql/resolvers/person.go deleted file mode 100644 index bb4bcb0d..00000000 --- a/graphql/resolvers/person.go +++ /dev/null @@ -1,37 +0,0 @@ -package resolvers - -import ( - "context" - - "github.com/MichaelMure/git-bug/bug" -) - -type personResolver struct{} - -func (personResolver) Name(ctx context.Context, obj *bug.Person) (*string, error) { - if obj.Name == "" { - return nil, nil - } - return &obj.Name, nil -} - -func (personResolver) Email(ctx context.Context, obj *bug.Person) (*string, error) { - if obj.Email == "" { - return nil, nil - } - return &obj.Email, nil -} - -func (personResolver) Login(ctx context.Context, obj *bug.Person) (*string, error) { - if obj.Login == "" { - return nil, nil - } - return &obj.Login, nil -} - -func (personResolver) AvatarURL(ctx context.Context, obj *bug.Person) (*string, error) { - if obj.AvatarUrl == "" { - return nil, nil - } - return &obj.AvatarUrl, nil -} diff --git a/graphql/resolvers/root.go b/graphql/resolvers/root.go index d7bd6021..9b3a730b 100644 --- a/graphql/resolvers/root.go +++ b/graphql/resolvers/root.go @@ -32,10 +32,6 @@ func (RootResolver) Bug() graph.BugResolver { return &bugResolver{} } -func (r RootResolver) Person() graph.PersonResolver { - return &personResolver{} -} - func (RootResolver) CommentHistoryStep() graph.CommentHistoryStepResolver { return &commentHistoryStepResolver{} } diff --git a/graphql/root.graphql b/graphql/root.graphql deleted file mode 100644 index fd8419fa..00000000 --- a/graphql/root.graphql +++ /dev/null @@ -1,38 +0,0 @@ -scalar Time -scalar Label -scalar Hash - -"""Information about pagination in a connection.""" -type PageInfo { - """When paginating forwards, are there more items?""" - hasNextPage: Boolean! - """When paginating backwards, are there more items?""" - hasPreviousPage: Boolean! - """When paginating backwards, the cursor to continue.""" - startCursor: String! - """When paginating forwards, the cursor to continue.""" - endCursor: String! -} - -"""An object that has an author.""" -interface Authored { - """The author of this object.""" - author: Person! -} - -type Query { - defaultRepository: Repository - repository(id: String!): Repository -} - -type Mutation { - newBug(repoRef: String, title: String!, message: String!, files: [Hash!]): Bug! - - addComment(repoRef: String, prefix: String!, message: String!, files: [Hash!]): Bug! - changeLabels(repoRef: String, prefix: String!, added: [String!], removed: [String!]): Bug! - open(repoRef: String, prefix: String!): Bug! - close(repoRef: String, prefix: String!): Bug! - setTitle(repoRef: String, prefix: String!, title: String!): Bug! - - commit(repoRef: String, prefix: String!): Bug! -} \ No newline at end of file diff --git a/graphql/schema/bug.graphql b/graphql/schema/bug.graphql new file mode 100644 index 00000000..9530c576 --- /dev/null +++ b/graphql/schema/bug.graphql @@ -0,0 +1,108 @@ +"""Represents a comment on a bug.""" +type Comment implements Authored { + """The author of this comment.""" + author: Identity! + + """The message of this comment.""" + message: String! + + """All media's hash referenced in this comment""" + files: [Hash!]! +} + +type CommentConnection { + edges: [CommentEdge!]! + nodes: [Comment!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type CommentEdge { + cursor: String! + node: Comment! +} + +enum Status { + OPEN + CLOSED +} + +type Bug { + id: String! + humanId: String! + status: Status! + title: String! + labels: [Label!]! + author: Identity! + createdAt: Time! + lastEdit: Time! + + comments( + """Returns the elements in the list that come after the specified cursor.""" + after: String + """Returns the elements in the list that come before the specified cursor.""" + before: String + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + ): CommentConnection! + + timeline( + """Returns the elements in the list that come after the specified cursor.""" + after: String + """Returns the elements in the list that come before the specified cursor.""" + before: String + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + ): TimelineItemConnection! + + operations( + """Returns the elements in the list that come after the specified cursor.""" + after: String + """Returns the elements in the list that come before the specified cursor.""" + before: String + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + ): OperationConnection! +} + +"""The connection type for Bug.""" +type BugConnection { + """A list of edges.""" + edges: [BugEdge!]! + nodes: [Bug!]! + """Information to aid in pagination.""" + pageInfo: PageInfo! + """Identifies the total count of items in the connection.""" + totalCount: Int! +} + +"""An edge in a connection.""" +type BugEdge { + """A cursor for use in pagination.""" + cursor: String! + """The item at the end of the edge.""" + node: Bug! +} + +type Repository { + allBugs( + """Returns the elements in the list that come after the specified cursor.""" + after: String + """Returns the elements in the list that come before the specified cursor.""" + before: String + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + """A query to select and order bugs""" + query: String + ): BugConnection! + bug(prefix: String!): Bug +} + diff --git a/graphql/schema/identity.graphql b/graphql/schema/identity.graphql new file mode 100644 index 00000000..9e76d885 --- /dev/null +++ b/graphql/schema/identity.graphql @@ -0,0 +1,13 @@ +"""Represents an identity""" +type Identity { + """The name of the person, if known.""" + name: String + """The email of the person, if known.""" + email: String + """The login of the person, if known.""" + login: String + """A string containing the either the name of the person, its login or both""" + displayName: String! + """An url to an avatar""" + avatarUrl: String +} \ No newline at end of file diff --git a/graphql/schema/operations.graphql b/graphql/schema/operations.graphql new file mode 100644 index 00000000..2b206418 --- /dev/null +++ b/graphql/schema/operations.graphql @@ -0,0 +1,100 @@ +"""An operation applied to a bug.""" +interface Operation { + """The hash of the operation""" + hash: Hash! + """The operations author.""" + author: Identity! + """The datetime when this operation was issued.""" + date: Time! +} + +# Connection + +"""The connection type for an Operation""" +type OperationConnection { + edges: [OperationEdge!]! + nodes: [Operation!]! + pageInfo: PageInfo! + totalCount: Int! +} + +"""Represent an Operation""" +type OperationEdge { + cursor: String! + node: Operation! +} + +# Operations + +type CreateOperation implements Operation & Authored { + """The hash of the operation""" + hash: Hash! + """The author of this object.""" + author: Identity! + """The datetime when this operation was issued.""" + date: Time! + + title: String! + message: String! + files: [Hash!]! +} + +type SetTitleOperation implements Operation & Authored { + """The hash of the operation""" + hash: Hash! + """The author of this object.""" + author: Identity! + """The datetime when this operation was issued.""" + date: Time! + + title: String! + was: String! +} + +type AddCommentOperation implements Operation & Authored { + """The hash of the operation""" + hash: Hash! + """The author of this object.""" + author: Identity! + """The datetime when this operation was issued.""" + date: Time! + + message: String! + files: [Hash!]! +} + +type EditCommentOperation implements Operation & Authored { + """The hash of the operation""" + hash: Hash! + """The author of this object.""" + author: Identity! + """The datetime when this operation was issued.""" + date: Time! + + target: Hash! + message: String! + files: [Hash!]! +} + +type SetStatusOperation implements Operation & Authored { + """The hash of the operation""" + hash: Hash! + """The author of this object.""" + author: Identity! + """The datetime when this operation was issued.""" + date: Time! + + status: Status! +} + +type LabelChangeOperation implements Operation & Authored { + """The hash of the operation""" + hash: Hash! + """The author of this object.""" + author: Identity! + """The datetime when this operation was issued.""" + date: Time! + + added: [Label!]! + removed: [Label!]! +} \ No newline at end of file diff --git a/graphql/schema/root.graphql b/graphql/schema/root.graphql new file mode 100644 index 00000000..56558f7c --- /dev/null +++ b/graphql/schema/root.graphql @@ -0,0 +1,38 @@ +scalar Time +scalar Label +scalar Hash + +"""Information about pagination in a connection.""" +type PageInfo { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + """When paginating backwards, the cursor to continue.""" + startCursor: String! + """When paginating forwards, the cursor to continue.""" + endCursor: String! +} + +"""An object that has an author.""" +interface Authored { + """The author of this object.""" + author: Identity! +} + +type Query { + defaultRepository: Repository + repository(id: String!): Repository +} + +type Mutation { + newBug(repoRef: String, title: String!, message: String!, files: [Hash!]): Bug! + + addComment(repoRef: String, prefix: String!, message: String!, files: [Hash!]): Bug! + changeLabels(repoRef: String, prefix: String!, added: [String!], removed: [String!]): Bug! + open(repoRef: String, prefix: String!): Bug! + close(repoRef: String, prefix: String!): Bug! + setTitle(repoRef: String, prefix: String!, title: String!): Bug! + + commit(repoRef: String, prefix: String!): Bug! +} \ No newline at end of file diff --git a/graphql/schema/timeline.graphql b/graphql/schema/timeline.graphql new file mode 100644 index 00000000..29ed6e60 --- /dev/null +++ b/graphql/schema/timeline.graphql @@ -0,0 +1,86 @@ +"""An item in the timeline of events""" +interface TimelineItem { + """The hash of the source operation""" + hash: Hash! +} + +"""CommentHistoryStep hold one version of a message in the history""" +type CommentHistoryStep { + message: String! + date: Time! +} + +# Connection + +"""The connection type for TimelineItem""" +type TimelineItemConnection { + edges: [TimelineItemEdge!]! + nodes: [TimelineItem!]! + pageInfo: PageInfo! + totalCount: Int! +} + +"""Represent a TimelineItem""" +type TimelineItemEdge { + cursor: String! + node: TimelineItem! +} + +# Items + +"""CreateTimelineItem is a TimelineItem that represent the creation of a bug and its message edition history""" +type CreateTimelineItem implements TimelineItem { + """The hash of the source operation""" + hash: Hash! + author: Identity! + message: String! + messageIsEmpty: Boolean! + files: [Hash!]! + createdAt: Time! + lastEdit: Time! + edited: Boolean! + history: [CommentHistoryStep!]! +} + +"""AddCommentTimelineItem is a TimelineItem that represent a Comment and its edition history""" +type AddCommentTimelineItem implements TimelineItem { + """The hash of the source operation""" + hash: Hash! + author: Identity! + message: String! + messageIsEmpty: Boolean! + files: [Hash!]! + createdAt: Time! + lastEdit: Time! + edited: Boolean! + history: [CommentHistoryStep!]! +} + +"""LabelChangeTimelineItem is a TimelineItem that represent a change in the labels of a bug""" +type LabelChangeTimelineItem implements TimelineItem { + """The hash of the source operation""" + hash: Hash! + author: Identity! + date: Time! + added: [Label!]! + removed: [Label!]! +} + +"""SetStatusTimelineItem is a TimelineItem that represent a change in the status of a bug""" +type SetStatusTimelineItem implements TimelineItem { + """The hash of the source operation""" + hash: Hash! + author: Identity! + date: Time! + status: Status! +} + +"""LabelChangeTimelineItem is a TimelineItem that represent a change in the title of a bug""" +type SetTitleTimelineItem implements TimelineItem { + """The hash of the source operation""" + hash: Hash! + author: Identity! + date: Time! + title: String! + was: String! +} \ No newline at end of file diff --git a/graphql/timeline.graphql b/graphql/timeline.graphql deleted file mode 100644 index 75f72305..00000000 --- a/graphql/timeline.graphql +++ /dev/null @@ -1,86 +0,0 @@ -"""An item in the timeline of events""" -interface TimelineItem { - """The hash of the source operation""" - hash: Hash! -} - -"""CommentHistoryStep hold one version of a message in the history""" -type CommentHistoryStep { - message: String! - date: Time! -} - -# Connection - -"""The connection type for TimelineItem""" -type TimelineItemConnection { - edges: [TimelineItemEdge!]! - nodes: [TimelineItem!]! - pageInfo: PageInfo! - totalCount: Int! -} - -"""Represent a TimelineItem""" -type TimelineItemEdge { - cursor: String! - node: TimelineItem! -} - -# Items - -"""CreateTimelineItem is a TimelineItem that represent the creation of a bug and its message edition history""" -type CreateTimelineItem implements TimelineItem { - """The hash of the source operation""" - hash: Hash! - author: Person! - message: String! - messageIsEmpty: Boolean! - files: [Hash!]! - createdAt: Time! - lastEdit: Time! - edited: Boolean! - history: [CommentHistoryStep!]! -} - -"""AddCommentTimelineItem is a TimelineItem that represent a Comment and its edition history""" -type AddCommentTimelineItem implements TimelineItem { - """The hash of the source operation""" - hash: Hash! - author: Person! - message: String! - messageIsEmpty: Boolean! - files: [Hash!]! - createdAt: Time! - lastEdit: Time! - edited: Boolean! - history: [CommentHistoryStep!]! -} - -"""LabelChangeTimelineItem is a TimelineItem that represent a change in the labels of a bug""" -type LabelChangeTimelineItem implements TimelineItem { - """The hash of the source operation""" - hash: Hash! - author: Person! - date: Time! - added: [Label!]! - removed: [Label!]! -} - -"""SetStatusTimelineItem is a TimelineItem that represent a change in the status of a bug""" -type SetStatusTimelineItem implements TimelineItem { - """The hash of the source operation""" - hash: Hash! - author: Person! - date: Time! - status: Status! -} - -"""LabelChangeTimelineItem is a TimelineItem that represent a change in the title of a bug""" -type SetTitleTimelineItem implements TimelineItem { - """The hash of the source operation""" - hash: Hash! - author: Person! - date: Time! - title: String! - was: String! -} \ No newline at end of file diff --git a/identity/bare.go b/identity/bare.go new file mode 100644 index 00000000..eec00e19 --- /dev/null +++ b/identity/bare.go @@ -0,0 +1,144 @@ +package identity + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/MichaelMure/git-bug/util/lamport" + "github.com/MichaelMure/git-bug/util/text" +) + +type Bare struct { + name string + email string + login string + avatarUrl string +} + +func NewBare(name string, email string) *Bare { + return &Bare{name: name, email: email} +} + +func NewBareFull(name string, email string, login string, avatarUrl string) *Bare { + return &Bare{name: name, email: email, login: login, avatarUrl: avatarUrl} +} + +type bareIdentityJson struct { + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + Login string `json:"login,omitempty"` + AvatarUrl string `json:"avatar_url,omitempty"` +} + +func (i Bare) MarshalJSON() ([]byte, error) { + return json.Marshal(bareIdentityJson{ + Name: i.name, + Email: i.email, + Login: i.login, + AvatarUrl: i.avatarUrl, + }) +} + +func (i Bare) UnmarshalJSON(data []byte) error { + aux := bareIdentityJson{} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + i.name = aux.Name + i.email = aux.Email + i.login = aux.Login + i.avatarUrl = aux.AvatarUrl + + return nil +} + +func (i Bare) Name() string { + return i.name +} + +func (i Bare) Email() string { + return i.email +} + +func (i Bare) Login() string { + return i.login +} + +func (i Bare) AvatarUrl() string { + return i.avatarUrl +} + +func (i Bare) Keys() []Key { + return []Key{} +} + +func (i Bare) ValidKeysAtTime(time lamport.Time) []Key { + return []Key{} +} + +// DisplayName return a non-empty string to display, representing the +// identity, based on the non-empty values. +func (i Bare) 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") +} + +// Match tell is the Person match the given query string +func (i Bare) Match(query string) bool { + query = strings.ToLower(query) + + return strings.Contains(strings.ToLower(i.name), query) || + strings.Contains(strings.ToLower(i.login), query) +} + +// Validate check if the Identity data is valid +func (i Bare) Validate() error { + if text.Empty(i.name) && text.Empty(i.login) { + return fmt.Errorf("either name or login should be set") + } + + if strings.Contains(i.name, "\n") { + return fmt.Errorf("name should be a single line") + } + + if !text.Safe(i.name) { + return fmt.Errorf("name is not fully printable") + } + + if strings.Contains(i.login, "\n") { + return fmt.Errorf("login should be a single line") + } + + if !text.Safe(i.login) { + return fmt.Errorf("login is not fully printable") + } + + if strings.Contains(i.email, "\n") { + return fmt.Errorf("email should be a single line") + } + + if !text.Safe(i.email) { + return fmt.Errorf("email is not fully printable") + } + + if i.avatarUrl != "" && !text.ValidUrl(i.avatarUrl) { + return fmt.Errorf("avatarUrl is not a valid URL") + } + + return nil +} + +func (i Bare) IsProtected() bool { + return false +} diff --git a/identity/identity.go b/identity/identity.go new file mode 100644 index 00000000..f65e2a86 --- /dev/null +++ b/identity/identity.go @@ -0,0 +1,285 @@ +// Package identity contains the identity data model and low-level related functions +package identity + +import ( + "fmt" + "strings" + + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/git" + "github.com/MichaelMure/git-bug/util/lamport" +) + +const identityRefPattern = "refs/identities/" +const versionEntryName = "version" +const identityConfigKey = "git-bug.identity" + +type Identity struct { + id string + Versions []Version +} + +func NewIdentity(name string, email string) (*Identity, error) { + return &Identity{ + Versions: []Version{ + { + Name: name, + Email: email, + Nonce: makeNonce(20), + }, + }, + }, nil +} + +type identityJson struct { + Id string `json:"id"` +} + +// TODO: marshal/unmarshal identity + load/write from OpBase + +func Read(repo repository.Repo, id string) (*Identity, error) { + // Todo + return &Identity{}, nil +} + +// NewFromGitUser will query the repository for user detail and +// build the corresponding Identity +/*func NewFromGitUser(repo repository.Repo) (*Identity, error) { + name, err := repo.GetUserName() + if err != nil { + return nil, err + } + if name == "" { + return nil, 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 nil, err + } + if email == "" { + return nil, errors.New("User name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`") + } + + return NewIdentity(name, email) +}*/ + +// +func BuildFromGit(repo repository.Repo) *Identity { + version := Version{} + + name, err := repo.GetUserName() + if err == nil { + version.Name = name + } + + email, err := repo.GetUserEmail() + if err == nil { + version.Email = email + } + + return &Identity{ + Versions: []Version{ + version, + }, + } +} + +// SetIdentity store the user identity's id in the git config +func SetIdentity(repo repository.RepoCommon, identity Identity) error { + return repo.StoreConfig(identityConfigKey, identity.Id()) +} + +// GetIdentity read the current user identity, set with a git config entry +func GetIdentity(repo repository.Repo) (*Identity, error) { + configs, err := repo.ReadConfigs(identityConfigKey) + if err != nil { + return nil, err + } + + if len(configs) == 0 { + return nil, fmt.Errorf("no identity set") + } + + if len(configs) > 1 { + return nil, fmt.Errorf("multiple identity config exist") + } + + var id string + for _, val := range configs { + id = val + } + + return Read(repo, id) +} + +func (i *Identity) AddVersion(version Version) { + i.Versions = append(i.Versions, version) +} + +func (i *Identity) Commit(repo repository.ClockedRepo) error { + // Todo: check for mismatch between memory and commited data + + var lastCommit git.Hash = "" + + for _, v := range i.Versions { + if v.commitHash != "" { + lastCommit = v.commitHash + // ignore already commited versions + continue + } + + blobHash, err := v.Write(repo) + if err != nil { + return err + } + + // Make a git tree referencing the blob + tree := []repository.TreeEntry{ + {ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName}, + } + + treeHash, err := repo.StoreTree(tree) + if err != nil { + return err + } + + var commitHash git.Hash + if lastCommit != "" { + commitHash, err = repo.StoreCommitWithParent(treeHash, lastCommit) + } else { + commitHash, err = repo.StoreCommit(treeHash) + } + + if err != nil { + return err + } + + lastCommit = commitHash + + // if it was the first commit, use the commit hash as the Identity id + if i.id == "" { + i.id = string(commitHash) + } + } + + if i.id == "" { + panic("identity with no id") + } + + ref := fmt.Sprintf("%s%s", identityRefPattern, i.id) + err := repo.UpdateRef(ref, lastCommit) + + if err != nil { + return err + } + + return nil +} + +// Validate check if the Identity data is valid +func (i *Identity) Validate() error { + lastTime := lamport.Time(0) + + for _, v := range i.Versions { + if err := v.Validate(); err != nil { + return err + } + + if v.Time < lastTime { + return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.Time) + } + + lastTime = v.Time + } + + return nil +} + +func (i *Identity) LastVersion() Version { + if len(i.Versions) <= 0 { + panic("no version at all") + } + + return i.Versions[len(i.Versions)-1] +} + +// Id return the Identity identifier +func (i *Identity) Id() string { + if i.id == "" { + // simply panic as it would be a coding error + // (using an id of an identity not stored yet) + panic("no id yet") + } + return i.id +} + +// Name return the last version of the name +func (i *Identity) Name() string { + return i.LastVersion().Name +} + +// Email return the last version of the email +func (i *Identity) Email() string { + return i.LastVersion().Email +} + +// Login return the last version of the login +func (i *Identity) Login() string { + return i.LastVersion().Login +} + +// Login return the last version of the Avatar URL +func (i *Identity) AvatarUrl() string { + return i.LastVersion().AvatarUrl +} + +// Login return the last version of the valid keys +func (i *Identity) Keys() []Key { + return i.LastVersion().Keys +} + +// IsProtected return true if the chain of git commits started to be signed. +// If that's the case, only signed commit with a valid key for this identity can be added. +func (i *Identity) IsProtected() bool { + // Todo + return false +} + +// ValidKeysAtTime return the set of keys valid at a given lamport time +func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key { + var result []Key + + for _, v := range i.Versions { + if v.Time > time { + return result + } + + result = v.Keys + } + + return result +} + +// Match tell is the Identity match the given query string +func (i *Identity) Match(query string) bool { + query = strings.ToLower(query) + + return strings.Contains(strings.ToLower(i.Name()), query) || + strings.Contains(strings.ToLower(i.Login()), query) +} + +// DisplayName return a non-empty string to display, representing the +// identity, based on the non-empty values. +func (i *Identity) 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") +} diff --git a/identity/identity_test.go b/identity/identity_test.go new file mode 100644 index 00000000..161fd56f --- /dev/null +++ b/identity/identity_test.go @@ -0,0 +1,145 @@ +package identity + +import ( + "testing" + + "github.com/MichaelMure/git-bug/repository" + "github.com/stretchr/testify/assert" +) + +func TestIdentityCommit(t *testing.T) { + mockRepo := repository.NewMockRepoForTest() + + // single version + + identity := Identity{ + Versions: []Version{ + { + Name: "René Descartes", + Email: "rene.descartes@example.com", + }, + }, + } + + err := identity.Commit(mockRepo) + + assert.Nil(t, err) + assert.NotEmpty(t, identity.id) + + // multiple version + + identity = Identity{ + Versions: []Version{ + { + Time: 100, + Name: "René Descartes", + Email: "rene.descartes@example.com", + Keys: []Key{ + {PubKey: "pubkeyA"}, + }, + }, + { + Time: 200, + Name: "René Descartes", + Email: "rene.descartes@example.com", + Keys: []Key{ + {PubKey: "pubkeyB"}, + }, + }, + { + Time: 201, + Name: "René Descartes", + Email: "rene.descartes@example.com", + Keys: []Key{ + {PubKey: "pubkeyC"}, + }, + }, + }, + } + + err = identity.Commit(mockRepo) + + assert.Nil(t, err) + assert.NotEmpty(t, identity.id) + + // add more version + + identity.AddVersion(Version{ + Time: 201, + Name: "René Descartes", + Email: "rene.descartes@example.com", + Keys: []Key{ + {PubKey: "pubkeyD"}, + }, + }) + + identity.AddVersion(Version{ + Time: 300, + Name: "René Descartes", + Email: "rene.descartes@example.com", + Keys: []Key{ + {PubKey: "pubkeyE"}, + }, + }) + + err = identity.Commit(mockRepo) + + assert.Nil(t, err) + assert.NotEmpty(t, identity.id) +} + +func TestIdentity_ValidKeysAtTime(t *testing.T) { + identity := Identity{ + Versions: []Version{ + { + Time: 100, + Name: "René Descartes", + Email: "rene.descartes@example.com", + Keys: []Key{ + {PubKey: "pubkeyA"}, + }, + }, + { + Time: 200, + Name: "René Descartes", + Email: "rene.descartes@example.com", + Keys: []Key{ + {PubKey: "pubkeyB"}, + }, + }, + { + Time: 201, + Name: "René Descartes", + Email: "rene.descartes@example.com", + Keys: []Key{ + {PubKey: "pubkeyC"}, + }, + }, + { + Time: 201, + Name: "René Descartes", + Email: "rene.descartes@example.com", + Keys: []Key{ + {PubKey: "pubkeyD"}, + }, + }, + { + Time: 300, + Name: "René Descartes", + Email: "rene.descartes@example.com", + Keys: []Key{ + {PubKey: "pubkeyE"}, + }, + }, + }, + } + + assert.Nil(t, identity.ValidKeysAtTime(10)) + assert.Equal(t, identity.ValidKeysAtTime(100), []Key{{PubKey: "pubkeyA"}}) + assert.Equal(t, identity.ValidKeysAtTime(140), []Key{{PubKey: "pubkeyA"}}) + assert.Equal(t, identity.ValidKeysAtTime(200), []Key{{PubKey: "pubkeyB"}}) + assert.Equal(t, identity.ValidKeysAtTime(201), []Key{{PubKey: "pubkeyD"}}) + assert.Equal(t, identity.ValidKeysAtTime(202), []Key{{PubKey: "pubkeyD"}}) + assert.Equal(t, identity.ValidKeysAtTime(300), []Key{{PubKey: "pubkeyE"}}) + assert.Equal(t, identity.ValidKeysAtTime(3000), []Key{{PubKey: "pubkeyE"}}) +} diff --git a/identity/interface.go b/identity/interface.go new file mode 100644 index 00000000..14287655 --- /dev/null +++ b/identity/interface.go @@ -0,0 +1,30 @@ +package identity + +import "github.com/MichaelMure/git-bug/util/lamport" + +type Interface interface { + Name() string + Email() string + Login() string + AvatarUrl() string + + // Login return the last version of the valid keys + Keys() []Key + + // ValidKeysAtTime return the set of keys valid at a given lamport time + ValidKeysAtTime(time lamport.Time) []Key + + // DisplayName return a non-empty string to display, representing the + // identity, based on the non-empty values. + DisplayName() string + + // Match tell is the Person match the given query string + Match(query string) bool + + // Validate check if the Identity data is valid + Validate() error + + // IsProtected return true if the chain of git commits started to be signed. + // If that's the case, only signed commit with a valid key for this identity can be added. + IsProtected() bool +} diff --git a/identity/key.go b/identity/key.go new file mode 100644 index 00000000..c498ec09 --- /dev/null +++ b/identity/key.go @@ -0,0 +1,7 @@ +package identity + +type Key struct { + // The GPG fingerprint of the key + Fingerprint string `json:"fingerprint"` + PubKey string `json:"pub_key"` +} diff --git a/identity/version.go b/identity/version.go new file mode 100644 index 00000000..f76ec4c5 --- /dev/null +++ b/identity/version.go @@ -0,0 +1,105 @@ +package identity + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "strings" + + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/git" + + "github.com/MichaelMure/git-bug/util/lamport" + "github.com/MichaelMure/git-bug/util/text" +) + +type Version struct { + // Private field so not serialized + commitHash git.Hash + + // The lamport time at which this version become effective + // The reference time is the bug edition lamport clock + Time lamport.Time `json:"time"` + + Name string `json:"name"` + Email string `json:"email"` + Login string `json:"login"` + AvatarUrl string `json:"avatar_url"` + + // The set of keys valid at that time, from this version onward, until they get removed + // in a new version. This allow to have multiple key for the same identity (e.g. one per + // device) as well as revoke key. + Keys []Key `json:"pub_keys"` + + // This optional array is here to ensure a better randomness of the identity id to avoid collisions. + // It has no functional purpose and should be ignored. + // It is advised to fill this array if there is not enough entropy, e.g. if there is no keys. + Nonce []byte `json:"nonce,omitempty"` +} + +func (v *Version) Validate() error { + if text.Empty(v.Name) && text.Empty(v.Login) { + return fmt.Errorf("either name or login should be set") + } + + if strings.Contains(v.Name, "\n") { + return fmt.Errorf("name should be a single line") + } + + if !text.Safe(v.Name) { + return fmt.Errorf("name is not fully printable") + } + + if strings.Contains(v.Login, "\n") { + return fmt.Errorf("login should be a single line") + } + + if !text.Safe(v.Login) { + return fmt.Errorf("login is not fully printable") + } + + if strings.Contains(v.Email, "\n") { + return fmt.Errorf("email should be a single line") + } + + if !text.Safe(v.Email) { + return fmt.Errorf("email is not fully printable") + } + + if v.AvatarUrl != "" && !text.ValidUrl(v.AvatarUrl) { + return fmt.Errorf("avatarUrl is not a valid URL") + } + + if len(v.Nonce) > 64 { + return fmt.Errorf("nonce is too big") + } + + return nil +} + +// Write will serialize and store the Version as a git blob and return +// its hash +func (v *Version) Write(repo repository.Repo) (git.Hash, error) { + data, err := json.Marshal(v) + + if err != nil { + return "", err + } + + hash, err := repo.StoreData(data) + + if err != nil { + return "", err + } + + return hash, nil +} + +func makeNonce(len int) []byte { + result := make([]byte, len) + _, err := rand.Read(result) + if err != nil { + panic(err) + } + return result +} diff --git a/misc/random_bugs/create_random_bugs.go b/misc/random_bugs/create_random_bugs.go index 8faee200..f30a9d8a 100644 --- a/misc/random_bugs/create_random_bugs.go +++ b/misc/random_bugs/create_random_bugs.go @@ -6,11 +6,12 @@ import ( "time" "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/icrowley/fake" ) -type opsGenerator func(bug.Interface, bug.Person) +type opsGenerator func(bug.Interface, identity.Interface) type Options struct { BugNumber int @@ -136,18 +137,15 @@ func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int return result } -func person() bug.Person { - return bug.Person{ - Name: fake.FullName(), - Email: fake.EmailAddress(), - } +func person() identity.Interface { + return identity.NewBare(fake.FullName(), fake.EmailAddress()) } -var persons []bug.Person +var persons []identity.Interface -func randomPerson(personNumber int) bug.Person { +func randomPerson(personNumber int) identity.Interface { if len(persons) == 0 { - persons = make([]bug.Person, personNumber) + persons = make([]identity.Interface, personNumber) for i := range persons { persons[i] = person() } @@ -162,25 +160,25 @@ func paragraphs() string { return strings.Replace(p, "\t", "\n\n", -1) } -func comment(b bug.Interface, p bug.Person) { +func comment(b bug.Interface, p identity.Interface) { _, _ = bug.AddComment(b, p, time.Now().Unix(), paragraphs()) } -func title(b bug.Interface, p bug.Person) { +func title(b bug.Interface, p identity.Interface) { _, _ = bug.SetTitle(b, p, time.Now().Unix(), fake.Sentence()) } -func open(b bug.Interface, p bug.Person) { +func open(b bug.Interface, p identity.Interface) { _, _ = bug.Open(b, p, time.Now().Unix()) } -func close(b bug.Interface, p bug.Person) { +func close(b bug.Interface, p identity.Interface) { _, _ = bug.Close(b, p, time.Now().Unix()) } var addedLabels []string -func labels(b bug.Interface, p bug.Person) { +func labels(b bug.Interface, p identity.Interface) { var removed []string nbRemoved := rand.Intn(3) for nbRemoved > 0 && len(addedLabels) > 0 { diff --git a/termui/bug_table.go b/termui/bug_table.go index 13d86aa7..ba946c86 100644 --- a/termui/bug_table.go +++ b/termui/bug_table.go @@ -6,6 +6,7 @@ import ( "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/text" "github.com/MichaelMure/gocui" @@ -289,7 +290,7 @@ func (bt *bugTable) render(v *gocui.View, maxX int) { columnWidths := bt.getColumnWidths(maxX) for _, b := range bt.bugs { - person := bug.Person{} + person := &identity.Identity{} snap := b.Snapshot() if len(snap.Comments) > 0 { create := snap.Comments[0] diff --git a/tests/graphql_test.go b/tests/graphql_test.go deleted file mode 100644 index 77008628..00000000 --- a/tests/graphql_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package tests - -import ( - "net/http/httptest" - "testing" - - "github.com/MichaelMure/git-bug/graphql" - "github.com/MichaelMure/git-bug/graphql/models" - "github.com/vektah/gqlgen/client" -) - -func TestQueries(t *testing.T) { - repo := createFilledRepo(10) - - handler, err := graphql.NewHandler(repo) - if err != nil { - t.Fatal(err) - } - - srv := httptest.NewServer(handler) - c := client.New(srv.URL) - - query := ` - query { - defaultRepository { - allBugs(first: 2) { - pageInfo { - endCursor - hasNextPage - startCursor - hasPreviousPage - } - nodes{ - author { - name - email - avatarUrl - } - - createdAt - humanId - id - lastEdit - status - title - - comments(first: 2) { - pageInfo { - endCursor - hasNextPage - startCursor - hasPreviousPage - } - nodes { - files - message - } - } - - operations(first: 20) { - pageInfo { - endCursor - hasNextPage - startCursor - hasPreviousPage - } - nodes { - author { - name - email - avatarUrl - } - date - ... on CreateOperation { - title - message - files - } - ... on SetTitleOperation { - title - was - } - ... on AddCommentOperation { - files - message - } - ... on SetStatusOperation { - status - } - ... on LabelChangeOperation { - added - removed - } - } - } - } - } - } - }` - - type Person struct { - Name string `json:"name"` - Email string `json:"email"` - AvatarUrl string `json:"avatarUrl"` - } - - var resp struct { - DefaultRepository struct { - AllBugs struct { - PageInfo models.PageInfo - Nodes []struct { - Author Person - CreatedAt string `json:"createdAt"` - HumanId string `json:"humanId"` - Id string - LastEdit string `json:"lastEdit"` - Status string - Title string - - Comments struct { - PageInfo models.PageInfo - Nodes []struct { - Files []string - Message string - } - } - - Operations struct { - PageInfo models.PageInfo - Nodes []struct { - Author Person - Date string - Title string - Files []string - Message string - Was string - Status string - Added []string - Removed []string - } - } - } - } - } - } - - c.MustPost(query, &resp) -} diff --git a/tests/read_bugs_test.go b/tests/read_bugs_test.go index e5de0cc2..80d6cc1f 100644 --- a/tests/read_bugs_test.go +++ b/tests/read_bugs_test.go @@ -1,60 +1,14 @@ package tests import ( - "io/ioutil" - "log" "testing" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/misc/random_bugs" - "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/test" ) -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 createFilledRepo(bugNumber int) repository.ClockedRepo { - repo := createRepo(false) - - var seed int64 = 42 - options := random_bugs.DefaultOptions() - - options.BugNumber = bugNumber - - random_bugs.CommitRandomBugsWithSeed(repo, options, seed) - return repo -} - func TestReadBugs(t *testing.T) { - repo := createFilledRepo(15) + repo := test.CreateFilledRepo(15) bugs := bug.ReadAllLocalBugs(repo) for b := range bugs { if b.Err != nil { @@ -64,7 +18,7 @@ func TestReadBugs(t *testing.T) { } func benchmarkReadBugs(bugNumber int, t *testing.B) { - repo := createFilledRepo(bugNumber) + repo := test.CreateFilledRepo(bugNumber) t.ResetTimer() for n := 0; n < t.N; n++ { diff --git a/util/test/repo.go b/util/test/repo.go new file mode 100644 index 00000000..8f0d2e5d --- /dev/null +++ b/util/test/repo.go @@ -0,0 +1,52 @@ +package test + +import ( + "io/ioutil" + "log" + + "github.com/MichaelMure/git-bug/misc/random_bugs" + "github.com/MichaelMure/git-bug/repository" +) + +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 CreateFilledRepo(bugNumber int) repository.ClockedRepo { + repo := CreateRepo(false) + + var seed int64 = 42 + options := random_bugs.DefaultOptions() + + options.BugNumber = bugNumber + + random_bugs.CommitRandomBugsWithSeed(repo, options, seed) + return repo +} -- cgit From 06d9c6872655b85f1a47599add92d49d570e7b2e Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Wed, 16 Jan 2019 21:23:49 +0100 Subject: identity: implement the loading from git --- bug/bug.go | 14 +++---- identity/bare.go | 9 +++++ identity/identity.go | 109 +++++++++++++++++++++++++++++++++++++++++++++----- identity/interface.go | 6 ++- identity/version.go | 2 +- 5 files changed, 120 insertions(+), 20 deletions(-) diff --git a/bug/bug.go b/bug/bug.go index b6d09c50..be3e2661 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -113,13 +113,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 +120,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, } diff --git a/identity/bare.go b/identity/bare.go index eec00e19..24f30f9f 100644 --- a/identity/bare.go +++ b/identity/bare.go @@ -9,6 +9,11 @@ import ( "github.com/MichaelMure/git-bug/util/text" ) +// Bare is a very minimal identity, designed to be fully embedded directly along +// other data. +// +// in particular, this identity is designed to be compatible with the handling of +// identities in the early version of git-bug. type Bare struct { name string email string @@ -71,10 +76,12 @@ func (i Bare) AvatarUrl() string { return i.avatarUrl } +// Keys return the last version of the valid keys func (i Bare) Keys() []Key { return []Key{} } +// ValidKeysAtTime return the set of keys valid at a given lamport time func (i Bare) ValidKeysAtTime(time lamport.Time) []Key { return []Key{} } @@ -139,6 +146,8 @@ func (i Bare) Validate() error { return nil } +// IsProtected return true if the chain of git commits started to be signed. +// If that's the case, only signed commit with a valid key for this identity can be added. func (i Bare) IsProtected() bool { return false } diff --git a/identity/identity.go b/identity/identity.go index f65e2a86..0fe13d21 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -2,18 +2,22 @@ package identity import ( + "encoding/json" "fmt" "strings" "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 identityRefPattern = "refs/identities/" const versionEntryName = "version" const identityConfigKey = "git-bug.identity" +var ErrIdentityNotExist = errors.New("identity doesn't exist") + type Identity struct { id string Versions []Version @@ -35,22 +39,106 @@ type identityJson struct { Id string `json:"id"` } -// TODO: marshal/unmarshal identity + load/write from OpBase +// MarshalJSON will only serialize the id +func (i *Identity) MarshalJSON() ([]byte, error) { + return json.Marshal(identityJson{ + Id: i.Id(), + }) +} + +// UnmarshalJSON will only read the id +// Users of this package are expected to run Load() to load +// the remaining data from the identities data in git. +func (i *Identity) UnmarshalJSON(data []byte) error { + aux := identityJson{} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + i.id = aux.Id + return nil +} + +// TODO: load/write from OpBase + +// Read load an Identity from the identities data available in git func Read(repo repository.Repo, id string) (*Identity, error) { - // Todo - return &Identity{}, nil + i := &Identity{ + id: id, + } + + err := i.Load(repo) + if err != nil { + return nil, err + } + + return i, nil +} + +// Load will read the corresponding identity data from git and replace any +// data already loaded if any. +func (i *Identity) Load(repo repository.Repo) error { + ref := fmt.Sprintf("%s%s", identityRefPattern, i.Id()) + + hashes, err := repo.ListCommits(ref) + + var versions []Version + + // TODO: this is not perfect, it might be a command invoke error + if err != nil { + return ErrIdentityNotExist + } + + for _, hash := range hashes { + entries, err := repo.ListEntries(hash) + if err != nil { + return errors.Wrap(err, "can't list git tree entries") + } + + if len(entries) != 1 { + return fmt.Errorf("invalid identity data at hash %s", hash) + } + + entry := entries[0] + + if entry.Name != versionEntryName { + return fmt.Errorf("invalid identity data at hash %s", hash) + } + + data, err := repo.ReadData(entry.Hash) + if err != nil { + return errors.Wrap(err, "failed to read git blob data") + } + + var version Version + err = json.Unmarshal(data, &version) + + if err != nil { + return errors.Wrapf(err, "failed to decode Identity version json %s", hash) + } + + // tag the version with the commit hash + version.commitHash = hash + + versions = append(versions, version) + } + + i.Versions = versions + + return nil } // NewFromGitUser will query the repository for user detail and // build the corresponding Identity -/*func NewFromGitUser(repo repository.Repo) (*Identity, error) { +func NewFromGitUser(repo repository.Repo) (*Identity, error) { name, err := repo.GetUserName() if err != nil { return nil, err } if name == "" { - return nil, errors.New("User name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`") + return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`") } email, err := repo.GetUserEmail() @@ -58,14 +146,15 @@ func Read(repo repository.Repo, id string) (*Identity, error) { return nil, err } if email == "" { - return nil, errors.New("User name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`") + return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`") } return NewIdentity(name, email) -}*/ +} -// -func BuildFromGit(repo repository.Repo) *Identity { +// BuildFromGit will query the repository for user detail and +// build the corresponding Identity +/*func BuildFromGit(repo repository.Repo) *Identity { version := Version{} name, err := repo.GetUserName() @@ -83,7 +172,7 @@ func BuildFromGit(repo repository.Repo) *Identity { version, }, } -} +}*/ // SetIdentity store the user identity's id in the git config func SetIdentity(repo repository.RepoCommon, identity Identity) error { diff --git a/identity/interface.go b/identity/interface.go index 14287655..058e6ec6 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -1,6 +1,8 @@ package identity -import "github.com/MichaelMure/git-bug/util/lamport" +import ( + "github.com/MichaelMure/git-bug/util/lamport" +) type Interface interface { Name() string @@ -8,7 +10,7 @@ type Interface interface { Login() string AvatarUrl() string - // Login return the last version of the valid keys + // Keys return the last version of the valid keys Keys() []Key // ValidKeysAtTime return the set of keys valid at a given lamport time diff --git a/identity/version.go b/identity/version.go index f76ec4c5..3e84ece3 100644 --- a/identity/version.go +++ b/identity/version.go @@ -8,11 +8,11 @@ import ( "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" - "github.com/MichaelMure/git-bug/util/lamport" "github.com/MichaelMure/git-bug/util/text" ) +// Version is a complete set of informations about an Identity at a point in time. type Version struct { // Private field so not serialized commitHash git.Hash -- cgit From 3df4f46c71650c9d23b267c44afec16f1b759e92 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Thu, 17 Jan 2019 02:05:50 +0100 Subject: identity: add metadata support --- identity/identity.go | 86 +++++++++++++++++++++++++++++++++++++++-------- identity/identity_test.go | 86 +++++++++++++++++++++++++++++++++++++++++++---- identity/interface.go | 2 ++ identity/version.go | 26 +++++++++++++- 4 files changed, 179 insertions(+), 21 deletions(-) diff --git a/identity/identity.go b/identity/identity.go index 0fe13d21..3d523d38 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -20,19 +20,33 @@ var ErrIdentityNotExist = errors.New("identity doesn't exist") type Identity struct { id string - Versions []Version + Versions []*Version } -func NewIdentity(name string, email string) (*Identity, error) { +func NewIdentity(name string, email string) *Identity { return &Identity{ - Versions: []Version{ + Versions: []*Version{ { Name: name, Email: email, Nonce: makeNonce(20), }, }, - }, nil + } +} + +func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity { + return &Identity{ + Versions: []*Version{ + { + Name: name, + Email: email, + Login: login, + AvatarUrl: avatarUrl, + Nonce: makeNonce(20), + }, + }, + } } type identityJson struct { @@ -84,7 +98,7 @@ func (i *Identity) Load(repo repository.Repo) error { hashes, err := repo.ListCommits(ref) - var versions []Version + var versions []*Version // TODO: this is not perfect, it might be a command invoke error if err != nil { @@ -122,7 +136,7 @@ func (i *Identity) Load(repo repository.Repo) error { // tag the version with the commit hash version.commitHash = hash - versions = append(versions, version) + versions = append(versions, &version) } i.Versions = versions @@ -149,7 +163,7 @@ func NewFromGitUser(repo repository.Repo) (*Identity, error) { return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`") } - return NewIdentity(name, email) + return NewIdentity(name, email), nil } // BuildFromGit will query the repository for user detail and @@ -202,7 +216,7 @@ func GetIdentity(repo repository.Repo) (*Identity, error) { return Read(repo, id) } -func (i *Identity) AddVersion(version Version) { +func (i *Identity) AddVersion(version *Version) { i.Versions = append(i.Versions, version) } @@ -285,7 +299,15 @@ func (i *Identity) Validate() error { return nil } -func (i *Identity) LastVersion() Version { +func (i *Identity) firstVersion() *Version { + if len(i.Versions) <= 0 { + panic("no version at all") + } + + return i.Versions[0] +} + +func (i *Identity) lastVersion() *Version { if len(i.Versions) <= 0 { panic("no version at all") } @@ -305,27 +327,27 @@ func (i *Identity) Id() string { // Name return the last version of the name func (i *Identity) Name() string { - return i.LastVersion().Name + return i.lastVersion().Name } // Email return the last version of the email func (i *Identity) Email() string { - return i.LastVersion().Email + return i.lastVersion().Email } // Login return the last version of the login func (i *Identity) Login() string { - return i.LastVersion().Login + return i.lastVersion().Login } // Login return the last version of the Avatar URL func (i *Identity) AvatarUrl() string { - return i.LastVersion().AvatarUrl + return i.lastVersion().AvatarUrl } // Login return the last version of the valid keys func (i *Identity) Keys() []Key { - return i.LastVersion().Keys + return i.lastVersion().Keys } // IsProtected return true if the chain of git commits started to be signed. @@ -372,3 +394,39 @@ func (i *Identity) DisplayName() string { panic("invalid person data") } + +// SetMetadata store arbitrary metadata along the last defined Version. +// If the Version has been commit to git already, it won't be overwritten. +func (i *Identity) SetMetadata(key string, value string) { + i.lastVersion().SetMetadata(key, value) +} + +// ImmutableMetadata return all metadata for this Identity, accumulated from each Version. +// If multiple value are found, the first defined takes precedence. +func (i *Identity) ImmutableMetadata() map[string]string { + metadata := make(map[string]string) + + for _, version := range i.Versions { + for key, value := range version.Metadata { + if _, has := metadata[key]; !has { + metadata[key] = value + } + } + } + + return metadata +} + +// MutableMetadata return all metadata for this Identity, accumulated from each Version. +// If multiple value are found, the last defined takes precedence. +func (i *Identity) MutableMetadata() map[string]string { + metadata := make(map[string]string) + + for _, version := range i.Versions { + for key, value := range version.Metadata { + metadata[key] = value + } + } + + return metadata +} diff --git a/identity/identity_test.go b/identity/identity_test.go index 161fd56f..914126be 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -5,15 +5,16 @@ import ( "github.com/MichaelMure/git-bug/repository" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestIdentityCommit(t *testing.T) { +func TestIdentityCommitLoad(t *testing.T) { mockRepo := repository.NewMockRepoForTest() // single version identity := Identity{ - Versions: []Version{ + Versions: []*Version{ { Name: "René Descartes", Email: "rene.descartes@example.com", @@ -26,10 +27,15 @@ func TestIdentityCommit(t *testing.T) { assert.Nil(t, err) assert.NotEmpty(t, identity.id) + loaded, err := Read(mockRepo, identity.id) + assert.Nil(t, err) + commitsAreSet(t, loaded) + equivalentIdentity(t, &identity, loaded) + // multiple version identity = Identity{ - Versions: []Version{ + Versions: []*Version{ { Time: 100, Name: "René Descartes", @@ -62,9 +68,14 @@ func TestIdentityCommit(t *testing.T) { assert.Nil(t, err) assert.NotEmpty(t, identity.id) + loaded, err = Read(mockRepo, identity.id) + assert.Nil(t, err) + commitsAreSet(t, loaded) + equivalentIdentity(t, &identity, loaded) + // add more version - identity.AddVersion(Version{ + identity.AddVersion(&Version{ Time: 201, Name: "René Descartes", Email: "rene.descartes@example.com", @@ -73,7 +84,7 @@ func TestIdentityCommit(t *testing.T) { }, }) - identity.AddVersion(Version{ + identity.AddVersion(&Version{ Time: 300, Name: "René Descartes", Email: "rene.descartes@example.com", @@ -86,11 +97,32 @@ func TestIdentityCommit(t *testing.T) { assert.Nil(t, err) assert.NotEmpty(t, identity.id) + + loaded, err = Read(mockRepo, identity.id) + assert.Nil(t, err) + commitsAreSet(t, loaded) + equivalentIdentity(t, &identity, loaded) +} + +func commitsAreSet(t *testing.T, identity *Identity) { + for _, version := range identity.Versions { + assert.NotEmpty(t, version.commitHash) + } +} + +func equivalentIdentity(t *testing.T, expected, actual *Identity) { + require.Equal(t, len(expected.Versions), len(actual.Versions)) + + for i, version := range expected.Versions { + actual.Versions[i].commitHash = version.commitHash + } + + assert.Equal(t, expected, actual) } func TestIdentity_ValidKeysAtTime(t *testing.T) { identity := Identity{ - Versions: []Version{ + Versions: []*Version{ { Time: 100, Name: "René Descartes", @@ -143,3 +175,45 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) { assert.Equal(t, identity.ValidKeysAtTime(300), []Key{{PubKey: "pubkeyE"}}) assert.Equal(t, identity.ValidKeysAtTime(3000), []Key{{PubKey: "pubkeyE"}}) } + +func TestMetadata(t *testing.T) { + mockRepo := repository.NewMockRepoForTest() + + identity := NewIdentity("René Descartes", "rene.descartes@example.com") + + identity.SetMetadata("key1", "value1") + assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1") + assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1") + + err := identity.Commit(mockRepo) + assert.NoError(t, err) + + assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1") + assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1") + + // try override + identity.AddVersion(&Version{ + Name: "René Descartes", + Email: "rene.descartes@example.com", + }) + + identity.SetMetadata("key1", "value2") + assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1") + assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value2") + + err = identity.Commit(mockRepo) + assert.NoError(t, err) + + // reload + loaded, err := Read(mockRepo, identity.id) + assert.Nil(t, err) + + assertHasKeyValue(t, loaded.ImmutableMetadata(), "key1", "value1") + assertHasKeyValue(t, loaded.MutableMetadata(), "key1", "value2") +} + +func assertHasKeyValue(t *testing.T, metadata map[string]string, key, value string) { + val, ok := metadata[key] + assert.True(t, ok) + assert.Equal(t, val, value) +} diff --git a/identity/interface.go b/identity/interface.go index 058e6ec6..6489efbe 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -5,6 +5,8 @@ import ( ) type Interface interface { + Id() string + Name() string Email() string Login() string diff --git a/identity/version.go b/identity/version.go index 3e84ece3..bc4561d9 100644 --- a/identity/version.go +++ b/identity/version.go @@ -12,7 +12,7 @@ import ( "github.com/MichaelMure/git-bug/util/text" ) -// Version is a complete set of informations about an Identity at a point in time. +// Version is a complete set of information about an Identity at a point in time. type Version struct { // Private field so not serialized commitHash git.Hash @@ -35,6 +35,9 @@ type Version struct { // It has no functional purpose and should be ignored. // It is advised to fill this array if there is not enough entropy, e.g. if there is no keys. Nonce []byte `json:"nonce,omitempty"` + + // A set of arbitrary key/value to store metadata about a version or about an Identity in general. + Metadata map[string]string `json:"metadata,omitempty"` } func (v *Version) Validate() error { @@ -103,3 +106,24 @@ func makeNonce(len int) []byte { } return result } + +// SetMetadata store arbitrary metadata about a version or an Identity in general +// If the Version has been commit to git already, it won't be overwritten. +func (v *Version) SetMetadata(key string, value string) { + if v.Metadata == nil { + v.Metadata = make(map[string]string) + } + + v.Metadata[key] = value +} + +// GetMetadata retrieve arbitrary metadata about the Version +func (v *Version) GetMetadata(key string) (string, bool) { + val, ok := v.Metadata[key] + return val, ok +} + +// AllMetadata return all metadata for this Identity +func (v *Version) AllMetadata() map[string]string { + return v.Metadata +} -- cgit From bdbe9e7e8256fff820efe1ce707e7154d517ecb3 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Thu, 17 Jan 2019 03:09:08 +0100 Subject: identity: more progress and fixes --- bridge/github/import.go | 23 ++-- bridge/launchpad/launchpad.go | 2 +- cache/bug_excerpt.go | 2 +- cache/repo_cache.go | 70 ++++++++++++ commands/id.go | 18 +++- graphql/gqlgen.yml | 2 +- graphql/graph/gen_graph.go | 244 +++++++++++++++++------------------------- repository/repo.go | 2 +- termui/bug_table.go | 10 +- 9 files changed, 206 insertions(+), 167 deletions(-) diff --git a/bridge/github/import.go b/bridge/github/import.go index 93390408..de125793 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -8,18 +8,20 @@ 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 + ghost identity.Interface } func (gi *githubImporter) Init(conf core.Configuration) error { @@ -69,7 +71,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(b, itemEdge.Cursor, itemEdge.Node, variables) + if err != nil { + return err + } } if !issue.Timeline.PageInfo.HasNextPage { @@ -561,7 +566,7 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash, } // makePerson create a bug.Person from the Github data -func (gi *githubImporter) makePerson(actor *actor) bug.Person { +func (gi *githubImporter) makePerson(actor *actor) identity.Interface { if actor == nil { return gi.ghost } @@ -609,12 +614,12 @@ 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), - } + gi.ghost = identity.NewIdentityFull( + name, + string(q.User.Login), + string(q.User.AvatarUrl), + string(q.User.Email), + ) return nil } diff --git a/bridge/launchpad/launchpad.go b/bridge/launchpad/launchpad.go index f862f24e..1fd9edc2 100644 --- a/bridge/launchpad/launchpad.go +++ b/bridge/launchpad/launchpad.go @@ -1,4 +1,4 @@ -// Package launchad contains the Launchpad bridge implementation +// Package launchpad contains the Launchpad bridge implementation package launchpad import ( diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index 77c18175..f5844b64 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -20,7 +20,7 @@ type BugExcerpt struct { EditUnixTime int64 Status bug.Status - Author *identity.Identity + Author identity.Interface Labels []bug.Label CreateMetadata map[string]string diff --git a/cache/repo_cache.go b/cache/repo_cache.go index a149fd73..7d3e7d1d 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -30,6 +30,8 @@ type RepoCache struct { excerpts map[string]*BugExcerpt // bug loaded in memory bugs map[string]*BugCache + // identities loaded in memory + identities map[string]*identity.Identity } func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) { @@ -279,6 +281,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() @@ -525,3 +528,70 @@ func repoIsAvailable(repo repository.Repo) error { return nil } + +// ResolveIdentity retrieve an identity matching the exact given id +func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) { + cached, ok := c.identities[id] + if ok { + return cached, nil + } + + i, err := identity.Read(c.repo, id) + if err != nil { + return nil, err + } + + c.identities[id] = i + + return i, nil +} + +// ResolveIdentityPrefix retrieve an Identity matching an id prefix. It fails if multiple +// bugs match. +// func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*BugCache, error) { +// // preallocate but empty +// matching := make([]string, 0, 5) +// +// for id := range c.excerpts { +// if strings.HasPrefix(id, prefix) { +// matching = append(matching, id) +// } +// } +// +// if len(matching) > 1 { +// return nil, bug.ErrMultipleMatch{Matching: matching} +// } +// +// if len(matching) == 0 { +// return nil, bug.ErrBugNotExist +// } +// +// return c.ResolveBug(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) (*BugCache, error) { + // // preallocate but empty + // matching := make([]string, 0, 5) + // + // for id, excerpt := range c.excerpts { + // if excerpt.CreateMetadata[key] == value { + // matching = append(matching, id) + // } + // } + // + // if len(matching) > 1 { + // return nil, bug.ErrMultipleMatch{Matching: matching} + // } + // + // if len(matching) == 0 { + // return nil, bug.ErrBugNotExist + // } + // + // return c.ResolveBug(matching[0]) + + // TODO + + return nil, nil +} diff --git a/commands/id.go b/commands/id.go index 485c5457..44294132 100644 --- a/commands/id.go +++ b/commands/id.go @@ -1,6 +1,7 @@ package commands import ( + "errors" "fmt" "github.com/MichaelMure/git-bug/identity" @@ -8,7 +9,19 @@ import ( ) func runId(cmd *cobra.Command, args []string) error { - id, err := identity.GetIdentity(repo) + if len(args) > 1 { + return errors.New("only one identity can be displayed at a time") + } + + var id *identity.Identity + var err error + + if len(args) == 1 { + id, err = identity.Read(repo, args[0]) + } else { + id, err = identity.GetIdentity(repo) + } + if err != nil { return err } @@ -24,7 +37,7 @@ func runId(cmd *cobra.Command, args []string) error { } var idCmd = &cobra.Command{ - Use: "id", + Use: "id []", Short: "Display or change the user identity", PreRunE: loadRepo, RunE: runId, @@ -32,4 +45,5 @@ var idCmd = &cobra.Command{ func init() { RootCmd.AddCommand(idCmd) + selectCmd.Flags().SortFlags = false } diff --git a/graphql/gqlgen.yml b/graphql/gqlgen.yml index 019f3444..0e389a53 100644 --- a/graphql/gqlgen.yml +++ b/graphql/gqlgen.yml @@ -14,7 +14,7 @@ models: Comment: model: github.com/MichaelMure/git-bug/bug.Comment Identity: - model: github.com/MichaelMure/git-bug/identity.Identity + model: github.com/MichaelMure/git-bug/identity.Interface Label: model: github.com/MichaelMure/git-bug/bug.Label Hash: diff --git a/graphql/graph/gen_graph.go b/graphql/graph/gen_graph.go index cc714ecc..fa8e4acb 100644 --- a/graphql/graph/gen_graph.go +++ b/graphql/graph/gen_graph.go @@ -44,6 +44,7 @@ type ResolverRoot interface { CreateOperation() CreateOperationResolver CreateTimelineItem() CreateTimelineItemResolver EditCommentOperation() EditCommentOperationResolver + Identity() IdentityResolver LabelChangeOperation() LabelChangeOperationResolver LabelChangeTimelineItem() LabelChangeTimelineItemResolver Mutation() MutationResolver @@ -292,6 +293,13 @@ type CreateTimelineItemResolver interface { type EditCommentOperationResolver interface { Date(ctx context.Context, obj *bug.EditCommentOperation) (time.Time, error) } +type IdentityResolver interface { + Name(ctx context.Context, obj *identity.Interface) (*string, error) + Email(ctx context.Context, obj *identity.Interface) (*string, error) + Login(ctx context.Context, obj *identity.Interface) (*string, error) + DisplayName(ctx context.Context, obj *identity.Interface) (string, error) + AvatarURL(ctx context.Context, obj *identity.Interface) (*string, error) +} type LabelChangeOperationResolver interface { Date(ctx context.Context, obj *bug.LabelChangeOperation) (time.Time, error) } @@ -2065,18 +2073,11 @@ func (ec *executionContext) _AddCommentOperation_author(ctx context.Context, fie } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -2296,18 +2297,11 @@ func (ec *executionContext) _AddCommentTimelineItem_author(ctx context.Context, } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -2807,18 +2801,11 @@ func (ec *executionContext) _Bug_author(ctx context.Context, field graphql.Colle } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -3348,18 +3335,11 @@ func (ec *executionContext) _Comment_author(ctx context.Context, field graphql.C } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -3937,18 +3917,11 @@ func (ec *executionContext) _CreateOperation_author(ctx context.Context, field g } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -4195,18 +4168,11 @@ func (ec *executionContext) _CreateTimelineItem_author(ctx context.Context, fiel } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -4548,18 +4514,11 @@ func (ec *executionContext) _EditCommentOperation_author(ctx context.Context, fi } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -4682,9 +4641,10 @@ func (ec *executionContext) _EditCommentOperation_files(ctx context.Context, fie var identityImplementors = []string{"Identity"} // nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj *identity.Identity) graphql.Marshaler { +func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj *identity.Interface) graphql.Marshaler { fields := graphql.CollectFields(ctx, sel, identityImplementors) + var wg sync.WaitGroup out := graphql.NewOrderedMap(len(fields)) invalid := false for i, field := range fields { @@ -4694,23 +4654,43 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, case "__typename": out.Values[i] = graphql.MarshalString("Identity") case "name": - out.Values[i] = ec._Identity_name(ctx, field, obj) + wg.Add(1) + go func(i int, field graphql.CollectedField) { + out.Values[i] = ec._Identity_name(ctx, field, obj) + wg.Done() + }(i, field) case "email": - out.Values[i] = ec._Identity_email(ctx, field, obj) + wg.Add(1) + go func(i int, field graphql.CollectedField) { + out.Values[i] = ec._Identity_email(ctx, field, obj) + wg.Done() + }(i, field) case "login": - out.Values[i] = ec._Identity_login(ctx, field, obj) + wg.Add(1) + go func(i int, field graphql.CollectedField) { + out.Values[i] = ec._Identity_login(ctx, field, obj) + wg.Done() + }(i, field) case "displayName": - out.Values[i] = ec._Identity_displayName(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalid = true - } + wg.Add(1) + go func(i int, field graphql.CollectedField) { + out.Values[i] = ec._Identity_displayName(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + wg.Done() + }(i, field) case "avatarUrl": - out.Values[i] = ec._Identity_avatarUrl(ctx, field, obj) + wg.Add(1) + go func(i int, field graphql.CollectedField) { + out.Values[i] = ec._Identity_avatarUrl(ctx, field, obj) + wg.Done() + }(i, field) default: panic("unknown field " + strconv.Quote(field.Name)) } } - + wg.Wait() if invalid { return graphql.Null } @@ -4718,7 +4698,7 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, } // nolint: vetshadow -func (ec *executionContext) _Identity_name(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler { +func (ec *executionContext) _Identity_name(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ @@ -4730,19 +4710,23 @@ func (ec *executionContext) _Identity_name(ctx context.Context, field graphql.Co ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Name(), nil + return ec.resolvers.Identity().Name(rctx, obj) }) if resTmp == nil { return graphql.Null } - res := resTmp.(string) + res := resTmp.(*string) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return graphql.MarshalString(res) + + if res == nil { + return graphql.Null + } + return graphql.MarshalString(*res) } // nolint: vetshadow -func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler { +func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ @@ -4754,19 +4738,23 @@ func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.C ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Email(), nil + return ec.resolvers.Identity().Email(rctx, obj) }) if resTmp == nil { return graphql.Null } - res := resTmp.(string) + res := resTmp.(*string) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return graphql.MarshalString(res) + + if res == nil { + return graphql.Null + } + return graphql.MarshalString(*res) } // nolint: vetshadow -func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler { +func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ @@ -4778,19 +4766,23 @@ func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.C ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Login(), nil + return ec.resolvers.Identity().Login(rctx, obj) }) if resTmp == nil { return graphql.Null } - res := resTmp.(string) + res := resTmp.(*string) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return graphql.MarshalString(res) + + if res == nil { + return graphql.Null + } + return graphql.MarshalString(*res) } // nolint: vetshadow -func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler { +func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ @@ -4802,7 +4794,7 @@ func (ec *executionContext) _Identity_displayName(ctx context.Context, field gra ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.DisplayName(), nil + return ec.resolvers.Identity().DisplayName(rctx, obj) }) if resTmp == nil { if !ec.HasError(rctx) { @@ -4817,7 +4809,7 @@ func (ec *executionContext) _Identity_displayName(ctx context.Context, field gra } // nolint: vetshadow -func (ec *executionContext) _Identity_avatarUrl(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler { +func (ec *executionContext) _Identity_avatarUrl(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ @@ -4829,15 +4821,19 @@ func (ec *executionContext) _Identity_avatarUrl(ctx context.Context, field graph ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.AvatarURL(), nil + return ec.resolvers.Identity().AvatarURL(rctx, obj) }) if resTmp == nil { return graphql.Null } - res := resTmp.(string) + res := resTmp.(*string) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return graphql.MarshalString(res) + + if res == nil { + return graphql.Null + } + return graphql.MarshalString(*res) } var labelChangeOperationImplementors = []string{"LabelChangeOperation", "Operation", "Authored"} @@ -4943,18 +4939,11 @@ func (ec *executionContext) _LabelChangeOperation_author(ctx context.Context, fi } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -5159,18 +5148,11 @@ func (ec *executionContext) _LabelChangeTimelineItem_author(ctx context.Context, } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -6423,18 +6405,11 @@ func (ec *executionContext) _SetStatusOperation_author(ctx context.Context, fiel } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -6593,18 +6568,11 @@ func (ec *executionContext) _SetStatusTimelineItem_author(ctx context.Context, f } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -6764,18 +6732,11 @@ func (ec *executionContext) _SetTitleOperation_author(ctx context.Context, field } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow @@ -6962,18 +6923,11 @@ func (ec *executionContext) _SetTitleTimelineItem_author(ctx context.Context, fi } return graphql.Null } - res := resTmp.(*identity.Identity) + res := resTmp.(identity.Interface) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - if res == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - - return ec._Identity(ctx, field.Selections, res) + return ec._Identity(ctx, field.Selections, &res) } // nolint: vetshadow diff --git a/repository/repo.go b/repository/repo.go index 3ae09057..100feaed 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -122,7 +122,7 @@ func prepareTreeEntries(entries []TreeEntry) bytes.Buffer { } func readTreeEntries(s string) ([]TreeEntry, error) { - split := strings.Split(s, "\n") + split := strings.Split(strings.TrimSpace(s), "\n") casted := make([]TreeEntry, len(split)) for i, line := range split { diff --git a/termui/bug_table.go b/termui/bug_table.go index ba946c86..69634151 100644 --- a/termui/bug_table.go +++ b/termui/bug_table.go @@ -6,7 +6,6 @@ import ( "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/text" "github.com/MichaelMure/gocui" @@ -290,12 +289,9 @@ func (bt *bugTable) render(v *gocui.View, maxX int) { columnWidths := bt.getColumnWidths(maxX) for _, b := range bt.bugs { - person := &identity.Identity{} snap := b.Snapshot() - if len(snap.Comments) > 0 { - create := snap.Comments[0] - person = create.Author - } + create := snap.Comments[0] + authorIdentity := create.Author summaryTxt := fmt.Sprintf("C:%-2d L:%-2d", len(snap.Comments)-1, @@ -305,7 +301,7 @@ func (bt *bugTable) render(v *gocui.View, maxX int) { id := text.LeftPadMaxLine(snap.HumanId(), columnWidths["id"], 1) status := text.LeftPadMaxLine(snap.Status.String(), columnWidths["status"], 1) title := text.LeftPadMaxLine(snap.Title, columnWidths["title"], 1) - author := text.LeftPadMaxLine(person.DisplayName(), columnWidths["author"], 1) + author := text.LeftPadMaxLine(authorIdentity.DisplayName(), columnWidths["author"], 1) summary := text.LeftPadMaxLine(summaryTxt, columnWidths["summary"], 1) lastEdit := text.LeftPadMaxLine(humanize.Time(snap.LastEditTime()), columnWidths["lastEdit"], 1) -- cgit From 844616baf8dc628360942d57fd69f24e298e08da Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 19 Jan 2019 16:01:06 +0100 Subject: identity: more progress and fixes --- bridge/github/import.go | 138 +++++++++++++++++++++++---------- bridge/launchpad/import.go | 40 ++++++++-- bug/op_create_test.go | 2 +- bug/op_edit_comment_test.go | 2 +- bug/op_set_metadata_test.go | 2 +- bug/operation_iterator_test.go | 2 +- bug/operation_test.go | 10 +-- cache/bug_cache.go | 4 + cache/multi_repo_cache.go | 1 + cache/repo_cache.go | 138 ++++++++++++++++++++++----------- commands/ls.go | 4 +- commands/show.go | 2 +- doc/man/git-bug-id.1 | 29 +++++++ doc/md/git-bug_id.md | 22 ++++++ graphql/resolvers/identity.go | 36 +++++++++ graphql/resolvers/root.go | 4 + identity/identity.go | 10 +++ misc/bash_completion/git-bug | 21 +++++ misc/random_bugs/create_random_bugs.go | 2 +- misc/zsh_completion/git-bug | 2 +- 20 files changed, 364 insertions(+), 107 deletions(-) create mode 100644 doc/man/git-bug-id.1 create mode 100644 doc/md/git-bug_id.md create mode 100644 graphql/resolvers/identity.go diff --git a/bridge/github/import.go b/bridge/github/import.go index de125793..43a8e3b5 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -21,14 +21,13 @@ const keyGithubLogin = "github-login" type githubImporter struct { client *githubv4.Client conf core.Configuration - ghost identity.Interface } 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 { @@ -71,7 +70,7 @@ func (gi *githubImporter) ImportAll(repo *cache.RepoCache) error { } for _, itemEdge := range q.Repository.Issues.Nodes[0].Timeline.Edges { - err = gi.ensureTimelineItem(b, itemEdge.Cursor, itemEdge.Node, variables) + err = gi.ensureTimelineItem(repo, b, itemEdge.Cursor, itemEdge.Node, variables) if err != nil { return err } @@ -114,6 +113,11 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline return nil, err } + author, err := gi.makePerson(repo, issue.Author) + if err != nil { + return nil, err + } + // if there is no edit, the UserContentEdits given by github is empty. That // means that the original message is given by the issue message. // @@ -128,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 @@ -140,7 +144,6 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline keyGithubUrl: issue.Url.String(), }, ) - if err != nil { return nil, err } @@ -166,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 @@ -189,7 +192,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline return nil, err } - err = gi.ensureCommentEdit(b, target, edit) + err = gi.ensureCommentEdit(repo, b, target, edit) if err != nil { return nil, err } @@ -199,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 @@ -248,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 @@ -271,7 +274,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline return nil, err } - err = gi.ensureCommentEdit(b, target, edit) + err = gi.ensureCommentEdit(repo, b, target, edit) if err != nil { return nil, err } @@ -289,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 @@ -306,12 +309,12 @@ 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) @@ -319,8 +322,12 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. if err != cache.ErrNoMatchingOp { return err } + author, err := gi.makePerson(repo, item.LabeledEvent.Actor) + if err != nil { + return err + } _, err = b.ChangeLabelsRaw( - gi.makePerson(item.LabeledEvent.Actor), + author, item.LabeledEvent.CreatedAt.Unix(), []string{ string(item.LabeledEvent.Label.Name), @@ -336,8 +343,12 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. if err != cache.ErrNoMatchingOp { return err } + author, err := gi.makePerson(repo, item.UnlabeledEvent.Actor) + if err != nil { + return err + } _, err = b.ChangeLabelsRaw( - gi.makePerson(item.UnlabeledEvent.Actor), + author, item.UnlabeledEvent.CreatedAt.Unix(), nil, []string{ @@ -353,8 +364,12 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. if err != cache.ErrNoMatchingOp { return err } + author, err := gi.makePerson(repo, item.ClosedEvent.Actor) + if err != nil { + return err + } return b.CloseRaw( - gi.makePerson(item.ClosedEvent.Actor), + author, item.ClosedEvent.CreatedAt.Unix(), map[string]string{keyGithubId: id}, ) @@ -365,8 +380,12 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. if err != cache.ErrNoMatchingOp { return err } + author, err := gi.makePerson(repo, item.ReopenedEvent.Actor) + if err != nil { + return err + } return b.OpenRaw( - gi.makePerson(item.ReopenedEvent.Actor), + author, item.ReopenedEvent.CreatedAt.Unix(), map[string]string{keyGithubId: id}, ) @@ -377,8 +396,12 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. if err != cache.ErrNoMatchingOp { return err } + author, err := gi.makePerson(repo, item.RenamedTitleEvent.Actor) + if err != nil { + return err + } return b.SetTitleRaw( - gi.makePerson(item.RenamedTitleEvent.Actor), + author, item.RenamedTitleEvent.CreatedAt.Unix(), string(item.RenamedTitleEvent.CurrentTitle), map[string]string{keyGithubId: id}, @@ -391,13 +414,18 @@ 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 { +func (gi *githubImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache, cursor githubv4.String, comment issueComment, rootVariables map[string]interface{}) error { target, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(comment.Id)) if err != nil && err != cache.ErrNoMatchingOp { // real error return err } + author, err := gi.makePerson(repo, comment.Author) + if err != nil { + return err + } + // if there is no edit, the UserContentEdits given by github is empty. That // means that the original message is given by the comment message. // @@ -412,7 +440,7 @@ 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), + author, comment.CreatedAt.Unix(), cleanupText(string(comment.Body)), nil, @@ -445,7 +473,7 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin } err = b.AddCommentRaw( - gi.makePerson(comment.Author), + author, comment.CreatedAt.Unix(), cleanupText(string(*edit.Diff)), nil, @@ -459,7 +487,7 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin } } - err := gi.ensureCommentEdit(b, target, edit) + err := gi.ensureCommentEdit(repo, b, target, edit) if err != nil { return err } @@ -501,7 +529,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 } @@ -519,7 +547,7 @@ 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. @@ -542,6 +570,11 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash, fmt.Println("import edition") + editor, err := gi.makePerson(repo, edit.Editor) + if err != nil { + return err + } + switch { case edit.DeletedAt != nil: // comment deletion, not supported yet @@ -549,7 +582,7 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash, case edit.DeletedAt == nil: // comment edition err := b.EditCommentRaw( - gi.makePerson(edit.Editor), + editor, edit.CreatedAt.Unix(), target, cleanupText(string(*edit.Diff)), @@ -566,10 +599,22 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash, } // makePerson create a bug.Person from the Github data -func (gi *githubImporter) makePerson(actor *actor) identity.Interface { +func (gi *githubImporter) makePerson(repo *cache.RepoCache, actor *actor) (*identity.Identity, 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 @@ -589,24 +634,36 @@ func (gi *githubImporter) makePerson(actor *actor) identity.Interface { 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) (*identity.Identity, 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 @@ -614,14 +671,15 @@ func (gi *githubImporter) fetchGhost() error { name = string(*q.User.Name) } - gi.ghost = identity.NewIdentityFull( + return repo.NewIdentityRaw( name, + string(q.User.Email), string(q.User.Login), string(q.User.AvatarUrl), - string(q.User.Email), + map[string]string{ + keyGithubLogin: string(q.User.Login), + }, ) - - return nil } // parseId convert the unusable githubv4.ID (an interface{}) into a string diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go index 10d25e6c..e65186ed 100644 --- a/bridge/launchpad/import.go +++ b/bridge/launchpad/import.go @@ -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) makePerson(repo *cache.RepoCache, owner LPPerson) (*identity.Identity, 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.makePerson(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, @@ -94,10 +113,15 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error { continue } + owner, err := li.makePerson(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), + owner, createdAt.Unix(), lpMessage.Content, nil, diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 227dea27..aff58acc 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -11,7 +11,7 @@ import ( func TestCreate(t *testing.T) { snapshot := Snapshot{} - var rene = identity.NewBare("René Descartes", "rene@descartes.fr") + var rene = identity.NewIdentity("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index ba9bc9d5..7eee2fc1 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -11,7 +11,7 @@ import ( func TestEdit(t *testing.T) { snapshot := Snapshot{} - var rene = identity.NewBare("René Descartes", "rene@descartes.fr") + var rene = identity.NewIdentity("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index c6f5c3c1..6e62c9a3 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -11,7 +11,7 @@ import ( func TestSetMetadata(t *testing.T) { snapshot := Snapshot{} - var rene = identity.NewBare("René Descartes", "rene@descartes.fr") + var rene = identity.NewIdentity("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go index 6b32cfc4..b8e1bf09 100644 --- a/bug/operation_iterator_test.go +++ b/bug/operation_iterator_test.go @@ -8,7 +8,7 @@ import ( ) var ( - rene = identity.NewBare("René Descartes", "rene@descartes.fr") + rene = identity.NewIdentity("René Descartes", "rene@descartes.fr") unix = time.Now().Unix() createOp = NewCreateOp(rene, unix, "title", "message", nil) diff --git a/bug/operation_test.go b/bug/operation_test.go index 0e2afc6c..083ccb1e 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -26,11 +26,11 @@ func TestValidate(t *testing.T) { bad := []Operation{ // opbase - NewSetStatusOp(identity.NewBare("", "rene@descartes.fr"), unix, ClosedStatus), - NewSetStatusOp(identity.NewBare("René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus), - NewSetStatusOp(identity.NewBare("René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus), - NewSetStatusOp(identity.NewBare("René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus), - NewSetStatusOp(identity.NewBare("René Descartes", "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, diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 25ff000c..53c5c7d9 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -11,6 +11,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 diff --git a/cache/multi_repo_cache.go b/cache/multi_repo_cache.go index ec435ff2..da1c26bd 100644 --- a/cache/multi_repo_cache.go +++ b/cache/multi_repo_cache.go @@ -8,6 +8,7 @@ import ( const lockfile = "lock" +// MultiRepoCache is the root cache, holding multiple RepoCache. type MultiRepoCache struct { repos map[string]*RepoCache } diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 7d3e7d1d..e1a3d8f8 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -23,6 +23,20 @@ import ( const cacheFile = "cache" const formatVersion = 1 +// 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 @@ -406,9 +420,14 @@ func (c *RepoCache) NewBugRaw(author *identity.Identity, unixTime int64, title s 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 @@ -546,52 +565,81 @@ func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) { return i, nil } -// ResolveIdentityPrefix retrieve an Identity matching an id prefix. It fails if multiple -// bugs match. -// func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*BugCache, error) { -// // preallocate but empty -// matching := make([]string, 0, 5) -// -// for id := range c.excerpts { -// if strings.HasPrefix(id, prefix) { -// matching = append(matching, id) -// } -// } -// -// if len(matching) > 1 { -// return nil, bug.ErrMultipleMatch{Matching: matching} -// } -// -// if len(matching) == 0 { -// return nil, bug.ErrBugNotExist -// } -// -// return c.ResolveBug(matching[0]) -// } +// ResolveIdentityPrefix retrieve an Identity matching an id prefix. +// It fails if multiple identities match. +func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*identity.Identity, error) { + // preallocate but empty + matching := make([]string, 0, 5) + + for id := range c.identities { + 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) (*BugCache, error) { - // // preallocate but empty - // matching := make([]string, 0, 5) - // - // for id, excerpt := range c.excerpts { - // if excerpt.CreateMetadata[key] == value { - // matching = append(matching, id) - // } - // } - // - // if len(matching) > 1 { - // return nil, bug.ErrMultipleMatch{Matching: matching} - // } - // - // if len(matching) == 0 { - // return nil, bug.ErrBugNotExist - // } - // - // return c.ResolveBug(matching[0]) - - // TODO - - return nil, nil +func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*identity.Identity, error) { + // preallocate but empty + matching := make([]string, 0, 5) + + for id, i := range c.identities { + 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]) +} + +// NewIdentity create a new identity +// The new identity is written in the repository (commit) +func (c *RepoCache) NewIdentity(name string, email string) (*identity.Identity, 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) (*identity.Identity, 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) (*identity.Identity, 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()) + } + + c.identities[i.Id()] = i + + return i, nil } diff --git a/commands/ls.go b/commands/ls.go index 2f621bc5..f641b58a 100644 --- a/commands/ls.go +++ b/commands/ls.go @@ -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] diff --git a/commands/show.go b/commands/show.go index 56717b3b..123a46dc 100644 --- a/commands/show.go +++ b/commands/show.go @@ -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 == "" { diff --git a/doc/man/git-bug-id.1 b/doc/man/git-bug-id.1 new file mode 100644 index 00000000..259c4c48 --- /dev/null +++ b/doc/man/git-bug-id.1 @@ -0,0 +1,29 @@ +.TH "GIT-BUG" "1" "Jan 2019" "Generated from git-bug's source code" "" +.nh +.ad l + + +.SH NAME +.PP +git\-bug\-id \- Display or change the user identity + + +.SH SYNOPSIS +.PP +\fBgit\-bug id [] [flags]\fP + + +.SH DESCRIPTION +.PP +Display or change the user identity + + +.SH OPTIONS +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for id + + +.SH SEE ALSO +.PP +\fBgit\-bug(1)\fP diff --git a/doc/md/git-bug_id.md b/doc/md/git-bug_id.md new file mode 100644 index 00000000..09f8f276 --- /dev/null +++ b/doc/md/git-bug_id.md @@ -0,0 +1,22 @@ +## git-bug id + +Display or change the user identity + +### Synopsis + +Display or change the user identity + +``` +git-bug id [] [flags] +``` + +### Options + +``` + -h, --help help for id +``` + +### SEE ALSO + +* [git-bug](git-bug.md) - A bug tracker embedded in Git + diff --git a/graphql/resolvers/identity.go b/graphql/resolvers/identity.go new file mode 100644 index 00000000..cc68197f --- /dev/null +++ b/graphql/resolvers/identity.go @@ -0,0 +1,36 @@ +package resolvers + +import ( + "context" + + "github.com/MichaelMure/git-bug/identity" +) + +type identityResolver struct{} + +func (identityResolver) Name(ctx context.Context, obj *identity.Interface) (*string, error) { + return nilIfEmpty((*obj).Name()) +} + +func (identityResolver) Email(ctx context.Context, obj *identity.Interface) (*string, error) { + return nilIfEmpty((*obj).Email()) +} + +func (identityResolver) Login(ctx context.Context, obj *identity.Interface) (*string, error) { + return nilIfEmpty((*obj).Login()) +} + +func (identityResolver) DisplayName(ctx context.Context, obj *identity.Interface) (string, error) { + return (*obj).DisplayName(), nil +} + +func (identityResolver) AvatarURL(ctx context.Context, obj *identity.Interface) (*string, error) { + return nilIfEmpty((*obj).AvatarUrl()) +} + +func nilIfEmpty(s string) (*string, error) { + if s == "" { + return nil, nil + } + return &s, nil +} diff --git a/graphql/resolvers/root.go b/graphql/resolvers/root.go index 9b3a730b..cfdfe346 100644 --- a/graphql/resolvers/root.go +++ b/graphql/resolvers/root.go @@ -32,6 +32,10 @@ func (RootResolver) Bug() graph.BugResolver { return &bugResolver{} } +func (r RootResolver) Identity() graph.IdentityResolver { + return &identityResolver{} +} + func (RootResolver) CommentHistoryStep() graph.CommentHistoryStepResolver { return &commentHistoryStepResolver{} } diff --git a/identity/identity.go b/identity/identity.go index 3d523d38..313e3fd7 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -18,6 +18,16 @@ const identityConfigKey = "git-bug.identity" var ErrIdentityNotExist = errors.New("identity doesn't exist") +type ErrMultipleMatch struct { + Matching []string +} + +func (e ErrMultipleMatch) Error() string { + return fmt.Sprintf("Multiple matching identities found:\n%s", strings.Join(e.Matching, "\n")) +} + +var _ Interface = &Identity{} + type Identity struct { id string Versions []*Version diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 8551223d..98d94a35 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -450,6 +450,26 @@ _git-bug_deselect() noun_aliases=() } +_git-bug_id() +{ + last_command="git-bug_id" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _git-bug_label_add() { last_command="git-bug_label_add" @@ -863,6 +883,7 @@ _git-bug_root_command() commands+=("commands") commands+=("comment") commands+=("deselect") + commands+=("id") commands+=("label") commands+=("ls") commands+=("ls-id") diff --git a/misc/random_bugs/create_random_bugs.go b/misc/random_bugs/create_random_bugs.go index f30a9d8a..085e89f0 100644 --- a/misc/random_bugs/create_random_bugs.go +++ b/misc/random_bugs/create_random_bugs.go @@ -138,7 +138,7 @@ func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int } func person() identity.Interface { - return identity.NewBare(fake.FullName(), fake.EmailAddress()) + return identity.NewIdentity(fake.FullName(), fake.EmailAddress()) } var persons []identity.Interface diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index a416ccef..d966b9be 100644 --- a/misc/zsh_completion/git-bug +++ b/misc/zsh_completion/git-bug @@ -8,7 +8,7 @@ case $state in level1) case $words[1] in git-bug) - _arguments '1: :(add bridge commands comment deselect label ls ls-label pull push select show status termui title version webui)' + _arguments '1: :(add bridge commands comment deselect id label ls ls-label pull push select show status termui title version webui)' ;; *) _arguments '*: :_files' -- cgit From d10c76469d40f13e27739fd363145e89bf74c3e0 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 19 Jan 2019 19:23:31 +0100 Subject: identity: somewhat getting closer ! --- bug/bug_actions_test.go | 149 +++++++++++++++++++++-------------------- bug/bug_test.go | 6 +- bug/op_add_comment.go | 54 ++++++++++++++- bug/op_add_comment_test.go | 25 +++++++ bug/op_create.go | 59 ++++++++++++++-- bug/op_create_test.go | 17 +++++ bug/op_edit_comment.go | 58 +++++++++++++++- bug/op_edit_comment_test.go | 18 ++++- bug/op_label_change.go | 53 ++++++++++++++- bug/op_label_change_test.go | 25 +++++++ bug/op_noop.go | 42 ++++++++++++ bug/op_noop_test.go | 25 +++++++ bug/op_set_metadata.go | 54 ++++++++++++++- bug/op_set_metadata_test.go | 19 ++++++ bug/op_set_status.go | 49 +++++++++++++- bug/op_set_status_test.go | 25 +++++++ bug/op_set_title.go | 53 ++++++++++++++- bug/op_set_title_test.go | 25 +++++++ bug/operation.go | 36 +++++----- bug/operation_iterator_test.go | 8 ++- bug/operation_pack.go | 8 +++ identity/bare.go | 55 +++++++++++---- identity/bare_test.go | 13 ++++ identity/common.go | 53 +++++++++++++++ identity/identity.go | 16 +---- identity/interface.go | 5 ++ util/git/hash.go | 2 +- 27 files changed, 813 insertions(+), 139 deletions(-) create mode 100644 bug/op_add_comment_test.go create mode 100644 bug/op_label_change_test.go create mode 100644 bug/op_noop_test.go create mode 100644 bug/op_set_status_test.go create mode 100644 bug/op_set_title_test.go create mode 100644 identity/bare_test.go create mode 100644 identity/common.go diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go index 4327ae58..95ca01c9 100644 --- a/bug/bug_actions_test.go +++ b/bug/bug_actions_test.go @@ -77,17 +77,20 @@ func TestPushPull(t *testing.T) { repoA, repoB, remote := setupRepos(t) defer cleanupRepos(repoA, repoB, remote) + err := rene.Commit(repoA) + assert.NoError(t, err) + bug1, _, err := Create(rene, unix, "bug1", "message") - assert.Nil(t, err) + assert.NoError(t, err) err = bug1.Commit(repoA) - assert.Nil(t, err) + assert.NoError(t, err) // A --> remote --> B _, err = Push(repoA, "origin") - assert.Nil(t, err) + assert.NoError(t, err) err = Pull(repoB, "origin") - assert.Nil(t, err) + assert.NoError(t, err) bugs := allBugs(t, ReadAllLocalBugs(repoB)) @@ -97,15 +100,15 @@ func TestPushPull(t *testing.T) { // B --> remote --> A bug2, _, err := Create(rene, unix, "bug2", "message") - assert.Nil(t, err) + assert.NoError(t, err) err = bug2.Commit(repoB) - assert.Nil(t, err) + assert.NoError(t, err) _, err = Push(repoB, "origin") - assert.Nil(t, err) + assert.NoError(t, err) err = Pull(repoA, "origin") - assert.Nil(t, err) + assert.NoError(t, err) bugs = allBugs(t, ReadAllLocalBugs(repoA)) @@ -140,37 +143,37 @@ func _RebaseTheirs(t testing.TB) { defer cleanupRepos(repoA, repoB, remote) bug1, _, err := Create(rene, unix, "bug1", "message") - assert.Nil(t, err) + assert.NoError(t, err) err = bug1.Commit(repoA) - assert.Nil(t, err) + assert.NoError(t, err) // A --> remote _, err = Push(repoA, "origin") - assert.Nil(t, err) + assert.NoError(t, err) // remote --> B err = Pull(repoB, "origin") - assert.Nil(t, err) + assert.NoError(t, err) bug2, err := ReadLocalBug(repoB, bug1.Id()) - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message2") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message3") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message4") - assert.Nil(t, err) + assert.NoError(t, err) err = bug2.Commit(repoB) - assert.Nil(t, err) + assert.NoError(t, err) // B --> remote _, err = Push(repoB, "origin") - assert.Nil(t, err) + assert.NoError(t, err) // remote --> A err = Pull(repoA, "origin") - assert.Nil(t, err) + assert.NoError(t, err) bugs := allBugs(t, ReadAllLocalBugs(repoB)) @@ -179,7 +182,7 @@ func _RebaseTheirs(t testing.TB) { } bug3, err := ReadLocalBug(repoA, bug1.Id()) - assert.Nil(t, err) + assert.NoError(t, err) if nbOps(bug3) != 4 { t.Fatal("Unexpected number of operations") @@ -201,48 +204,48 @@ func _RebaseOurs(t testing.TB) { defer cleanupRepos(repoA, repoB, remote) bug1, _, err := Create(rene, unix, "bug1", "message") - assert.Nil(t, err) + assert.NoError(t, err) err = bug1.Commit(repoA) - assert.Nil(t, err) + assert.NoError(t, err) // A --> remote _, err = Push(repoA, "origin") - assert.Nil(t, err) + assert.NoError(t, err) // remote --> B err = Pull(repoB, "origin") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message2") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message3") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message4") - assert.Nil(t, err) + assert.NoError(t, err) err = bug1.Commit(repoA) - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message5") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message6") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message7") - assert.Nil(t, err) + assert.NoError(t, err) err = bug1.Commit(repoA) - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message8") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message9") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message10") - assert.Nil(t, err) + assert.NoError(t, err) err = bug1.Commit(repoA) - assert.Nil(t, err) + assert.NoError(t, err) // remote --> A err = Pull(repoA, "origin") - assert.Nil(t, err) + assert.NoError(t, err) bugs := allBugs(t, ReadAllLocalBugs(repoA)) @@ -251,7 +254,7 @@ func _RebaseOurs(t testing.TB) { } bug2, err := ReadLocalBug(repoA, bug1.Id()) - assert.Nil(t, err) + assert.NoError(t, err) if nbOps(bug2) != 10 { t.Fatal("Unexpected number of operations") @@ -282,82 +285,82 @@ func _RebaseConflict(t testing.TB) { defer cleanupRepos(repoA, repoB, remote) bug1, _, err := Create(rene, unix, "bug1", "message") - assert.Nil(t, err) + assert.NoError(t, err) err = bug1.Commit(repoA) - assert.Nil(t, err) + assert.NoError(t, err) // A --> remote _, err = Push(repoA, "origin") - assert.Nil(t, err) + assert.NoError(t, err) // remote --> B err = Pull(repoB, "origin") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message2") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message3") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message4") - assert.Nil(t, err) + assert.NoError(t, err) err = bug1.Commit(repoA) - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message5") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message6") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message7") - assert.Nil(t, err) + assert.NoError(t, err) err = bug1.Commit(repoA) - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message8") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message9") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug1, rene, unix, "message10") - assert.Nil(t, err) + assert.NoError(t, err) err = bug1.Commit(repoA) - assert.Nil(t, err) + assert.NoError(t, err) bug2, err := ReadLocalBug(repoB, bug1.Id()) - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message11") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message12") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message13") - assert.Nil(t, err) + assert.NoError(t, err) err = bug2.Commit(repoB) - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message14") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message15") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message16") - assert.Nil(t, err) + assert.NoError(t, err) err = bug2.Commit(repoB) - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message17") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message18") - assert.Nil(t, err) + assert.NoError(t, err) _, err = AddComment(bug2, rene, unix, "message19") - assert.Nil(t, err) + assert.NoError(t, err) err = bug2.Commit(repoB) - assert.Nil(t, err) + assert.NoError(t, err) // A --> remote _, err = Push(repoA, "origin") - assert.Nil(t, err) + assert.NoError(t, err) // remote --> B err = Pull(repoB, "origin") - assert.Nil(t, err) + assert.NoError(t, err) bugs := allBugs(t, ReadAllLocalBugs(repoB)) @@ -366,7 +369,7 @@ func _RebaseConflict(t testing.TB) { } bug3, err := ReadLocalBug(repoB, bug1.Id()) - assert.Nil(t, err) + assert.NoError(t, err) if nbOps(bug3) != 19 { t.Fatal("Unexpected number of operations") @@ -374,11 +377,11 @@ func _RebaseConflict(t testing.TB) { // B --> remote _, err = Push(repoB, "origin") - assert.Nil(t, err) + assert.NoError(t, err) // remote --> A err = Pull(repoA, "origin") - assert.Nil(t, err) + assert.NoError(t, err) bugs = allBugs(t, ReadAllLocalBugs(repoA)) @@ -387,7 +390,7 @@ func _RebaseConflict(t testing.TB) { } bug4, err := ReadLocalBug(repoA, bug1.Id()) - assert.Nil(t, err) + assert.NoError(t, err) if nbOps(bug4) != 19 { t.Fatal("Unexpected number of operations") diff --git a/bug/bug_test.go b/bug/bug_test.go index 0fd373d5..41a5b03d 100644 --- a/bug/bug_test.go +++ b/bug/bug_test.go @@ -2,7 +2,6 @@ package bug import ( "github.com/MichaelMure/git-bug/repository" - "github.com/go-test/deep" "github.com/stretchr/testify/assert" "testing" @@ -87,8 +86,5 @@ func TestBugSerialisation(t *testing.T) { } } - deep.CompareUnexportedFields = true - if diff := deep.Equal(bug1, bug2); diff != nil { - t.Fatal(diff) - } + assert.Equal(t, bug1, bug2) } diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index 23a10419..ba5d611e 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -1,10 +1,10 @@ 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" ) @@ -14,9 +14,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 { @@ -67,6 +67,54 @@ 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() {} diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go new file mode 100644 index 00000000..a38d0228 --- /dev/null +++ b/bug/op_add_comment_test.go @@ -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) +} diff --git a/bug/op_create.go b/bug/op_create.go index 01b2bf03..1d157e67 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -1,11 +1,11 @@ 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" ) @@ -15,9 +15,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 { @@ -83,6 +83,57 @@ 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() {} diff --git a/bug/op_create_test.go b/bug/op_create_test.go index aff58acc..31693a4a 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -1,11 +1,13 @@ package bug import ( + "encoding/json" "testing" "time" "github.com/MichaelMure/git-bug/identity" "github.com/go-test/deep" + "github.com/stretchr/testify/assert" ) func TestCreate(t *testing.T) { @@ -45,3 +47,18 @@ func TestCreate(t *testing.T) { t.Fatal(diff) } } + +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) +} diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index 9e0afc02..3ff16653 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -1,6 +1,7 @@ package bug import ( + "encoding/json" "fmt" "github.com/MichaelMure/git-bug/identity" @@ -14,9 +15,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 { @@ -94,6 +95,57 @@ 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() {} diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index 7eee2fc1..dbdf341d 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -1,11 +1,12 @@ package bug import ( + "encoding/json" "testing" "time" "github.com/MichaelMure/git-bug/identity" - "gotest.tools/assert" + "github.com/stretchr/testify/assert" ) func TestEdit(t *testing.T) { @@ -49,3 +50,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) +} diff --git a/bug/op_label_change.go b/bug/op_label_change.go index 5d0b6a78..b0dd2c33 100644 --- a/bug/op_label_change.go +++ b/bug/op_label_change.go @@ -1,6 +1,7 @@ package bug import ( + "encoding/json" "fmt" "sort" @@ -15,8 +16,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 { @@ -99,6 +100,54 @@ 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() {} diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go new file mode 100644 index 00000000..f5550b72 --- /dev/null +++ b/bug/op_label_change_test.go @@ -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) +} diff --git a/bug/op_noop.go b/bug/op_noop.go index 410799b3..fbc112a8 100644 --- a/bug/op_noop.go +++ b/bug/op_noop.go @@ -1,6 +1,8 @@ package bug import ( + "encoding/json" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/git" ) @@ -30,6 +32,46 @@ 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() {} diff --git a/bug/op_noop_test.go b/bug/op_noop_test.go new file mode 100644 index 00000000..385bc914 --- /dev/null +++ b/bug/op_noop_test.go @@ -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) +} diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go index e18f1cb6..57b78667 100644 --- a/bug/op_set_metadata.go +++ b/bug/op_set_metadata.go @@ -1,6 +1,8 @@ package bug import ( + "encoding/json" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/git" ) @@ -9,8 +11,8 @@ 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 { @@ -56,6 +58,54 @@ func (op *SetMetadataOperation) Validate() error { 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() {} diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index 6e62c9a3..847164f3 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -1,6 +1,7 @@ package bug import ( + "encoding/json" "testing" "time" @@ -94,3 +95,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) +} diff --git a/bug/op_set_status.go b/bug/op_set_status.go index 9fc64e52..6deb1675 100644 --- a/bug/op_set_status.go +++ b/bug/op_set_status.go @@ -1,6 +1,8 @@ package bug import ( + "encoding/json" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/git" "github.com/pkg/errors" @@ -11,7 +13,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 { @@ -54,6 +56,51 @@ 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() {} diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go new file mode 100644 index 00000000..2506b947 --- /dev/null +++ b/bug/op_set_status_test.go @@ -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) +} diff --git a/bug/op_set_title.go b/bug/op_set_title.go index 3b253c06..ae6484c6 100644 --- a/bug/op_set_title.go +++ b/bug/op_set_title.go @@ -1,6 +1,7 @@ package bug import ( + "encoding/json" "fmt" "strings" @@ -15,8 +16,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 { @@ -76,6 +77,54 @@ 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() {} diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go new file mode 100644 index 00000000..1f730596 --- /dev/null +++ b/bug/op_set_title_test.go @@ -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) +} diff --git a/bug/operation.go b/bug/operation.go index 8dec5644..cc5b0007 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -76,8 +76,6 @@ func hashOperation(op Operation) (git.Hash, error) { return base.hash, nil } -// TODO: serialization with identity - // OpBase implement the common code for all operations type OpBase struct { OperationType OperationType @@ -100,28 +98,40 @@ func newOpBase(opType OperationType, author identity.Interface, unixTime int64) } } -type opBaseJson struct { - OperationType OperationType `json:"type"` - UnixTime int64 `json:"timestamp"` - Metadata map[string]string `json:"metadata,omitempty"` -} - -func (op *OpBase) MarshalJSON() ([]byte, error) { - return json.Marshal(opBaseJson{ +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 := opBaseJson{} + 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 @@ -149,10 +159,6 @@ 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") } diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go index b8e1bf09..e1aa8911 100644 --- a/bug/operation_iterator_test.go +++ b/bug/operation_iterator_test.go @@ -3,6 +3,8 @@ package bug import ( "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" + "github.com/stretchr/testify/assert" + "testing" "time" ) @@ -29,13 +31,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) diff --git a/bug/operation_pack.go b/bug/operation_pack.go index fc395d90..18b2a478 100644 --- a/bug/operation_pack.go +++ b/bug/operation_pack.go @@ -139,6 +139,14 @@ 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) { + // First, make sure that all the identities are properly Commit as well + for _, op := range opp.Operations { + err := op.base().Author.Commit(repo) + if err != nil { + return "", err + } + } + data, err := json.Marshal(opp) if err != nil { diff --git a/identity/bare.go b/identity/bare.go index 24f30f9f..729dc2e0 100644 --- a/identity/bare.go +++ b/identity/bare.go @@ -1,20 +1,25 @@ package identity import ( + "crypto/sha256" "encoding/json" "fmt" "strings" + "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" "github.com/MichaelMure/git-bug/util/text" ) +var _ Interface = &Bare{} + // Bare is a very minimal identity, designed to be fully embedded directly along // other data. // // in particular, this identity is designed to be compatible with the handling of // identities in the early version of git-bug. type Bare struct { + id string name string email string login string @@ -36,7 +41,7 @@ type bareIdentityJson struct { AvatarUrl string `json:"avatar_url,omitempty"` } -func (i Bare) MarshalJSON() ([]byte, error) { +func (i *Bare) MarshalJSON() ([]byte, error) { return json.Marshal(bareIdentityJson{ Name: i.name, Email: i.email, @@ -45,7 +50,7 @@ func (i Bare) MarshalJSON() ([]byte, error) { }) } -func (i Bare) UnmarshalJSON(data []byte) error { +func (i *Bare) UnmarshalJSON(data []byte) error { aux := bareIdentityJson{} if err := json.Unmarshal(data, &aux); err != nil { @@ -60,35 +65,54 @@ func (i Bare) UnmarshalJSON(data []byte) error { return nil } -func (i Bare) Name() string { +func (i *Bare) Id() string { + // We don't have a proper ID at hand, so let's hash all the data to get one. + // Hopefully the + + if i.id != "" { + return i.id + } + + data, err := json.Marshal(i) + if err != nil { + panic(err) + } + + h := fmt.Sprintf("%x", sha256.New().Sum(data)[:16]) + i.id = string(h) + + return i.id +} + +func (i *Bare) Name() string { return i.name } -func (i Bare) Email() string { +func (i *Bare) Email() string { return i.email } -func (i Bare) Login() string { +func (i *Bare) Login() string { return i.login } -func (i Bare) AvatarUrl() string { +func (i *Bare) AvatarUrl() string { return i.avatarUrl } // Keys return the last version of the valid keys -func (i Bare) Keys() []Key { +func (i *Bare) Keys() []Key { return []Key{} } // ValidKeysAtTime return the set of keys valid at a given lamport time -func (i Bare) ValidKeysAtTime(time lamport.Time) []Key { +func (i *Bare) ValidKeysAtTime(time lamport.Time) []Key { return []Key{} } // DisplayName return a non-empty string to display, representing the // identity, based on the non-empty values. -func (i Bare) DisplayName() string { +func (i *Bare) DisplayName() string { switch { case i.name == "" && i.login != "": return i.login @@ -102,7 +126,7 @@ func (i Bare) DisplayName() string { } // Match tell is the Person match the given query string -func (i Bare) Match(query string) bool { +func (i *Bare) Match(query string) bool { query = strings.ToLower(query) return strings.Contains(strings.ToLower(i.name), query) || @@ -110,7 +134,7 @@ func (i Bare) Match(query string) bool { } // Validate check if the Identity data is valid -func (i Bare) Validate() error { +func (i *Bare) Validate() error { if text.Empty(i.name) && text.Empty(i.login) { return fmt.Errorf("either name or login should be set") } @@ -146,8 +170,15 @@ func (i Bare) Validate() error { return nil } +// Write the identity into the Repository. In particular, this ensure that +// the Id is properly set. +func (i *Bare) Commit(repo repository.Repo) error { + // Nothing to do, everything is directly embedded + return nil +} + // IsProtected return true if the chain of git commits started to be signed. // If that's the case, only signed commit with a valid key for this identity can be added. -func (i Bare) IsProtected() bool { +func (i *Bare) IsProtected() bool { return false } diff --git a/identity/bare_test.go b/identity/bare_test.go new file mode 100644 index 00000000..1458107a --- /dev/null +++ b/identity/bare_test.go @@ -0,0 +1,13 @@ +package identity + +import ( + "testing" + + "github.com/magiconair/properties/assert" +) + +func TestBare_Id(t *testing.T) { + i := NewBare("name", "email") + id := i.Id() + assert.Equal(t, "7b226e616d65223a226e616d65222c22", id) +} diff --git a/identity/common.go b/identity/common.go new file mode 100644 index 00000000..32dd3d9e --- /dev/null +++ b/identity/common.go @@ -0,0 +1,53 @@ +package identity + +import ( + "encoding/json" + "errors" + "fmt" + "strings" +) + +var ErrIdentityNotExist = errors.New("identity doesn't exist") + +type ErrMultipleMatch struct { + Matching []string +} + +func (e ErrMultipleMatch) Error() string { + return fmt.Sprintf("Multiple matching identities found:\n%s", strings.Join(e.Matching, "\n")) +} + +// Custom unmarshaling function to allow package user to delegate +// the decoding of an Identity and distinguish between an Identity +// and a Bare. +// +// If the given message has a "id" field, it's considered being a proper Identity. +func UnmarshalJSON(raw json.RawMessage) (Interface, error) { + // First try to decode as a normal Identity + var i Identity + + err := json.Unmarshal(raw, &i) + if err == nil && i.id != "" { + return &i, nil + } + + // abort if we have an error other than the wrong type + if _, ok := err.(*json.UnmarshalTypeError); err != nil && !ok { + return nil, err + } + + // Fallback on a legacy Bare identity + var b Bare + + err = json.Unmarshal(raw, &b) + if err == nil && (b.name != "" || b.login != "") { + return &b, nil + } + + // abort if we have an error other than the wrong type + if _, ok := err.(*json.UnmarshalTypeError); err != nil && !ok { + return nil, err + } + + return nil, fmt.Errorf("unknown identity type") +} diff --git a/identity/identity.go b/identity/identity.go index 313e3fd7..38729e37 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -16,16 +16,6 @@ const identityRefPattern = "refs/identities/" const versionEntryName = "version" const identityConfigKey = "git-bug.identity" -var ErrIdentityNotExist = errors.New("identity doesn't exist") - -type ErrMultipleMatch struct { - Matching []string -} - -func (e ErrMultipleMatch) Error() string { - return fmt.Sprintf("Multiple matching identities found:\n%s", strings.Join(e.Matching, "\n")) -} - var _ Interface = &Identity{} type Identity struct { @@ -85,8 +75,6 @@ func (i *Identity) UnmarshalJSON(data []byte) error { return nil } -// TODO: load/write from OpBase - // Read load an Identity from the identities data available in git func Read(repo repository.Repo, id string) (*Identity, error) { i := &Identity{ @@ -230,7 +218,9 @@ func (i *Identity) AddVersion(version *Version) { i.Versions = append(i.Versions, version) } -func (i *Identity) Commit(repo repository.ClockedRepo) error { +// Write the identity into the Repository. In particular, this ensure that +// the Id is properly set. +func (i *Identity) Commit(repo repository.Repo) error { // Todo: check for mismatch between memory and commited data var lastCommit git.Hash = "" diff --git a/identity/interface.go b/identity/interface.go index 6489efbe..c784a7a6 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -1,6 +1,7 @@ package identity import ( + "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -28,6 +29,10 @@ type Interface interface { // Validate check if the Identity data is valid Validate() error + // Write the identity into the Repository. In particular, this ensure that + // the Id is properly set. + Commit(repo repository.Repo) error + // IsProtected return true if the chain of git commits started to be signed. // If that's the case, only signed commit with a valid key for this identity can be added. IsProtected() bool diff --git a/util/git/hash.go b/util/git/hash.go index 401e6edc..d9160d75 100644 --- a/util/git/hash.go +++ b/util/git/hash.go @@ -30,7 +30,7 @@ func (h *Hash) UnmarshalGQL(v interface{}) error { // MarshalGQL implement the Marshaler interface for gqlgen func (h Hash) MarshalGQL(w io.Writer) { - w.Write([]byte(`"` + h.String() + `"`)) + _, _ = w.Write([]byte(`"` + h.String() + `"`)) } // IsValid tell if the hash is valid -- cgit From 14b240af8fef269d2c1d5dde2fff192b656c50f3 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 20 Jan 2019 15:41:27 +0100 Subject: identity: more cleaning and fixes after a code review --- Gopkg.lock | 37 -- bug/bug_actions_test.go | 2 +- bug/op_create_test.go | 10 +- bug/operation_pack.go | 6 + bug/operation_pack_test.go | 25 +- cache/bug_cache.go | 12 +- cache/bug_excerpt.go | 18 +- cache/filter.go | 7 +- cache/repo_cache.go | 2 +- commands/id.go | 2 +- graphql/schema/bug.graphql | 1 - graphql/schema/identity.graphql | 2 +- graphql/schema/operations.graphql | 2 +- graphql/schema/root.graphql | 2 +- graphql/schema/timeline.graphql | 2 +- identity/bare.go | 14 +- identity/bare_test.go | 2 +- identity/common.go | 4 + identity/identity.go | 53 +- identity/identity_test.go | 13 +- identity/interface.go | 3 - identity/key.go | 6 + identity/version.go | 84 +++- vendor/github.com/go-test/deep/.gitignore | 2 - vendor/github.com/go-test/deep/.travis.yml | 13 - vendor/github.com/go-test/deep/CHANGES.md | 9 - vendor/github.com/go-test/deep/LICENSE | 21 - vendor/github.com/go-test/deep/README.md | 51 -- vendor/github.com/go-test/deep/deep.go | 352 ------------- vendor/github.com/google/go-cmp/LICENSE | 27 - vendor/github.com/google/go-cmp/cmp/compare.go | 553 --------------------- .../go-cmp/cmp/internal/diff/debug_disable.go | 17 - .../go-cmp/cmp/internal/diff/debug_enable.go | 122 ----- .../google/go-cmp/cmp/internal/diff/diff.go | 363 -------------- .../google/go-cmp/cmp/internal/function/func.go | 49 -- .../google/go-cmp/cmp/internal/value/format.go | 277 ----------- .../google/go-cmp/cmp/internal/value/sort.go | 111 ----- vendor/github.com/google/go-cmp/cmp/options.go | 453 ----------------- vendor/github.com/google/go-cmp/cmp/path.go | 309 ------------ vendor/github.com/google/go-cmp/cmp/reporter.go | 53 -- .../github.com/google/go-cmp/cmp/unsafe_panic.go | 15 - .../github.com/google/go-cmp/cmp/unsafe_reflect.go | 23 - vendor/gotest.tools/LICENSE | 202 -------- vendor/gotest.tools/assert/assert.go | 311 ------------ vendor/gotest.tools/assert/cmp/compare.go | 312 ------------ vendor/gotest.tools/assert/cmp/result.go | 94 ---- vendor/gotest.tools/assert/result.go | 107 ---- vendor/gotest.tools/internal/difflib/LICENSE | 27 - vendor/gotest.tools/internal/difflib/difflib.go | 420 ---------------- vendor/gotest.tools/internal/format/diff.go | 161 ------ vendor/gotest.tools/internal/format/format.go | 27 - vendor/gotest.tools/internal/source/source.go | 163 ------ 52 files changed, 150 insertions(+), 4803 deletions(-) delete mode 100644 vendor/github.com/go-test/deep/.gitignore delete mode 100644 vendor/github.com/go-test/deep/.travis.yml delete mode 100644 vendor/github.com/go-test/deep/CHANGES.md delete mode 100644 vendor/github.com/go-test/deep/LICENSE delete mode 100644 vendor/github.com/go-test/deep/README.md delete mode 100644 vendor/github.com/go-test/deep/deep.go delete mode 100644 vendor/github.com/google/go-cmp/LICENSE delete mode 100644 vendor/github.com/google/go-cmp/cmp/compare.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/internal/function/func.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/internal/value/format.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/internal/value/sort.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/options.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/path.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/reporter.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/unsafe_panic.go delete mode 100644 vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go delete mode 100644 vendor/gotest.tools/LICENSE delete mode 100644 vendor/gotest.tools/assert/assert.go delete mode 100644 vendor/gotest.tools/assert/cmp/compare.go delete mode 100644 vendor/gotest.tools/assert/cmp/result.go delete mode 100644 vendor/gotest.tools/assert/result.go delete mode 100644 vendor/gotest.tools/internal/difflib/LICENSE delete mode 100644 vendor/gotest.tools/internal/difflib/difflib.go delete mode 100644 vendor/gotest.tools/internal/format/diff.go delete mode 100644 vendor/gotest.tools/internal/format/format.go delete mode 100644 vendor/gotest.tools/internal/source/source.go diff --git a/Gopkg.lock b/Gopkg.lock index a208c91a..eb03979c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -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,7 +431,6 @@ "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", @@ -486,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 diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go index 95ca01c9..af561bf6 100644 --- a/bug/bug_actions_test.go +++ b/bug/bug_actions_test.go @@ -50,7 +50,7 @@ func cleanupRepo(repo repository.Repo) error { func setupRepos(t testing.TB) (repoA, repoB, remote *repository.GitRepo) { repoA = createRepo(false) repoB = createRepo(false) - remote = createRepo(false) + remote = createRepo(true) remoteAddr := "file://" + remote.GetPath() diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 31693a4a..065b81c5 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/MichaelMure/git-bug/identity" - "github.com/go-test/deep" "github.com/stretchr/testify/assert" ) @@ -22,9 +21,7 @@ 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)} @@ -42,10 +39,7 @@ 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) { diff --git a/bug/operation_pack.go b/bug/operation_pack.go index 18b2a478..1ffc1d1a 100644 --- a/bug/operation_pack.go +++ b/bug/operation_pack.go @@ -139,6 +139,12 @@ 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) { + // 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.Commit(repo) diff --git a/bug/operation_pack_test.go b/bug/operation_pack_test.go index 48f9f80c..8a8c7e62 100644 --- a/bug/operation_pack_test.go +++ b/bug/operation_pack_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/MichaelMure/git-bug/util/git" - "github.com/go-test/deep" + "github.com/stretchr/testify/assert" ) func TestOperationPackSerialize(t *testing.T) { @@ -21,9 +21,7 @@ func TestOperationPackSerialize(t *testing.T) { 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{ "abcdef", @@ -31,23 +29,14 @@ func TestOperationPackSerialize(t *testing.T) { }) 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) - } - - deep.CompareUnexportedFields = false - if diff := deep.Equal(opp, opp2); diff != nil { - t.Fatal(diff) - } + + assert.NoError(t, err) + assert.Equal(t, opp, opp2) } diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 53c5c7d9..53c1db64 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -93,7 +93,7 @@ func (c *BugCache) AddComment(message string) error { } func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error { - author, err := identity.GetIdentity(c.repoCache.repo) + author, err := identity.GetUserIdentity(c.repoCache.repo) if err != nil { return err } @@ -115,7 +115,7 @@ func (c *BugCache) AddCommentRaw(author *identity.Identity, unixTime int64, mess } func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelChangeResult, error) { - author, err := identity.GetIdentity(c.repoCache.repo) + author, err := identity.GetUserIdentity(c.repoCache.repo) if err != nil { return nil, err } @@ -142,7 +142,7 @@ func (c *BugCache) ChangeLabelsRaw(author *identity.Identity, unixTime int64, ad } func (c *BugCache) Open() error { - author, err := identity.GetIdentity(c.repoCache.repo) + author, err := identity.GetUserIdentity(c.repoCache.repo) if err != nil { return err } @@ -164,7 +164,7 @@ func (c *BugCache) OpenRaw(author *identity.Identity, unixTime int64, metadata m } func (c *BugCache) Close() error { - author, err := identity.GetIdentity(c.repoCache.repo) + author, err := identity.GetUserIdentity(c.repoCache.repo) if err != nil { return err } @@ -186,7 +186,7 @@ func (c *BugCache) CloseRaw(author *identity.Identity, unixTime int64, metadata } func (c *BugCache) SetTitle(title string) error { - author, err := identity.GetIdentity(c.repoCache.repo) + author, err := identity.GetUserIdentity(c.repoCache.repo) if err != nil { return err } @@ -208,7 +208,7 @@ func (c *BugCache) SetTitleRaw(author *identity.Identity, unixTime int64, title } func (c *BugCache) EditComment(target git.Hash, message string) error { - author, err := identity.GetIdentity(c.repoCache.repo) + author, err := identity.GetUserIdentity(c.repoCache.repo) if err != nil { return err } diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index f5844b64..daf89c4f 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -3,8 +3,6 @@ package cache import ( "encoding/gob" - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -20,12 +18,17 @@ type BugExcerpt struct { EditUnixTime int64 Status bug.Status - Author identity.Interface + Author AuthorExcerpt Labels []bug.Label CreateMetadata map[string]string } +type AuthorExcerpt struct { + Name string + Login string +} + func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { return &BugExcerpt{ Id: b.Id(), @@ -34,9 +37,12 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { CreateUnixTime: b.FirstOp().GetUnixTime(), EditUnixTime: snap.LastEditUnix(), Status: snap.Status, - Author: snap.Author, - Labels: snap.Labels, - CreateMetadata: b.FirstOp().AllMetadata(), + Author: AuthorExcerpt{ + Login: snap.Author.Login(), + Name: snap.Author.Name(), + }, + Labels: snap.Labels, + CreateMetadata: b.FirstOp().AllMetadata(), } } diff --git a/cache/filter.go b/cache/filter.go index 033df131..3cf4a991 100644 --- a/cache/filter.go +++ b/cache/filter.go @@ -1,6 +1,8 @@ package cache import ( + "strings" + "github.com/MichaelMure/git-bug/bug" ) @@ -22,7 +24,10 @@ func StatusFilter(query string) (Filter, error) { // AuthorFilter return a Filter that match a bug author func AuthorFilter(query string) Filter { return func(excerpt *BugExcerpt) bool { - return excerpt.Author.Match(query) + query = strings.ToLower(query) + + return strings.Contains(strings.ToLower(excerpt.Author.Name), query) || + strings.Contains(strings.ToLower(excerpt.Author.Login), query) } } diff --git a/cache/repo_cache.go b/cache/repo_cache.go index e1a3d8f8..3d8b352b 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -394,7 +394,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 := identity.GetIdentity(c.repo) + author, err := identity.GetUserIdentity(c.repo) if err != nil { return nil, err } diff --git a/commands/id.go b/commands/id.go index 44294132..19d040ee 100644 --- a/commands/id.go +++ b/commands/id.go @@ -19,7 +19,7 @@ func runId(cmd *cobra.Command, args []string) error { if len(args) == 1 { id, err = identity.Read(repo, args[0]) } else { - id, err = identity.GetIdentity(repo) + id, err = identity.GetUserIdentity(repo) } if err != nil { diff --git a/graphql/schema/bug.graphql b/graphql/schema/bug.graphql index 9530c576..7e1c57b5 100644 --- a/graphql/schema/bug.graphql +++ b/graphql/schema/bug.graphql @@ -105,4 +105,3 @@ type Repository { ): BugConnection! bug(prefix: String!): Bug } - diff --git a/graphql/schema/identity.graphql b/graphql/schema/identity.graphql index 9e76d885..7c5ef126 100644 --- a/graphql/schema/identity.graphql +++ b/graphql/schema/identity.graphql @@ -10,4 +10,4 @@ type Identity { displayName: String! """An url to an avatar""" avatarUrl: String -} \ No newline at end of file +} diff --git a/graphql/schema/operations.graphql b/graphql/schema/operations.graphql index 2b206418..d37df2e2 100644 --- a/graphql/schema/operations.graphql +++ b/graphql/schema/operations.graphql @@ -97,4 +97,4 @@ type LabelChangeOperation implements Operation & Authored { added: [Label!]! removed: [Label!]! -} \ No newline at end of file +} diff --git a/graphql/schema/root.graphql b/graphql/schema/root.graphql index 56558f7c..7b43366f 100644 --- a/graphql/schema/root.graphql +++ b/graphql/schema/root.graphql @@ -35,4 +35,4 @@ type Mutation { setTitle(repoRef: String, prefix: String!, title: String!): Bug! commit(repoRef: String, prefix: String!): Bug! -} \ No newline at end of file +} diff --git a/graphql/schema/timeline.graphql b/graphql/schema/timeline.graphql index 29ed6e60..35bb88bf 100644 --- a/graphql/schema/timeline.graphql +++ b/graphql/schema/timeline.graphql @@ -83,4 +83,4 @@ type SetTitleTimelineItem implements TimelineItem { date: Time! title: String! was: String! -} \ No newline at end of file +} diff --git a/identity/bare.go b/identity/bare.go index 729dc2e0..5aaa0166 100644 --- a/identity/bare.go +++ b/identity/bare.go @@ -34,7 +34,7 @@ func NewBareFull(name string, email string, login string, avatarUrl string) *Bar return &Bare{name: name, email: email, login: login, avatarUrl: avatarUrl} } -type bareIdentityJson struct { +type bareIdentityJSON struct { Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` Login string `json:"login,omitempty"` @@ -42,7 +42,7 @@ type bareIdentityJson struct { } func (i *Bare) MarshalJSON() ([]byte, error) { - return json.Marshal(bareIdentityJson{ + return json.Marshal(bareIdentityJSON{ Name: i.name, Email: i.email, Login: i.login, @@ -51,7 +51,7 @@ func (i *Bare) MarshalJSON() ([]byte, error) { } func (i *Bare) UnmarshalJSON(data []byte) error { - aux := bareIdentityJson{} + aux := bareIdentityJSON{} if err := json.Unmarshal(data, &aux); err != nil { return err @@ -125,14 +125,6 @@ func (i *Bare) DisplayName() string { panic("invalid person data") } -// Match tell is the Person match the given query string -func (i *Bare) Match(query string) bool { - query = strings.ToLower(query) - - return strings.Contains(strings.ToLower(i.name), query) || - strings.Contains(strings.ToLower(i.login), query) -} - // Validate check if the Identity data is valid func (i *Bare) Validate() error { if text.Empty(i.name) && text.Empty(i.login) { diff --git a/identity/bare_test.go b/identity/bare_test.go index 1458107a..4b28c7ad 100644 --- a/identity/bare_test.go +++ b/identity/bare_test.go @@ -3,7 +3,7 @@ package identity import ( "testing" - "github.com/magiconair/properties/assert" + "github.com/stretchr/testify/assert" ) func TestBare_Id(t *testing.T) { diff --git a/identity/common.go b/identity/common.go index 32dd3d9e..5301471a 100644 --- a/identity/common.go +++ b/identity/common.go @@ -51,3 +51,7 @@ func UnmarshalJSON(raw json.RawMessage) (Interface, error) { return nil, fmt.Errorf("unknown identity type") } + +type Resolver interface { + ResolveIdentity(id string) (Interface, error) +} diff --git a/identity/identity.go b/identity/identity.go index 38729e37..2a422789 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -4,7 +4,6 @@ package identity import ( "encoding/json" "fmt" - "strings" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" @@ -49,13 +48,13 @@ func NewIdentityFull(name string, email string, login string, avatarUrl string) } } -type identityJson struct { +type identityJSON struct { Id string `json:"id"` } // MarshalJSON will only serialize the id func (i *Identity) MarshalJSON() ([]byte, error) { - return json.Marshal(identityJson{ + return json.Marshal(identityJSON{ Id: i.Id(), }) } @@ -64,7 +63,7 @@ func (i *Identity) MarshalJSON() ([]byte, error) { // Users of this package are expected to run Load() to load // the remaining data from the identities data in git. func (i *Identity) UnmarshalJSON(data []byte) error { - aux := identityJson{} + aux := identityJSON{} if err := json.Unmarshal(data, &aux); err != nil { return err @@ -164,35 +163,13 @@ func NewFromGitUser(repo repository.Repo) (*Identity, error) { return NewIdentity(name, email), nil } -// BuildFromGit will query the repository for user detail and -// build the corresponding Identity -/*func BuildFromGit(repo repository.Repo) *Identity { - version := Version{} - - name, err := repo.GetUserName() - if err == nil { - version.Name = name - } - - email, err := repo.GetUserEmail() - if err == nil { - version.Email = email - } - - return &Identity{ - Versions: []Version{ - version, - }, - } -}*/ - -// SetIdentity store the user identity's id in the git config -func SetIdentity(repo repository.RepoCommon, identity Identity) error { +// SetUserIdentity store the user identity's id in the git config +func SetUserIdentity(repo repository.RepoCommon, identity Identity) error { return repo.StoreConfig(identityConfigKey, identity.Id()) } -// GetIdentity read the current user identity, set with a git config entry -func GetIdentity(repo repository.Repo) (*Identity, error) { +// GetUserIdentity read the current user identity, set with a git config entry +func GetUserIdentity(repo repository.Repo) (*Identity, error) { configs, err := repo.ReadConfigs(identityConfigKey) if err != nil { return nil, err @@ -299,14 +276,6 @@ func (i *Identity) Validate() error { return nil } -func (i *Identity) firstVersion() *Version { - if len(i.Versions) <= 0 { - panic("no version at all") - } - - return i.Versions[0] -} - func (i *Identity) lastVersion() *Version { if len(i.Versions) <= 0 { panic("no version at all") @@ -372,14 +341,6 @@ func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key { return result } -// Match tell is the Identity match the given query string -func (i *Identity) Match(query string) bool { - query = strings.ToLower(query) - - return strings.Contains(strings.ToLower(i.Name()), query) || - strings.Contains(strings.ToLower(i.Login()), query) -} - // DisplayName return a non-empty string to display, representing the // identity, based on the non-empty values. func (i *Identity) DisplayName() string { diff --git a/identity/identity_test.go b/identity/identity_test.go index 914126be..afb804fc 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -8,12 +8,13 @@ import ( "github.com/stretchr/testify/require" ) +// Test the commit and load of an Identity with multiple versions func TestIdentityCommitLoad(t *testing.T) { mockRepo := repository.NewMockRepoForTest() // single version - identity := Identity{ + identity := &Identity{ Versions: []*Version{ { Name: "René Descartes", @@ -30,11 +31,11 @@ func TestIdentityCommitLoad(t *testing.T) { loaded, err := Read(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) - equivalentIdentity(t, &identity, loaded) + equivalentIdentity(t, identity, loaded) // multiple version - identity = Identity{ + identity = &Identity{ Versions: []*Version{ { Time: 100, @@ -71,7 +72,7 @@ func TestIdentityCommitLoad(t *testing.T) { loaded, err = Read(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) - equivalentIdentity(t, &identity, loaded) + equivalentIdentity(t, identity, loaded) // add more version @@ -101,7 +102,7 @@ func TestIdentityCommitLoad(t *testing.T) { loaded, err = Read(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) - equivalentIdentity(t, &identity, loaded) + equivalentIdentity(t, identity, loaded) } func commitsAreSet(t *testing.T, identity *Identity) { @@ -120,6 +121,7 @@ func equivalentIdentity(t *testing.T, expected, actual *Identity) { assert.Equal(t, expected, actual) } +// Test that the correct crypto keys are returned for a given lamport time func TestIdentity_ValidKeysAtTime(t *testing.T) { identity := Identity{ Versions: []*Version{ @@ -176,6 +178,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) { assert.Equal(t, identity.ValidKeysAtTime(3000), []Key{{PubKey: "pubkeyE"}}) } +// Test the immutable or mutable metadata search func TestMetadata(t *testing.T) { mockRepo := repository.NewMockRepoForTest() diff --git a/identity/interface.go b/identity/interface.go index c784a7a6..1d534eb8 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -23,9 +23,6 @@ type Interface interface { // identity, based on the non-empty values. DisplayName() string - // Match tell is the Person match the given query string - Match(query string) bool - // Validate check if the Identity data is valid Validate() error diff --git a/identity/key.go b/identity/key.go index c498ec09..90edfb60 100644 --- a/identity/key.go +++ b/identity/key.go @@ -5,3 +5,9 @@ type Key struct { Fingerprint string `json:"fingerprint"` PubKey string `json:"pub_key"` } + +func (k *Key) Validate() error { + // Todo + + return nil +} diff --git a/identity/version.go b/identity/version.go index bc4561d9..d4afc893 100644 --- a/identity/version.go +++ b/identity/version.go @@ -10,34 +10,88 @@ import ( "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/util/lamport" "github.com/MichaelMure/git-bug/util/text" + "github.com/pkg/errors" ) +const formatVersion = 1 + // Version is a complete set of information about an Identity at a point in time. type Version struct { - // Private field so not serialized + // Not serialized commitHash git.Hash // The lamport time at which this version become effective // The reference time is the bug edition lamport clock - Time lamport.Time `json:"time"` + Time lamport.Time - Name string `json:"name"` - Email string `json:"email"` - Login string `json:"login"` - AvatarUrl string `json:"avatar_url"` + Name string + Email string + Login string + AvatarUrl string // The set of keys valid at that time, from this version onward, until they get removed // in a new version. This allow to have multiple key for the same identity (e.g. one per // device) as well as revoke key. - Keys []Key `json:"pub_keys"` + Keys []Key // This optional array is here to ensure a better randomness of the identity id to avoid collisions. // It has no functional purpose and should be ignored. // It is advised to fill this array if there is not enough entropy, e.g. if there is no keys. - Nonce []byte `json:"nonce,omitempty"` + Nonce []byte // A set of arbitrary key/value to store metadata about a version or about an Identity in general. - Metadata map[string]string `json:"metadata,omitempty"` + Metadata map[string]string +} + +type VersionJSON struct { + // Additional field to version the data + FormatVersion uint `json:"version"` + + Time lamport.Time `json:"time"` + Name string `json:"name"` + Email string `json:"email"` + Login string `json:"login"` + AvatarUrl string `json:"avatar_url"` + Keys []Key `json:"pub_keys"` + Nonce []byte `json:"nonce,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +func (v *Version) MarshalJSON() ([]byte, error) { + return json.Marshal(VersionJSON{ + FormatVersion: formatVersion, + Time: v.Time, + Name: v.Name, + Email: v.Email, + Login: v.Login, + AvatarUrl: v.AvatarUrl, + Keys: v.Keys, + Nonce: v.Nonce, + Metadata: v.Metadata, + }) +} + +func (v *Version) UnmarshalJSON(data []byte) error { + var aux VersionJSON + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if aux.FormatVersion != formatVersion { + return fmt.Errorf("unknown format version %v", aux.FormatVersion) + } + + v.Time = aux.Time + v.Name = aux.Name + v.Email = aux.Email + v.Login = aux.Login + v.AvatarUrl = aux.AvatarUrl + v.Keys = aux.Keys + v.Nonce = aux.Nonce + v.Metadata = aux.Metadata + + return nil } func (v *Version) Validate() error { @@ -77,12 +131,24 @@ func (v *Version) Validate() error { return fmt.Errorf("nonce is too big") } + for _, k := range v.Keys { + if err := k.Validate(); err != nil { + return errors.Wrap(err, "invalid key") + } + } + return nil } // Write will serialize and store the Version as a git blob and return // its hash func (v *Version) Write(repo repository.Repo) (git.Hash, error) { + // make sure we don't write invalid data + err := v.Validate() + if err != nil { + return "", errors.Wrap(err, "validation error") + } + data, err := json.Marshal(v) if err != nil { diff --git a/vendor/github.com/go-test/deep/.gitignore b/vendor/github.com/go-test/deep/.gitignore deleted file mode 100644 index 53f12f0f..00000000 --- a/vendor/github.com/go-test/deep/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.swp -*.out diff --git a/vendor/github.com/go-test/deep/.travis.yml b/vendor/github.com/go-test/deep/.travis.yml deleted file mode 100644 index 2279c614..00000000 --- a/vendor/github.com/go-test/deep/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: go - -go: - - 1.7 - - 1.8 - - 1.9 - -before_install: - - go get github.com/mattn/goveralls - - go get golang.org/x/tools/cover - -script: - - $HOME/gopath/bin/goveralls -service=travis-ci diff --git a/vendor/github.com/go-test/deep/CHANGES.md b/vendor/github.com/go-test/deep/CHANGES.md deleted file mode 100644 index 4351819d..00000000 --- a/vendor/github.com/go-test/deep/CHANGES.md +++ /dev/null @@ -1,9 +0,0 @@ -# go-test/deep Changelog - -## v1.0.1 released 2018-01-28 - -* Fixed #12: Arrays are not properly compared (samlitowitz) - -## v1.0.0 releaesd 2017-10-27 - -* First release diff --git a/vendor/github.com/go-test/deep/LICENSE b/vendor/github.com/go-test/deep/LICENSE deleted file mode 100644 index 228ef16f..00000000 --- a/vendor/github.com/go-test/deep/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright 2015-2017 Daniel Nichter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/go-test/deep/README.md b/vendor/github.com/go-test/deep/README.md deleted file mode 100644 index 3b78eac7..00000000 --- a/vendor/github.com/go-test/deep/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Deep Variable Equality for Humans - -[![Go Report Card](https://goreportcard.com/badge/github.com/go-test/deep)](https://goreportcard.com/report/github.com/go-test/deep) [![Build Status](https://travis-ci.org/go-test/deep.svg?branch=master)](https://travis-ci.org/go-test/deep) [![Coverage Status](https://coveralls.io/repos/github/go-test/deep/badge.svg?branch=master)](https://coveralls.io/github/go-test/deep?branch=master) [![GoDoc](https://godoc.org/github.com/go-test/deep?status.svg)](https://godoc.org/github.com/go-test/deep) - -This package provides a single function: `deep.Equal`. It's like [reflect.DeepEqual](http://golang.org/pkg/reflect/#DeepEqual) but much friendlier to humans (or any sentient being) for two reason: - -* `deep.Equal` returns a list of differences -* `deep.Equal` does not compare unexported fields (by default) - -`reflect.DeepEqual` is good (like all things Golang!), but it's a game of [Hunt the Wumpus](https://en.wikipedia.org/wiki/Hunt_the_Wumpus). For large maps, slices, and structs, finding the difference is difficult. - -`deep.Equal` doesn't play games with you, it lists the differences: - -```go -package main_test - -import ( - "testing" - "github.com/go-test/deep" -) - -type T struct { - Name string - Numbers []float64 -} - -func TestDeepEqual(t *testing.T) { - // Can you spot the difference? - t1 := T{ - Name: "Isabella", - Numbers: []float64{1.13459, 2.29343, 3.010100010}, - } - t2 := T{ - Name: "Isabella", - Numbers: []float64{1.13459, 2.29843, 3.010100010}, - } - - if diff := deep.Equal(t1, t2); diff != nil { - t.Error(diff) - } -} -``` - - -``` -$ go test ---- FAIL: TestDeepEqual (0.00s) - main_test.go:25: [Numbers.slice[1]: 2.29343 != 2.29843] -``` - -The difference is in `Numbers.slice[1]`: the two values aren't equal using Go `==`. diff --git a/vendor/github.com/go-test/deep/deep.go b/vendor/github.com/go-test/deep/deep.go deleted file mode 100644 index 4ea14cb0..00000000 --- a/vendor/github.com/go-test/deep/deep.go +++ /dev/null @@ -1,352 +0,0 @@ -// Package deep provides function deep.Equal which is like reflect.DeepEqual but -// returns a list of differences. This is helpful when comparing complex types -// like structures and maps. -package deep - -import ( - "errors" - "fmt" - "log" - "reflect" - "strings" -) - -var ( - // FloatPrecision is the number of decimal places to round float values - // to when comparing. - FloatPrecision = 10 - - // MaxDiff specifies the maximum number of differences to return. - MaxDiff = 10 - - // MaxDepth specifies the maximum levels of a struct to recurse into. - MaxDepth = 10 - - // LogErrors causes errors to be logged to STDERR when true. - LogErrors = false - - // CompareUnexportedFields causes unexported struct fields, like s in - // T{s int}, to be comparsed when true. - CompareUnexportedFields = false -) - -var ( - // ErrMaxRecursion is logged when MaxDepth is reached. - ErrMaxRecursion = errors.New("recursed to MaxDepth") - - // ErrTypeMismatch is logged when Equal passed two different types of values. - ErrTypeMismatch = errors.New("variables are different reflect.Type") - - // ErrNotHandled is logged when a primitive Go kind is not handled. - ErrNotHandled = errors.New("cannot compare the reflect.Kind") -) - -type cmp struct { - diff []string - buff []string - floatFormat string -} - -var errorType = reflect.TypeOf((*error)(nil)).Elem() - -// Equal compares variables a and b, recursing into their structure up to -// MaxDepth levels deep, and returns a list of differences, or nil if there are -// none. Some differences may not be found if an error is also returned. -// -// If a type has an Equal method, like time.Equal, it is called to check for -// equality. -func Equal(a, b interface{}) []string { - aVal := reflect.ValueOf(a) - bVal := reflect.ValueOf(b) - c := &cmp{ - diff: []string{}, - buff: []string{}, - floatFormat: fmt.Sprintf("%%.%df", FloatPrecision), - } - if a == nil && b == nil { - return nil - } else if a == nil && b != nil { - c.saveDiff(b, "") - } else if a != nil && b == nil { - c.saveDiff(a, "") - } - if len(c.diff) > 0 { - return c.diff - } - - c.equals(aVal, bVal, 0) - if len(c.diff) > 0 { - return c.diff // diffs - } - return nil // no diffs -} - -func (c *cmp) equals(a, b reflect.Value, level int) { - if level > MaxDepth { - logError(ErrMaxRecursion) - return - } - - // Check if one value is nil, e.g. T{x: *X} and T.x is nil - if !a.IsValid() || !b.IsValid() { - if a.IsValid() && !b.IsValid() { - c.saveDiff(a.Type(), "") - } else if !a.IsValid() && b.IsValid() { - c.saveDiff("", b.Type()) - } - return - } - - // If differenet types, they can't be equal - aType := a.Type() - bType := b.Type() - if aType != bType { - c.saveDiff(aType, bType) - logError(ErrTypeMismatch) - return - } - - // Primitive https://golang.org/pkg/reflect/#Kind - aKind := a.Kind() - bKind := b.Kind() - - // If both types implement the error interface, compare the error strings. - // This must be done before dereferencing because the interface is on a - // pointer receiver. - if aType.Implements(errorType) && bType.Implements(errorType) { - if a.Elem().IsValid() && b.Elem().IsValid() { // both err != nil - aString := a.MethodByName("Error").Call(nil)[0].String() - bString := b.MethodByName("Error").Call(nil)[0].String() - if aString != bString { - c.saveDiff(aString, bString) - } - return - } - } - - // Dereference pointers and interface{} - if aElem, bElem := (aKind == reflect.Ptr || aKind == reflect.Interface), - (bKind == reflect.Ptr || bKind == reflect.Interface); aElem || bElem { - - if aElem { - a = a.Elem() - } - - if bElem { - b = b.Elem() - } - - c.equals(a, b, level+1) - return - } - - // Types with an Equal(), like time.Time. - eqFunc := a.MethodByName("Equal") - if eqFunc.IsValid() { - retVals := eqFunc.Call([]reflect.Value{b}) - if !retVals[0].Bool() { - c.saveDiff(a, b) - } - return - } - - switch aKind { - - ///////////////////////////////////////////////////////////////////// - // Iterable kinds - ///////////////////////////////////////////////////////////////////// - - case reflect.Struct: - /* - The variables are structs like: - type T struct { - FirstName string - LastName string - } - Type = .T, Kind = reflect.Struct - - Iterate through the fields (FirstName, LastName), recurse into their values. - */ - for i := 0; i < a.NumField(); i++ { - if aType.Field(i).PkgPath != "" && !CompareUnexportedFields { - continue // skip unexported field, e.g. s in type T struct {s string} - } - - c.push(aType.Field(i).Name) // push field name to buff - - // Get the Value for each field, e.g. FirstName has Type = string, - // Kind = reflect.String. - af := a.Field(i) - bf := b.Field(i) - - // Recurse to compare the field values - c.equals(af, bf, level+1) - - c.pop() // pop field name from buff - - if len(c.diff) >= MaxDiff { - break - } - } - case reflect.Map: - /* - The variables are maps like: - map[string]int{ - "foo": 1, - "bar": 2, - } - Type = map[string]int, Kind = reflect.Map - - Or: - type T map[string]int{} - Type = .T, Kind = reflect.Map - - Iterate through the map keys (foo, bar), recurse into their values. - */ - - if a.IsNil() || b.IsNil() { - if a.IsNil() && !b.IsNil() { - c.saveDiff("", b) - } else if !a.IsNil() && b.IsNil() { - c.saveDiff(a, "") - } - return - } - - if a.Pointer() == b.Pointer() { - return - } - - for _, key := range a.MapKeys() { - c.push(fmt.Sprintf("map[%s]", key)) - - aVal := a.MapIndex(key) - bVal := b.MapIndex(key) - if bVal.IsValid() { - c.equals(aVal, bVal, level+1) - } else { - c.saveDiff(aVal, "") - } - - c.pop() - - if len(c.diff) >= MaxDiff { - return - } - } - - for _, key := range b.MapKeys() { - if aVal := a.MapIndex(key); aVal.IsValid() { - continue - } - - c.push(fmt.Sprintf("map[%s]", key)) - c.saveDiff("", b.MapIndex(key)) - c.pop() - if len(c.diff) >= MaxDiff { - return - } - } - case reflect.Array: - n := a.Len() - for i := 0; i < n; i++ { - c.push(fmt.Sprintf("array[%d]", i)) - c.equals(a.Index(i), b.Index(i), level+1) - c.pop() - if len(c.diff) >= MaxDiff { - break - } - } - case reflect.Slice: - if a.IsNil() || b.IsNil() { - if a.IsNil() && !b.IsNil() { - c.saveDiff("", b) - } else if !a.IsNil() && b.IsNil() { - c.saveDiff(a, "") - } - return - } - - if a.Pointer() == b.Pointer() { - return - } - - aLen := a.Len() - bLen := b.Len() - n := aLen - if bLen > aLen { - n = bLen - } - for i := 0; i < n; i++ { - c.push(fmt.Sprintf("slice[%d]", i)) - if i < aLen && i < bLen { - c.equals(a.Index(i), b.Index(i), level+1) - } else if i < aLen { - c.saveDiff(a.Index(i), "") - } else { - c.saveDiff("", b.Index(i)) - } - c.pop() - if len(c.diff) >= MaxDiff { - break - } - } - - ///////////////////////////////////////////////////////////////////// - // Primitive kinds - ///////////////////////////////////////////////////////////////////// - - case reflect.Float32, reflect.Float64: - // Avoid 0.04147685731961082 != 0.041476857319611 - // 6 decimal places is close enough - aval := fmt.Sprintf(c.floatFormat, a.Float()) - bval := fmt.Sprintf(c.floatFormat, b.Float()) - if aval != bval { - c.saveDiff(a.Float(), b.Float()) - } - case reflect.Bool: - if a.Bool() != b.Bool() { - c.saveDiff(a.Bool(), b.Bool()) - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if a.Int() != b.Int() { - c.saveDiff(a.Int(), b.Int()) - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if a.Uint() != b.Uint() { - c.saveDiff(a.Uint(), b.Uint()) - } - case reflect.String: - if a.String() != b.String() { - c.saveDiff(a.String(), b.String()) - } - - default: - logError(ErrNotHandled) - } -} - -func (c *cmp) push(name string) { - c.buff = append(c.buff, name) -} - -func (c *cmp) pop() { - if len(c.buff) > 0 { - c.buff = c.buff[0 : len(c.buff)-1] - } -} - -func (c *cmp) saveDiff(aval, bval interface{}) { - if len(c.buff) > 0 { - varName := strings.Join(c.buff, ".") - c.diff = append(c.diff, fmt.Sprintf("%s: %v != %v", varName, aval, bval)) - } else { - c.diff = append(c.diff, fmt.Sprintf("%v != %v", aval, bval)) - } -} - -func logError(err error) { - if LogErrors { - log.Println(err) - } -} diff --git a/vendor/github.com/google/go-cmp/LICENSE b/vendor/github.com/google/go-cmp/LICENSE deleted file mode 100644 index 32017f8f..00000000 --- a/vendor/github.com/google/go-cmp/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2017 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/go-cmp/cmp/compare.go b/vendor/github.com/google/go-cmp/cmp/compare.go deleted file mode 100644 index 7e215f22..00000000 --- a/vendor/github.com/google/go-cmp/cmp/compare.go +++ /dev/null @@ -1,553 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// Package cmp determines equality of values. -// -// This package is intended to be a more powerful and safer alternative to -// reflect.DeepEqual for comparing whether two values are semantically equal. -// -// The primary features of cmp are: -// -// • When the default behavior of equality does not suit the needs of the test, -// custom equality functions can override the equality operation. -// For example, an equality function may report floats as equal so long as they -// are within some tolerance of each other. -// -// • Types that have an Equal method may use that method to determine equality. -// This allows package authors to determine the equality operation for the types -// that they define. -// -// • If no custom equality functions are used and no Equal method is defined, -// equality is determined by recursively comparing the primitive kinds on both -// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported -// fields are not compared by default; they result in panics unless suppressed -// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared -// using the AllowUnexported option. -package cmp - -import ( - "fmt" - "reflect" - - "github.com/google/go-cmp/cmp/internal/diff" - "github.com/google/go-cmp/cmp/internal/function" - "github.com/google/go-cmp/cmp/internal/value" -) - -// BUG(dsnet): Maps with keys containing NaN values cannot be properly compared due to -// the reflection package's inability to retrieve such entries. Equal will panic -// anytime it comes across a NaN key, but this behavior may change. -// -// See https://golang.org/issue/11104 for more details. - -var nothing = reflect.Value{} - -// Equal reports whether x and y are equal by recursively applying the -// following rules in the given order to x and y and all of their sub-values: -// -// • If two values are not of the same type, then they are never equal -// and the overall result is false. -// -// • Let S be the set of all Ignore, Transformer, and Comparer options that -// remain after applying all path filters, value filters, and type filters. -// If at least one Ignore exists in S, then the comparison is ignored. -// If the number of Transformer and Comparer options in S is greater than one, -// then Equal panics because it is ambiguous which option to use. -// If S contains a single Transformer, then use that to transform the current -// values and recursively call Equal on the output values. -// If S contains a single Comparer, then use that to compare the current values. -// Otherwise, evaluation proceeds to the next rule. -// -// • If the values have an Equal method of the form "(T) Equal(T) bool" or -// "(T) Equal(I) bool" where T is assignable to I, then use the result of -// x.Equal(y) even if x or y is nil. -// Otherwise, no such method exists and evaluation proceeds to the next rule. -// -// • Lastly, try to compare x and y based on their basic kinds. -// Simple kinds like booleans, integers, floats, complex numbers, strings, and -// channels are compared using the equivalent of the == operator in Go. -// Functions are only equal if they are both nil, otherwise they are unequal. -// Pointers are equal if the underlying values they point to are also equal. -// Interfaces are equal if their underlying concrete values are also equal. -// -// Structs are equal if all of their fields are equal. If a struct contains -// unexported fields, Equal panics unless the AllowUnexported option is used or -// an Ignore option (e.g., cmpopts.IgnoreUnexported) ignores that field. -// -// Arrays, slices, and maps are equal if they are both nil or both non-nil -// with the same length and the elements at each index or key are equal. -// Note that a non-nil empty slice and a nil slice are not equal. -// To equate empty slices and maps, consider using cmpopts.EquateEmpty. -// Map keys are equal according to the == operator. -// To use custom comparisons for map keys, consider using cmpopts.SortMaps. -func Equal(x, y interface{}, opts ...Option) bool { - s := newState(opts) - s.compareAny(reflect.ValueOf(x), reflect.ValueOf(y)) - return s.result.Equal() -} - -// Diff returns a human-readable report of the differences between two values. -// It returns an empty string if and only if Equal returns true for the same -// input values and options. The output string will use the "-" symbol to -// indicate elements removed from x, and the "+" symbol to indicate elements -// added to y. -// -// Do not depend on this output being stable. -func Diff(x, y interface{}, opts ...Option) string { - r := new(defaultReporter) - opts = Options{Options(opts), r} - eq := Equal(x, y, opts...) - d := r.String() - if (d == "") != eq { - panic("inconsistent difference and equality results") - } - return d -} - -type state struct { - // These fields represent the "comparison state". - // Calling statelessCompare must not result in observable changes to these. - result diff.Result // The current result of comparison - curPath Path // The current path in the value tree - reporter reporter // Optional reporter used for difference formatting - - // dynChecker triggers pseudo-random checks for option correctness. - // It is safe for statelessCompare to mutate this value. - dynChecker dynChecker - - // These fields, once set by processOption, will not change. - exporters map[reflect.Type]bool // Set of structs with unexported field visibility - opts Options // List of all fundamental and filter options -} - -func newState(opts []Option) *state { - s := new(state) - for _, opt := range opts { - s.processOption(opt) - } - return s -} - -func (s *state) processOption(opt Option) { - switch opt := opt.(type) { - case nil: - case Options: - for _, o := range opt { - s.processOption(o) - } - case coreOption: - type filtered interface { - isFiltered() bool - } - if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() { - panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt)) - } - s.opts = append(s.opts, opt) - case visibleStructs: - if s.exporters == nil { - s.exporters = make(map[reflect.Type]bool) - } - for t := range opt { - s.exporters[t] = true - } - case reporter: - if s.reporter != nil { - panic("difference reporter already registered") - } - s.reporter = opt - default: - panic(fmt.Sprintf("unknown option %T", opt)) - } -} - -// statelessCompare compares two values and returns the result. -// This function is stateless in that it does not alter the current result, -// or output to any registered reporters. -func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result { - // We do not save and restore the curPath because all of the compareX - // methods should properly push and pop from the path. - // It is an implementation bug if the contents of curPath differs from - // when calling this function to when returning from it. - - oldResult, oldReporter := s.result, s.reporter - s.result = diff.Result{} // Reset result - s.reporter = nil // Remove reporter to avoid spurious printouts - s.compareAny(vx, vy) - res := s.result - s.result, s.reporter = oldResult, oldReporter - return res -} - -func (s *state) compareAny(vx, vy reflect.Value) { - // TODO: Support cyclic data structures. - - // Rule 0: Differing types are never equal. - if !vx.IsValid() || !vy.IsValid() { - s.report(vx.IsValid() == vy.IsValid(), vx, vy) - return - } - if vx.Type() != vy.Type() { - s.report(false, vx, vy) // Possible for path to be empty - return - } - t := vx.Type() - if len(s.curPath) == 0 { - s.curPath.push(&pathStep{typ: t}) - defer s.curPath.pop() - } - vx, vy = s.tryExporting(vx, vy) - - // Rule 1: Check whether an option applies on this node in the value tree. - if s.tryOptions(vx, vy, t) { - return - } - - // Rule 2: Check whether the type has a valid Equal method. - if s.tryMethod(vx, vy, t) { - return - } - - // Rule 3: Recursively descend into each value's underlying kind. - switch t.Kind() { - case reflect.Bool: - s.report(vx.Bool() == vy.Bool(), vx, vy) - return - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - s.report(vx.Int() == vy.Int(), vx, vy) - return - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - s.report(vx.Uint() == vy.Uint(), vx, vy) - return - case reflect.Float32, reflect.Float64: - s.report(vx.Float() == vy.Float(), vx, vy) - return - case reflect.Complex64, reflect.Complex128: - s.report(vx.Complex() == vy.Complex(), vx, vy) - return - case reflect.String: - s.report(vx.String() == vy.String(), vx, vy) - return - case reflect.Chan, reflect.UnsafePointer: - s.report(vx.Pointer() == vy.Pointer(), vx, vy) - return - case reflect.Func: - s.report(vx.IsNil() && vy.IsNil(), vx, vy) - return - case reflect.Ptr: - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) - return - } - s.curPath.push(&indirect{pathStep{t.Elem()}}) - defer s.curPath.pop() - s.compareAny(vx.Elem(), vy.Elem()) - return - case reflect.Interface: - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) - return - } - if vx.Elem().Type() != vy.Elem().Type() { - s.report(false, vx.Elem(), vy.Elem()) - return - } - s.curPath.push(&typeAssertion{pathStep{vx.Elem().Type()}}) - defer s.curPath.pop() - s.compareAny(vx.Elem(), vy.Elem()) - return - case reflect.Slice: - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) - return - } - fallthrough - case reflect.Array: - s.compareArray(vx, vy, t) - return - case reflect.Map: - s.compareMap(vx, vy, t) - return - case reflect.Struct: - s.compareStruct(vx, vy, t) - return - default: - panic(fmt.Sprintf("%v kind not handled", t.Kind())) - } -} - -func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) { - if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported { - if sf.force { - // Use unsafe pointer arithmetic to get read-write access to an - // unexported field in the struct. - vx = unsafeRetrieveField(sf.pvx, sf.field) - vy = unsafeRetrieveField(sf.pvy, sf.field) - } else { - // We are not allowed to export the value, so invalidate them - // so that tryOptions can panic later if not explicitly ignored. - vx = nothing - vy = nothing - } - } - return vx, vy -} - -func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool { - // If there were no FilterValues, we will not detect invalid inputs, - // so manually check for them and append invalid if necessary. - // We still evaluate the options since an ignore can override invalid. - opts := s.opts - if !vx.IsValid() || !vy.IsValid() { - opts = Options{opts, invalid{}} - } - - // Evaluate all filters and apply the remaining options. - if opt := opts.filter(s, vx, vy, t); opt != nil { - opt.apply(s, vx, vy) - return true - } - return false -} - -func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool { - // Check if this type even has an Equal method. - m, ok := t.MethodByName("Equal") - if !ok || !function.IsType(m.Type, function.EqualAssignable) { - return false - } - - eq := s.callTTBFunc(m.Func, vx, vy) - s.report(eq, vx, vy) - return true -} - -func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { - v = sanitizeValue(v, f.Type().In(0)) - if !s.dynChecker.Next() { - return f.Call([]reflect.Value{v})[0] - } - - // Run the function twice and ensure that we get the same results back. - // We run in goroutines so that the race detector (if enabled) can detect - // unsafe mutations to the input. - c := make(chan reflect.Value) - go detectRaces(c, f, v) - want := f.Call([]reflect.Value{v})[0] - if got := <-c; !s.statelessCompare(got, want).Equal() { - // To avoid false-positives with non-reflexive equality operations, - // we sanity check whether a value is equal to itself. - if !s.statelessCompare(want, want).Equal() { - return want - } - fn := getFuncName(f.Pointer()) - panic(fmt.Sprintf("non-deterministic function detected: %s", fn)) - } - return want -} - -func (s *state) callTTBFunc(f, x, y reflect.Value) bool { - x = sanitizeValue(x, f.Type().In(0)) - y = sanitizeValue(y, f.Type().In(1)) - if !s.dynChecker.Next() { - return f.Call([]reflect.Value{x, y})[0].Bool() - } - - // Swapping the input arguments is sufficient to check that - // f is symmetric and deterministic. - // We run in goroutines so that the race detector (if enabled) can detect - // unsafe mutations to the input. - c := make(chan reflect.Value) - go detectRaces(c, f, y, x) - want := f.Call([]reflect.Value{x, y})[0].Bool() - if got := <-c; !got.IsValid() || got.Bool() != want { - fn := getFuncName(f.Pointer()) - panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", fn)) - } - return want -} - -func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { - var ret reflect.Value - defer func() { - recover() // Ignore panics, let the other call to f panic instead - c <- ret - }() - ret = f.Call(vs)[0] -} - -// sanitizeValue converts nil interfaces of type T to those of type R, -// assuming that T is assignable to R. -// Otherwise, it returns the input value as is. -func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { - // TODO(dsnet): Remove this hacky workaround. - // See https://golang.org/issue/22143 - if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t { - return reflect.New(t).Elem() - } - return v -} - -func (s *state) compareArray(vx, vy reflect.Value, t reflect.Type) { - step := &sliceIndex{pathStep{t.Elem()}, 0, 0} - s.curPath.push(step) - - // Compute an edit-script for slices vx and vy. - es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { - step.xkey, step.ykey = ix, iy - return s.statelessCompare(vx.Index(ix), vy.Index(iy)) - }) - - // Report the entire slice as is if the arrays are of primitive kind, - // and the arrays are different enough. - isPrimitive := false - switch t.Elem().Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, - reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: - isPrimitive = true - } - if isPrimitive && es.Dist() > (vx.Len()+vy.Len())/4 { - s.curPath.pop() // Pop first since we are reporting the whole slice - s.report(false, vx, vy) - return - } - - // Replay the edit-script. - var ix, iy int - for _, e := range es { - switch e { - case diff.UniqueX: - step.xkey, step.ykey = ix, -1 - s.report(false, vx.Index(ix), nothing) - ix++ - case diff.UniqueY: - step.xkey, step.ykey = -1, iy - s.report(false, nothing, vy.Index(iy)) - iy++ - default: - step.xkey, step.ykey = ix, iy - if e == diff.Identity { - s.report(true, vx.Index(ix), vy.Index(iy)) - } else { - s.compareAny(vx.Index(ix), vy.Index(iy)) - } - ix++ - iy++ - } - } - s.curPath.pop() - return -} - -func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) - return - } - - // We combine and sort the two map keys so that we can perform the - // comparisons in a deterministic order. - step := &mapIndex{pathStep: pathStep{t.Elem()}} - s.curPath.push(step) - defer s.curPath.pop() - for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { - step.key = k - vvx := vx.MapIndex(k) - vvy := vy.MapIndex(k) - switch { - case vvx.IsValid() && vvy.IsValid(): - s.compareAny(vvx, vvy) - case vvx.IsValid() && !vvy.IsValid(): - s.report(false, vvx, nothing) - case !vvx.IsValid() && vvy.IsValid(): - s.report(false, nothing, vvy) - default: - // It is possible for both vvx and vvy to be invalid if the - // key contained a NaN value in it. There is no way in - // reflection to be able to retrieve these values. - // See https://golang.org/issue/11104 - panic(fmt.Sprintf("%#v has map key with NaNs", s.curPath)) - } - } -} - -func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) { - var vax, vay reflect.Value // Addressable versions of vx and vy - - step := &structField{} - s.curPath.push(step) - defer s.curPath.pop() - for i := 0; i < t.NumField(); i++ { - vvx := vx.Field(i) - vvy := vy.Field(i) - step.typ = t.Field(i).Type - step.name = t.Field(i).Name - step.idx = i - step.unexported = !isExported(step.name) - if step.unexported { - // Defer checking of unexported fields until later to give an - // Ignore a chance to ignore the field. - if !vax.IsValid() || !vay.IsValid() { - // For unsafeRetrieveField to work, the parent struct must - // be addressable. Create a new copy of the values if - // necessary to make them addressable. - vax = makeAddressable(vx) - vay = makeAddressable(vy) - } - step.force = s.exporters[t] - step.pvx = vax - step.pvy = vay - step.field = t.Field(i) - } - s.compareAny(vvx, vvy) - } -} - -// report records the result of a single comparison. -// It also calls Report if any reporter is registered. -func (s *state) report(eq bool, vx, vy reflect.Value) { - if eq { - s.result.NSame++ - } else { - s.result.NDiff++ - } - if s.reporter != nil { - s.reporter.Report(vx, vy, eq, s.curPath) - } -} - -// dynChecker tracks the state needed to periodically perform checks that -// user provided functions are symmetric and deterministic. -// The zero value is safe for immediate use. -type dynChecker struct{ curr, next int } - -// Next increments the state and reports whether a check should be performed. -// -// Checks occur every Nth function call, where N is a triangular number: -// 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ... -// See https://en.wikipedia.org/wiki/Triangular_number -// -// This sequence ensures that the cost of checks drops significantly as -// the number of functions calls grows larger. -func (dc *dynChecker) Next() bool { - ok := dc.curr == dc.next - if ok { - dc.curr = 0 - dc.next++ - } - dc.curr++ - return ok -} - -// makeAddressable returns a value that is always addressable. -// It returns the input verbatim if it is already addressable, -// otherwise it creates a new value and returns an addressable copy. -func makeAddressable(v reflect.Value) reflect.Value { - if v.CanAddr() { - return v - } - vc := reflect.New(v.Type()).Elem() - vc.Set(v) - return vc -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go deleted file mode 100644 index 42afa496..00000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// +build !debug - -package diff - -var debug debugger - -type debugger struct{} - -func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc { - return f -} -func (debugger) Update() {} -func (debugger) Finish() {} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go deleted file mode 100644 index fd9f7f17..00000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// +build debug - -package diff - -import ( - "fmt" - "strings" - "sync" - "time" -) - -// The algorithm can be seen running in real-time by enabling debugging: -// go test -tags=debug -v -// -// Example output: -// === RUN TestDifference/#34 -// ┌───────────────────────────────┐ -// │ \ · · · · · · · · · · · · · · │ -// │ · # · · · · · · · · · · · · · │ -// │ · \ · · · · · · · · · · · · · │ -// │ · · \ · · · · · · · · · · · · │ -// │ · · · X # · · · · · · · · · · │ -// │ · · · # \ · · · · · · · · · · │ -// │ · · · · · # # · · · · · · · · │ -// │ · · · · · # \ · · · · · · · · │ -// │ · · · · · · · \ · · · · · · · │ -// │ · · · · · · · · \ · · · · · · │ -// │ · · · · · · · · · \ · · · · · │ -// │ · · · · · · · · · · \ · · # · │ -// │ · · · · · · · · · · · \ # # · │ -// │ · · · · · · · · · · · # # # · │ -// │ · · · · · · · · · · # # # # · │ -// │ · · · · · · · · · # # # # # · │ -// │ · · · · · · · · · · · · · · \ │ -// └───────────────────────────────┘ -// [.Y..M.XY......YXYXY.|] -// -// The grid represents the edit-graph where the horizontal axis represents -// list X and the vertical axis represents list Y. The start of the two lists -// is the top-left, while the ends are the bottom-right. The '·' represents -// an unexplored node in the graph. The '\' indicates that the two symbols -// from list X and Y are equal. The 'X' indicates that two symbols are similar -// (but not exactly equal) to each other. The '#' indicates that the two symbols -// are different (and not similar). The algorithm traverses this graph trying to -// make the paths starting in the top-left and the bottom-right connect. -// -// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents -// the currently established path from the forward and reverse searches, -// separated by a '|' character. - -const ( - updateDelay = 100 * time.Millisecond - finishDelay = 500 * time.Millisecond - ansiTerminal = true // ANSI escape codes used to move terminal cursor -) - -var debug debugger - -type debugger struct { - sync.Mutex - p1, p2 EditScript - fwdPath, revPath *EditScript - grid []byte - lines int -} - -func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc { - dbg.Lock() - dbg.fwdPath, dbg.revPath = p1, p2 - top := "┌─" + strings.Repeat("──", nx) + "┐\n" - row := "│ " + strings.Repeat("· ", nx) + "│\n" - btm := "└─" + strings.Repeat("──", nx) + "┘\n" - dbg.grid = []byte(top + strings.Repeat(row, ny) + btm) - dbg.lines = strings.Count(dbg.String(), "\n") - fmt.Print(dbg) - - // Wrap the EqualFunc so that we can intercept each result. - return func(ix, iy int) (r Result) { - cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")] - for i := range cell { - cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot - } - switch r = f(ix, iy); { - case r.Equal(): - cell[0] = '\\' - case r.Similar(): - cell[0] = 'X' - default: - cell[0] = '#' - } - return - } -} - -func (dbg *debugger) Update() { - dbg.print(updateDelay) -} - -func (dbg *debugger) Finish() { - dbg.print(finishDelay) - dbg.Unlock() -} - -func (dbg *debugger) String() string { - dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0] - for i := len(*dbg.revPath) - 1; i >= 0; i-- { - dbg.p2 = append(dbg.p2, (*dbg.revPath)[i]) - } - return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2) -} - -func (dbg *debugger) print(d time.Duration) { - if ansiTerminal { - fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor - } - fmt.Print(dbg) - time.Sleep(d) -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go deleted file mode 100644 index 260befea..00000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go +++ /dev/null @@ -1,363 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// Package diff implements an algorithm for producing edit-scripts. -// The edit-script is a sequence of operations needed to transform one list -// of symbols into another (or vice-versa). The edits allowed are insertions, -// deletions, and modifications. The summation of all edits is called the -// Levenshtein distance as this problem is well-known in computer science. -// -// This package prioritizes performance over accuracy. That is, the run time -// is more important than obtaining a minimal Levenshtein distance. -package diff - -// EditType represents a single operation within an edit-script. -type EditType uint8 - -const ( - // Identity indicates that a symbol pair is identical in both list X and Y. - Identity EditType = iota - // UniqueX indicates that a symbol only exists in X and not Y. - UniqueX - // UniqueY indicates that a symbol only exists in Y and not X. - UniqueY - // Modified indicates that a symbol pair is a modification of each other. - Modified -) - -// EditScript represents the series of differences between two lists. -type EditScript []EditType - -// String returns a human-readable string representing the edit-script where -// Identity, UniqueX, UniqueY, and Modified are represented by the -// '.', 'X', 'Y', and 'M' characters, respectively. -func (es EditScript) String() string { - b := make([]byte, len(es)) - for i, e := range es { - switch e { - case Identity: - b[i] = '.' - case UniqueX: - b[i] = 'X' - case UniqueY: - b[i] = 'Y' - case Modified: - b[i] = 'M' - default: - panic("invalid edit-type") - } - } - return string(b) -} - -// stats returns a histogram of the number of each type of edit operation. -func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) { - for _, e := range es { - switch e { - case Identity: - s.NI++ - case UniqueX: - s.NX++ - case UniqueY: - s.NY++ - case Modified: - s.NM++ - default: - panic("invalid edit-type") - } - } - return -} - -// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if -// lists X and Y are equal. -func (es EditScript) Dist() int { return len(es) - es.stats().NI } - -// LenX is the length of the X list. -func (es EditScript) LenX() int { return len(es) - es.stats().NY } - -// LenY is the length of the Y list. -func (es EditScript) LenY() int { return len(es) - es.stats().NX } - -// EqualFunc reports whether the symbols at indexes ix and iy are equal. -// When called by Difference, the index is guaranteed to be within nx and ny. -type EqualFunc func(ix int, iy int) Result - -// Result is the result of comparison. -// NSame is the number of sub-elements that are equal. -// NDiff is the number of sub-elements that are not equal. -type Result struct{ NSame, NDiff int } - -// Equal indicates whether the symbols are equal. Two symbols are equal -// if and only if NDiff == 0. If Equal, then they are also Similar. -func (r Result) Equal() bool { return r.NDiff == 0 } - -// Similar indicates whether two symbols are similar and may be represented -// by using the Modified type. As a special case, we consider binary comparisons -// (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar. -// -// The exact ratio of NSame to NDiff to determine similarity may change. -func (r Result) Similar() bool { - // Use NSame+1 to offset NSame so that binary comparisons are similar. - return r.NSame+1 >= r.NDiff -} - -// Difference reports whether two lists of lengths nx and ny are equal -// given the definition of equality provided as f. -// -// This function returns an edit-script, which is a sequence of operations -// needed to convert one list into the other. The following invariants for -// the edit-script are maintained: -// • eq == (es.Dist()==0) -// • nx == es.LenX() -// • ny == es.LenY() -// -// This algorithm is not guaranteed to be an optimal solution (i.e., one that -// produces an edit-script with a minimal Levenshtein distance). This algorithm -// favors performance over optimality. The exact output is not guaranteed to -// be stable and may change over time. -func Difference(nx, ny int, f EqualFunc) (es EditScript) { - // This algorithm is based on traversing what is known as an "edit-graph". - // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" - // by Eugene W. Myers. Since D can be as large as N itself, this is - // effectively O(N^2). Unlike the algorithm from that paper, we are not - // interested in the optimal path, but at least some "decent" path. - // - // For example, let X and Y be lists of symbols: - // X = [A B C A B B A] - // Y = [C B A B A C] - // - // The edit-graph can be drawn as the following: - // A B C A B B A - // ┌─────────────┐ - // C │_|_|\|_|_|_|_│ 0 - // B │_|\|_|_|\|\|_│ 1 - // A │\|_|_|\|_|_|\│ 2 - // B │_|\|_|_|\|\|_│ 3 - // A │\|_|_|\|_|_|\│ 4 - // C │ | |\| | | | │ 5 - // └─────────────┘ 6 - // 0 1 2 3 4 5 6 7 - // - // List X is written along the horizontal axis, while list Y is written - // along the vertical axis. At any point on this grid, if the symbol in - // list X matches the corresponding symbol in list Y, then a '\' is drawn. - // The goal of any minimal edit-script algorithm is to find a path from the - // top-left corner to the bottom-right corner, while traveling through the - // fewest horizontal or vertical edges. - // A horizontal edge is equivalent to inserting a symbol from list X. - // A vertical edge is equivalent to inserting a symbol from list Y. - // A diagonal edge is equivalent to a matching symbol between both X and Y. - - // Invariants: - // • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx - // • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny - // - // In general: - // • fwdFrontier.X < revFrontier.X - // • fwdFrontier.Y < revFrontier.Y - // Unless, it is time for the algorithm to terminate. - fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)} - revPath := path{-1, point{nx, ny}, make(EditScript, 0)} - fwdFrontier := fwdPath.point // Forward search frontier - revFrontier := revPath.point // Reverse search frontier - - // Search budget bounds the cost of searching for better paths. - // The longest sequence of non-matching symbols that can be tolerated is - // approximately the square-root of the search budget. - searchBudget := 4 * (nx + ny) // O(n) - - // The algorithm below is a greedy, meet-in-the-middle algorithm for - // computing sub-optimal edit-scripts between two lists. - // - // The algorithm is approximately as follows: - // • Searching for differences switches back-and-forth between - // a search that starts at the beginning (the top-left corner), and - // a search that starts at the end (the bottom-right corner). The goal of - // the search is connect with the search from the opposite corner. - // • As we search, we build a path in a greedy manner, where the first - // match seen is added to the path (this is sub-optimal, but provides a - // decent result in practice). When matches are found, we try the next pair - // of symbols in the lists and follow all matches as far as possible. - // • When searching for matches, we search along a diagonal going through - // through the "frontier" point. If no matches are found, we advance the - // frontier towards the opposite corner. - // • This algorithm terminates when either the X coordinates or the - // Y coordinates of the forward and reverse frontier points ever intersect. - // - // This algorithm is correct even if searching only in the forward direction - // or in the reverse direction. We do both because it is commonly observed - // that two lists commonly differ because elements were added to the front - // or end of the other list. - // - // Running the tests with the "debug" build tag prints a visualization of - // the algorithm running in real-time. This is educational for understanding - // how the algorithm works. See debug_enable.go. - f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) - for { - // Forward search from the beginning. - if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { - break - } - for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { - // Search in a diagonal pattern for a match. - z := zigzag(i) - p := point{fwdFrontier.X + z, fwdFrontier.Y - z} - switch { - case p.X >= revPath.X || p.Y < fwdPath.Y: - stop1 = true // Hit top-right corner - case p.Y >= revPath.Y || p.X < fwdPath.X: - stop2 = true // Hit bottom-left corner - case f(p.X, p.Y).Equal(): - // Match found, so connect the path to this point. - fwdPath.connect(p, f) - fwdPath.append(Identity) - // Follow sequence of matches as far as possible. - for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { - if !f(fwdPath.X, fwdPath.Y).Equal() { - break - } - fwdPath.append(Identity) - } - fwdFrontier = fwdPath.point - stop1, stop2 = true, true - default: - searchBudget-- // Match not found - } - debug.Update() - } - // Advance the frontier towards reverse point. - if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y { - fwdFrontier.X++ - } else { - fwdFrontier.Y++ - } - - // Reverse search from the end. - if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { - break - } - for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { - // Search in a diagonal pattern for a match. - z := zigzag(i) - p := point{revFrontier.X - z, revFrontier.Y + z} - switch { - case fwdPath.X >= p.X || revPath.Y < p.Y: - stop1 = true // Hit bottom-left corner - case fwdPath.Y >= p.Y || revPath.X < p.X: - stop2 = true // Hit top-right corner - case f(p.X-1, p.Y-1).Equal(): - // Match found, so connect the path to this point. - revPath.connect(p, f) - revPath.append(Identity) - // Follow sequence of matches as far as possible. - for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { - if !f(revPath.X-1, revPath.Y-1).Equal() { - break - } - revPath.append(Identity) - } - revFrontier = revPath.point - stop1, stop2 = true, true - default: - searchBudget-- // Match not found - } - debug.Update() - } - // Advance the frontier towards forward point. - if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y { - revFrontier.X-- - } else { - revFrontier.Y-- - } - } - - // Join the forward and reverse paths and then append the reverse path. - fwdPath.connect(revPath.point, f) - for i := len(revPath.es) - 1; i >= 0; i-- { - t := revPath.es[i] - revPath.es = revPath.es[:i] - fwdPath.append(t) - } - debug.Finish() - return fwdPath.es -} - -type path struct { - dir int // +1 if forward, -1 if reverse - point // Leading point of the EditScript path - es EditScript -} - -// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types -// to the edit-script to connect p.point to dst. -func (p *path) connect(dst point, f EqualFunc) { - if p.dir > 0 { - // Connect in forward direction. - for dst.X > p.X && dst.Y > p.Y { - switch r := f(p.X, p.Y); { - case r.Equal(): - p.append(Identity) - case r.Similar(): - p.append(Modified) - case dst.X-p.X >= dst.Y-p.Y: - p.append(UniqueX) - default: - p.append(UniqueY) - } - } - for dst.X > p.X { - p.append(UniqueX) - } - for dst.Y > p.Y { - p.append(UniqueY) - } - } else { - // Connect in reverse direction. - for p.X > dst.X && p.Y > dst.Y { - switch r := f(p.X-1, p.Y-1); { - case r.Equal(): - p.append(Identity) - case r.Similar(): - p.append(Modified) - case p.Y-dst.Y >= p.X-dst.X: - p.append(UniqueY) - default: - p.append(UniqueX) - } - } - for p.X > dst.X { - p.append(UniqueX) - } - for p.Y > dst.Y { - p.append(UniqueY) - } - } -} - -func (p *path) append(t EditType) { - p.es = append(p.es, t) - switch t { - case Identity, Modified: - p.add(p.dir, p.dir) - case UniqueX: - p.add(p.dir, 0) - case UniqueY: - p.add(0, p.dir) - } - debug.Update() -} - -type point struct{ X, Y int } - -func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy } - -// zigzag maps a consecutive sequence of integers to a zig-zag sequence. -// [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...] -func zigzag(x int) int { - if x&1 != 0 { - x = ^x - } - return x >> 1 -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/function/func.go b/vendor/github.com/google/go-cmp/cmp/internal/function/func.go deleted file mode 100644 index 4c35ff11..00000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/function/func.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// Package function identifies function types. -package function - -import "reflect" - -type funcType int - -const ( - _ funcType = iota - - ttbFunc // func(T, T) bool - tibFunc // func(T, I) bool - trFunc // func(T) R - - Equal = ttbFunc // func(T, T) bool - EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool - Transformer = trFunc // func(T) R - ValueFilter = ttbFunc // func(T, T) bool - Less = ttbFunc // func(T, T) bool -) - -var boolType = reflect.TypeOf(true) - -// IsType reports whether the reflect.Type is of the specified function type. -func IsType(t reflect.Type, ft funcType) bool { - if t == nil || t.Kind() != reflect.Func || t.IsVariadic() { - return false - } - ni, no := t.NumIn(), t.NumOut() - switch ft { - case ttbFunc: // func(T, T) bool - if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType { - return true - } - case tibFunc: // func(T, I) bool - if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType { - return true - } - case trFunc: // func(T) R - if ni == 1 && no == 1 { - return true - } - } - return false -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/format.go b/vendor/github.com/google/go-cmp/cmp/internal/value/format.go deleted file mode 100644 index 657e5087..00000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/value/format.go +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// Package value provides functionality for reflect.Value types. -package value - -import ( - "fmt" - "reflect" - "strconv" - "strings" - "unicode" -) - -var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() - -// Format formats the value v as a string. -// -// This is similar to fmt.Sprintf("%+v", v) except this: -// * Prints the type unless it can be elided -// * Avoids printing struct fields that are zero -// * Prints a nil-slice as being nil, not empty -// * Prints map entries in deterministic order -func Format(v reflect.Value, conf FormatConfig) string { - conf.printType = true - conf.followPointers = true - conf.realPointers = true - return formatAny(v, conf, nil) -} - -type FormatConfig struct { - UseStringer bool // Should the String method be used if available? - printType bool // Should we print the type before the value? - PrintPrimitiveType bool // Should we print the type of primitives? - followPointers bool // Should we recursively follow pointers? - realPointers bool // Should we print the real address of pointers? -} - -func formatAny(v reflect.Value, conf FormatConfig, visited map[uintptr]bool) string { - // TODO: Should this be a multi-line printout in certain situations? - - if !v.IsValid() { - return "" - } - if conf.UseStringer && v.Type().Implements(stringerIface) && v.CanInterface() { - if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() { - return "" - } - - const stringerPrefix = "s" // Indicates that the String method was used - s := v.Interface().(fmt.Stringer).String() - return stringerPrefix + formatString(s) - } - - switch v.Kind() { - case reflect.Bool: - return formatPrimitive(v.Type(), v.Bool(), conf) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return formatPrimitive(v.Type(), v.Int(), conf) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr { - // Unnamed uints are usually bytes or words, so use hexadecimal. - return formatPrimitive(v.Type(), formatHex(v.Uint()), conf) - } - return formatPrimitive(v.Type(), v.Uint(), conf) - case reflect.Float32, reflect.Float64: - return formatPrimitive(v.Type(), v.Float(), conf) - case reflect.Complex64, reflect.Complex128: - return formatPrimitive(v.Type(), v.Complex(), conf) - case reflect.String: - return formatPrimitive(v.Type(), formatString(v.String()), conf) - case reflect.UnsafePointer, reflect.Chan, reflect.Func: - return formatPointer(v, conf) - case reflect.Ptr: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("(%v)(nil)", v.Type()) - } - return "" - } - if visited[v.Pointer()] || !conf.followPointers { - return formatPointer(v, conf) - } - visited = insertPointer(visited, v.Pointer()) - return "&" + formatAny(v.Elem(), conf, visited) - case reflect.Interface: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "" - } - return formatAny(v.Elem(), conf, visited) - case reflect.Slice: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "" - } - if visited[v.Pointer()] { - return formatPointer(v, conf) - } - visited = insertPointer(visited, v.Pointer()) - fallthrough - case reflect.Array: - var ss []string - subConf := conf - subConf.printType = v.Type().Elem().Kind() == reflect.Interface - for i := 0; i < v.Len(); i++ { - s := formatAny(v.Index(i), subConf, visited) - ss = append(ss, s) - } - s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) - if conf.printType { - return v.Type().String() + s - } - return s - case reflect.Map: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "" - } - if visited[v.Pointer()] { - return formatPointer(v, conf) - } - visited = insertPointer(visited, v.Pointer()) - - var ss []string - keyConf, valConf := conf, conf - keyConf.printType = v.Type().Key().Kind() == reflect.Interface - keyConf.followPointers = false - valConf.printType = v.Type().Elem().Kind() == reflect.Interface - for _, k := range SortKeys(v.MapKeys()) { - sk := formatAny(k, keyConf, visited) - sv := formatAny(v.MapIndex(k), valConf, visited) - ss = append(ss, fmt.Sprintf("%s: %s", sk, sv)) - } - s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) - if conf.printType { - return v.Type().String() + s - } - return s - case reflect.Struct: - var ss []string - subConf := conf - subConf.printType = true - for i := 0; i < v.NumField(); i++ { - vv := v.Field(i) - if isZero(vv) { - continue // Elide zero value fields - } - name := v.Type().Field(i).Name - subConf.UseStringer = conf.UseStringer - s := formatAny(vv, subConf, visited) - ss = append(ss, fmt.Sprintf("%s: %s", name, s)) - } - s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) - if conf.printType { - return v.Type().String() + s - } - return s - default: - panic(fmt.Sprintf("%v kind not handled", v.Kind())) - } -} - -func formatString(s string) string { - // Use quoted string if it the same length as a raw string literal. - // Otherwise, attempt to use the raw string form. - qs := strconv.Quote(s) - if len(qs) == 1+len(s)+1 { - return qs - } - - // Disallow newlines to ensure output is a single line. - // Only allow printable runes for readability purposes. - rawInvalid := func(r rune) bool { - return r == '`' || r == '\n' || !unicode.IsPrint(r) - } - if strings.IndexFunc(s, rawInvalid) < 0 { - return "`" + s + "`" - } - return qs -} - -func formatPrimitive(t reflect.Type, v interface{}, conf FormatConfig) string { - if conf.printType && (conf.PrintPrimitiveType || t.PkgPath() != "") { - return fmt.Sprintf("%v(%v)", t, v) - } - return fmt.Sprintf("%v", v) -} - -func formatPointer(v reflect.Value, conf FormatConfig) string { - p := v.Pointer() - if !conf.realPointers { - p = 0 // For deterministic printing purposes - } - s := formatHex(uint64(p)) - if conf.printType { - return fmt.Sprintf("(%v)(%s)", v.Type(), s) - } - return s -} - -func formatHex(u uint64) string { - var f string - switch { - case u <= 0xff: - f = "0x%02x" - case u <= 0xffff: - f = "0x%04x" - case u <= 0xffffff: - f = "0x%06x" - case u <= 0xffffffff: - f = "0x%08x" - case u <= 0xffffffffff: - f = "0x%010x" - case u <= 0xffffffffffff: - f = "0x%012x" - case u <= 0xffffffffffffff: - f = "0x%014x" - case u <= 0xffffffffffffffff: - f = "0x%016x" - } - return fmt.Sprintf(f, u) -} - -// insertPointer insert p into m, allocating m if necessary. -func insertPointer(m map[uintptr]bool, p uintptr) map[uintptr]bool { - if m == nil { - m = make(map[uintptr]bool) - } - m[p] = true - return m -} - -// isZero reports whether v is the zero value. -// This does not rely on Interface and so can be used on unexported fields. -func isZero(v reflect.Value) bool { - switch v.Kind() { - case reflect.Bool: - return v.Bool() == false - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Complex64, reflect.Complex128: - return v.Complex() == 0 - case reflect.String: - return v.String() == "" - case reflect.UnsafePointer: - return v.Pointer() == 0 - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: - return v.IsNil() - case reflect.Array: - for i := 0; i < v.Len(); i++ { - if !isZero(v.Index(i)) { - return false - } - } - return true - case reflect.Struct: - for i := 0; i < v.NumField(); i++ { - if !isZero(v.Field(i)) { - return false - } - } - return true - } - return false -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go b/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go deleted file mode 100644 index fe8aa27a..00000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -package value - -import ( - "fmt" - "math" - "reflect" - "sort" -) - -// SortKeys sorts a list of map keys, deduplicating keys if necessary. -// The type of each value must be comparable. -func SortKeys(vs []reflect.Value) []reflect.Value { - if len(vs) == 0 { - return vs - } - - // Sort the map keys. - sort.Sort(valueSorter(vs)) - - // Deduplicate keys (fails for NaNs). - vs2 := vs[:1] - for _, v := range vs[1:] { - if isLess(vs2[len(vs2)-1], v) { - vs2 = append(vs2, v) - } - } - return vs2 -} - -// TODO: Use sort.Slice once Google AppEngine is on Go1.8 or above. -type valueSorter []reflect.Value - -func (vs valueSorter) Len() int { return len(vs) } -func (vs valueSorter) Less(i, j int) bool { return isLess(vs[i], vs[j]) } -func (vs valueSorter) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } - -// isLess is a generic function for sorting arbitrary map keys. -// The inputs must be of the same type and must be comparable. -func isLess(x, y reflect.Value) bool { - switch x.Type().Kind() { - case reflect.Bool: - return !x.Bool() && y.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return x.Int() < y.Int() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return x.Uint() < y.Uint() - case reflect.Float32, reflect.Float64: - fx, fy := x.Float(), y.Float() - return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) - case reflect.Complex64, reflect.Complex128: - cx, cy := x.Complex(), y.Complex() - rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) - if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) { - return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) - } - return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) - case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: - return x.Pointer() < y.Pointer() - case reflect.String: - return x.String() < y.String() - case reflect.Array: - for i := 0; i < x.Len(); i++ { - if isLess(x.Index(i), y.Index(i)) { - return true - } - if isLess(y.Index(i), x.Index(i)) { - return false - } - } - return false - case reflect.Struct: - for i := 0; i < x.NumField(); i++ { - if isLess(x.Field(i), y.Field(i)) { - return true - } - if isLess(y.Field(i), x.Field(i)) { - return false - } - } - return false - case reflect.Interface: - vx, vy := x.Elem(), y.Elem() - if !vx.IsValid() || !vy.IsValid() { - return !vx.IsValid() && vy.IsValid() - } - tx, ty := vx.Type(), vy.Type() - if tx == ty { - return isLess(x.Elem(), y.Elem()) - } - if tx.Kind() != ty.Kind() { - return vx.Kind() < vy.Kind() - } - if tx.String() != ty.String() { - return tx.String() < ty.String() - } - if tx.PkgPath() != ty.PkgPath() { - return tx.PkgPath() < ty.PkgPath() - } - // This can happen in rare situations, so we fallback to just comparing - // the unique pointer for a reflect.Type. This guarantees deterministic - // ordering within a program, but it is obviously not stable. - return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() - default: - // Must be Func, Map, or Slice; which are not comparable. - panic(fmt.Sprintf("%T is not comparable", x.Type())) - } -} diff --git a/vendor/github.com/google/go-cmp/cmp/options.go b/vendor/github.com/google/go-cmp/cmp/options.go deleted file mode 100644 index 91d4b066..00000000 --- a/vendor/github.com/google/go-cmp/cmp/options.go +++ /dev/null @@ -1,453 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -package cmp - -import ( - "fmt" - "reflect" - "runtime" - "strings" - - "github.com/google/go-cmp/cmp/internal/function" -) - -// Option configures for specific behavior of Equal and Diff. In particular, -// the fundamental Option functions (Ignore, Transformer, and Comparer), -// configure how equality is determined. -// -// The fundamental options may be composed with filters (FilterPath and -// FilterValues) to control the scope over which they are applied. -// -// The cmp/cmpopts package provides helper functions for creating options that -// may be used with Equal and Diff. -type Option interface { - // filter applies all filters and returns the option that remains. - // Each option may only read s.curPath and call s.callTTBFunc. - // - // An Options is returned only if multiple comparers or transformers - // can apply simultaneously and will only contain values of those types - // or sub-Options containing values of those types. - filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption -} - -// applicableOption represents the following types: -// Fundamental: ignore | invalid | *comparer | *transformer -// Grouping: Options -type applicableOption interface { - Option - - // apply executes the option, which may mutate s or panic. - apply(s *state, vx, vy reflect.Value) -} - -// coreOption represents the following types: -// Fundamental: ignore | invalid | *comparer | *transformer -// Filters: *pathFilter | *valuesFilter -type coreOption interface { - Option - isCore() -} - -type core struct{} - -func (core) isCore() {} - -// Options is a list of Option values that also satisfies the Option interface. -// Helper comparison packages may return an Options value when packing multiple -// Option values into a single Option. When this package processes an Options, -// it will be implicitly expanded into a flat list. -// -// Applying a filter on an Options is equivalent to applying that same filter -// on all individual options held within. -type Options []Option - -func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) { - for _, opt := range opts { - switch opt := opt.filter(s, vx, vy, t); opt.(type) { - case ignore: - return ignore{} // Only ignore can short-circuit evaluation - case invalid: - out = invalid{} // Takes precedence over comparer or transformer - case *comparer, *transformer, Options: - switch out.(type) { - case nil: - out = opt - case invalid: - // Keep invalid - case *comparer, *transformer, Options: - out = Options{out, opt} // Conflicting comparers or transformers - } - } - } - return out -} - -func (opts Options) apply(s *state, _, _ reflect.Value) { - const warning = "ambiguous set of applicable options" - const help = "consider using filters to ensure at most one Comparer or Transformer may apply" - var ss []string - for _, opt := range flattenOptions(nil, opts) { - ss = append(ss, fmt.Sprint(opt)) - } - set := strings.Join(ss, "\n\t") - panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help)) -} - -func (opts Options) String() string { - var ss []string - for _, opt := range opts { - ss = append(ss, fmt.Sprint(opt)) - } - return fmt.Sprintf("Options{%s}", strings.Join(ss, ", ")) -} - -// FilterPath returns a new Option where opt is only evaluated if filter f -// returns true for the current Path in the value tree. -// -// The option passed in may be an Ignore, Transformer, Comparer, Options, or -// a previously filtered Option. -func FilterPath(f func(Path) bool, opt Option) Option { - if f == nil { - panic("invalid path filter function") - } - if opt := normalizeOption(opt); opt != nil { - return &pathFilter{fnc: f, opt: opt} - } - return nil -} - -type pathFilter struct { - core - fnc func(Path) bool - opt Option -} - -func (f pathFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { - if f.fnc(s.curPath) { - return f.opt.filter(s, vx, vy, t) - } - return nil -} - -func (f pathFilter) String() string { - fn := getFuncName(reflect.ValueOf(f.fnc).Pointer()) - return fmt.Sprintf("FilterPath(%s, %v)", fn, f.opt) -} - -// FilterValues returns a new Option where opt is only evaluated if filter f, -// which is a function of the form "func(T, T) bool", returns true for the -// current pair of values being compared. If the type of the values is not -// assignable to T, then this filter implicitly returns false. -// -// The filter function must be -// symmetric (i.e., agnostic to the order of the inputs) and -// deterministic (i.e., produces the same result when given the same inputs). -// If T is an interface, it is possible that f is called with two values with -// different concrete types that both implement T. -// -// The option passed in may be an Ignore, Transformer, Comparer, Options, or -// a previously filtered Option. -func FilterValues(f interface{}, opt Option) Option { - v := reflect.ValueOf(f) - if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() { - panic(fmt.Sprintf("invalid values filter function: %T", f)) - } - if opt := normalizeOption(opt); opt != nil { - vf := &valuesFilter{fnc: v, opt: opt} - if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { - vf.typ = ti - } - return vf - } - return nil -} - -type valuesFilter struct { - core - typ reflect.Type // T - fnc reflect.Value // func(T, T) bool - opt Option -} - -func (f valuesFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { - if !vx.IsValid() || !vy.IsValid() { - return invalid{} - } - if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) { - return f.opt.filter(s, vx, vy, t) - } - return nil -} - -func (f valuesFilter) String() string { - fn := getFuncName(f.fnc.Pointer()) - return fmt.Sprintf("FilterValues(%s, %v)", fn, f.opt) -} - -// Ignore is an Option that causes all comparisons to be ignored. -// This value is intended to be combined with FilterPath or FilterValues. -// It is an error to pass an unfiltered Ignore option to Equal. -func Ignore() Option { return ignore{} } - -type ignore struct{ core } - -func (ignore) isFiltered() bool { return false } -func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} } -func (ignore) apply(_ *state, _, _ reflect.Value) { return } -func (ignore) String() string { return "Ignore()" } - -// invalid is a sentinel Option type to indicate that some options could not -// be evaluated due to unexported fields. -type invalid struct{ core } - -func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} } -func (invalid) apply(s *state, _, _ reflect.Value) { - const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" - panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) -} - -// Transformer returns an Option that applies a transformation function that -// converts values of a certain type into that of another. -// -// The transformer f must be a function "func(T) R" that converts values of -// type T to those of type R and is implicitly filtered to input values -// assignable to T. The transformer must not mutate T in any way. -// -// To help prevent some cases of infinite recursive cycles applying the -// same transform to the output of itself (e.g., in the case where the -// input and output types are the same), an implicit filter is added such that -// a transformer is applicable only if that exact transformer is not already -// in the tail of the Path since the last non-Transform step. -// -// The name is a user provided label that is used as the Transform.Name in the -// transformation PathStep. If empty, an arbitrary name is used. -func Transformer(name string, f interface{}) Option { - v := reflect.ValueOf(f) - if !function.IsType(v.Type(), function.Transformer) || v.IsNil() { - panic(fmt.Sprintf("invalid transformer function: %T", f)) - } - if name == "" { - name = "λ" // Lambda-symbol as place-holder for anonymous transformer - } - if !isValid(name) { - panic(fmt.Sprintf("invalid name: %q", name)) - } - tr := &transformer{name: name, fnc: reflect.ValueOf(f)} - if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { - tr.typ = ti - } - return tr -} - -type transformer struct { - core - name string - typ reflect.Type // T - fnc reflect.Value // func(T) R -} - -func (tr *transformer) isFiltered() bool { return tr.typ != nil } - -func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) applicableOption { - for i := len(s.curPath) - 1; i >= 0; i-- { - if t, ok := s.curPath[i].(*transform); !ok { - break // Hit most recent non-Transform step - } else if tr == t.trans { - return nil // Cannot directly use same Transform - } - } - if tr.typ == nil || t.AssignableTo(tr.typ) { - return tr - } - return nil -} - -func (tr *transformer) apply(s *state, vx, vy reflect.Value) { - // Update path before calling the Transformer so that dynamic checks - // will use the updated path. - s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr}) - defer s.curPath.pop() - - vx = s.callTRFunc(tr.fnc, vx) - vy = s.callTRFunc(tr.fnc, vy) - s.compareAny(vx, vy) -} - -func (tr transformer) String() string { - return fmt.Sprintf("Transformer(%s, %s)", tr.name, getFuncName(tr.fnc.Pointer())) -} - -// Comparer returns an Option that determines whether two values are equal -// to each other. -// -// The comparer f must be a function "func(T, T) bool" and is implicitly -// filtered to input values assignable to T. If T is an interface, it is -// possible that f is called with two values of different concrete types that -// both implement T. -// -// The equality function must be: -// • Symmetric: equal(x, y) == equal(y, x) -// • Deterministic: equal(x, y) == equal(x, y) -// • Pure: equal(x, y) does not modify x or y -func Comparer(f interface{}) Option { - v := reflect.ValueOf(f) - if !function.IsType(v.Type(), function.Equal) || v.IsNil() { - panic(fmt.Sprintf("invalid comparer function: %T", f)) - } - cm := &comparer{fnc: v} - if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { - cm.typ = ti - } - return cm -} - -type comparer struct { - core - typ reflect.Type // T - fnc reflect.Value // func(T, T) bool -} - -func (cm *comparer) isFiltered() bool { return cm.typ != nil } - -func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption { - if cm.typ == nil || t.AssignableTo(cm.typ) { - return cm - } - return nil -} - -func (cm *comparer) apply(s *state, vx, vy reflect.Value) { - eq := s.callTTBFunc(cm.fnc, vx, vy) - s.report(eq, vx, vy) -} - -func (cm comparer) String() string { - return fmt.Sprintf("Comparer(%s)", getFuncName(cm.fnc.Pointer())) -} - -// AllowUnexported returns an Option that forcibly allows operations on -// unexported fields in certain structs, which are specified by passing in a -// value of each struct type. -// -// Users of this option must understand that comparing on unexported fields -// from external packages is not safe since changes in the internal -// implementation of some external package may cause the result of Equal -// to unexpectedly change. However, it may be valid to use this option on types -// defined in an internal package where the semantic meaning of an unexported -// field is in the control of the user. -// -// For some cases, a custom Comparer should be used instead that defines -// equality as a function of the public API of a type rather than the underlying -// unexported implementation. -// -// For example, the reflect.Type documentation defines equality to be determined -// by the == operator on the interface (essentially performing a shallow pointer -// comparison) and most attempts to compare *regexp.Regexp types are interested -// in only checking that the regular expression strings are equal. -// Both of these are accomplished using Comparers: -// -// Comparer(func(x, y reflect.Type) bool { return x == y }) -// Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() }) -// -// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore -// all unexported fields on specified struct types. -func AllowUnexported(types ...interface{}) Option { - if !supportAllowUnexported { - panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS") - } - m := make(map[reflect.Type]bool) - for _, typ := range types { - t := reflect.TypeOf(typ) - if t.Kind() != reflect.Struct { - panic(fmt.Sprintf("invalid struct type: %T", typ)) - } - m[t] = true - } - return visibleStructs(m) -} - -type visibleStructs map[reflect.Type]bool - -func (visibleStructs) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { - panic("not implemented") -} - -// reporter is an Option that configures how differences are reported. -type reporter interface { - // TODO: Not exported yet. - // - // Perhaps add PushStep and PopStep and change Report to only accept - // a PathStep instead of the full-path? Adding a PushStep and PopStep makes - // it clear that we are traversing the value tree in a depth-first-search - // manner, which has an effect on how values are printed. - - Option - - // Report is called for every comparison made and will be provided with - // the two values being compared, the equality result, and the - // current path in the value tree. It is possible for x or y to be an - // invalid reflect.Value if one of the values is non-existent; - // which is possible with maps and slices. - Report(x, y reflect.Value, eq bool, p Path) -} - -// normalizeOption normalizes the input options such that all Options groups -// are flattened and groups with a single element are reduced to that element. -// Only coreOptions and Options containing coreOptions are allowed. -func normalizeOption(src Option) Option { - switch opts := flattenOptions(nil, Options{src}); len(opts) { - case 0: - return nil - case 1: - return opts[0] - default: - return opts - } -} - -// flattenOptions copies all options in src to dst as a flat list. -// Only coreOptions and Options containing coreOptions are allowed. -func flattenOptions(dst, src Options) Options { - for _, opt := range src { - switch opt := opt.(type) { - case nil: - continue - case Options: - dst = flattenOptions(dst, opt) - case coreOption: - dst = append(dst, opt) - default: - panic(fmt.Sprintf("invalid option type: %T", opt)) - } - } - return dst -} - -// getFuncName returns a short function name from the pointer. -// The string parsing logic works up until Go1.9. -func getFuncName(p uintptr) string { - fnc := runtime.FuncForPC(p) - if fnc == nil { - return "" - } - name := fnc.Name() // E.g., "long/path/name/mypkg.(mytype).(long/path/name/mypkg.myfunc)-fm" - if strings.HasSuffix(name, ")-fm") || strings.HasSuffix(name, ")·fm") { - // Strip the package name from method name. - name = strings.TrimSuffix(name, ")-fm") - name = strings.TrimSuffix(name, ")·fm") - if i := strings.LastIndexByte(name, '('); i >= 0 { - methodName := name[i+1:] // E.g., "long/path/name/mypkg.myfunc" - if j := strings.LastIndexByte(methodName, '.'); j >= 0 { - methodName = methodName[j+1:] // E.g., "myfunc" - } - name = name[:i] + methodName // E.g., "long/path/name/mypkg.(mytype)." + "myfunc" - } - } - if i := strings.LastIndexByte(name, '/'); i >= 0 { - // Strip the package name. - name = name[i+1:] // E.g., "mypkg.(mytype).myfunc" - } - return name -} diff --git a/vendor/github.com/google/go-cmp/cmp/path.go b/vendor/github.com/google/go-cmp/cmp/path.go deleted file mode 100644 index c08a3cf8..00000000 --- a/vendor/github.com/google/go-cmp/cmp/path.go +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -package cmp - -import ( - "fmt" - "reflect" - "strings" - "unicode" - "unicode/utf8" -) - -type ( - // Path is a list of PathSteps describing the sequence of operations to get - // from some root type to the current position in the value tree. - // The first Path element is always an operation-less PathStep that exists - // simply to identify the initial type. - // - // When traversing structs with embedded structs, the embedded struct will - // always be accessed as a field before traversing the fields of the - // embedded struct themselves. That is, an exported field from the - // embedded struct will never be accessed directly from the parent struct. - Path []PathStep - - // PathStep is a union-type for specific operations to traverse - // a value's tree structure. Users of this package never need to implement - // these types as values of this type will be returned by this package. - PathStep interface { - String() string - Type() reflect.Type // Resulting type after performing the path step - isPathStep() - } - - // SliceIndex is an index operation on a slice or array at some index Key. - SliceIndex interface { - PathStep - Key() int // May return -1 if in a split state - - // SplitKeys returns the indexes for indexing into slices in the - // x and y values, respectively. These indexes may differ due to the - // insertion or removal of an element in one of the slices, causing - // all of the indexes to be shifted. If an index is -1, then that - // indicates that the element does not exist in the associated slice. - // - // Key is guaranteed to return -1 if and only if the indexes returned - // by SplitKeys are not the same. SplitKeys will never return -1 for - // both indexes. - SplitKeys() (x int, y int) - - isSliceIndex() - } - // MapIndex is an index operation on a map at some index Key. - MapIndex interface { - PathStep - Key() reflect.Value - isMapIndex() - } - // TypeAssertion represents a type assertion on an interface. - TypeAssertion interface { - PathStep - isTypeAssertion() - } - // StructField represents a struct field access on a field called Name. - StructField interface { - PathStep - Name() string - Index() int - isStructField() - } - // Indirect represents pointer indirection on the parent type. - Indirect interface { - PathStep - isIndirect() - } - // Transform is a transformation from the parent type to the current type. - Transform interface { - PathStep - Name() string - Func() reflect.Value - - // Option returns the originally constructed Transformer option. - // The == operator can be used to detect the exact option used. - Option() Option - - isTransform() - } -) - -func (pa *Path) push(s PathStep) { - *pa = append(*pa, s) -} - -func (pa *Path) pop() { - *pa = (*pa)[:len(*pa)-1] -} - -// Last returns the last PathStep in the Path. -// If the path is empty, this returns a non-nil PathStep that reports a nil Type. -func (pa Path) Last() PathStep { - return pa.Index(-1) -} - -// Index returns the ith step in the Path and supports negative indexing. -// A negative index starts counting from the tail of the Path such that -1 -// refers to the last step, -2 refers to the second-to-last step, and so on. -// If index is invalid, this returns a non-nil PathStep that reports a nil Type. -func (pa Path) Index(i int) PathStep { - if i < 0 { - i = len(pa) + i - } - if i < 0 || i >= len(pa) { - return pathStep{} - } - return pa[i] -} - -// String returns the simplified path to a node. -// The simplified path only contains struct field accesses. -// -// For example: -// MyMap.MySlices.MyField -func (pa Path) String() string { - var ss []string - for _, s := range pa { - if _, ok := s.(*structField); ok { - ss = append(ss, s.String()) - } - } - return strings.TrimPrefix(strings.Join(ss, ""), ".") -} - -// GoString returns the path to a specific node using Go syntax. -// -// For example: -// (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField -func (pa Path) GoString() string { - var ssPre, ssPost []string - var numIndirect int - for i, s := range pa { - var nextStep PathStep - if i+1 < len(pa) { - nextStep = pa[i+1] - } - switch s := s.(type) { - case *indirect: - numIndirect++ - pPre, pPost := "(", ")" - switch nextStep.(type) { - case *indirect: - continue // Next step is indirection, so let them batch up - case *structField: - numIndirect-- // Automatic indirection on struct fields - case nil: - pPre, pPost = "", "" // Last step; no need for parenthesis - } - if numIndirect > 0 { - ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect)) - ssPost = append(ssPost, pPost) - } - numIndirect = 0 - continue - case *transform: - ssPre = append(ssPre, s.trans.name+"(") - ssPost = append(ssPost, ")") - continue - case *typeAssertion: - // As a special-case, elide type assertions on anonymous types - // since they are typically generated dynamically and can be very - // verbose. For example, some transforms return interface{} because - // of Go's lack of generics, but typically take in and return the - // exact same concrete type. - if s.Type().PkgPath() == "" { - continue - } - } - ssPost = append(ssPost, s.String()) - } - for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 { - ssPre[i], ssPre[j] = ssPre[j], ssPre[i] - } - return strings.Join(ssPre, "") + strings.Join(ssPost, "") -} - -type ( - pathStep struct { - typ reflect.Type - } - - sliceIndex struct { - pathStep - xkey, ykey int - } - mapIndex struct { - pathStep - key reflect.Value - } - typeAssertion struct { - pathStep - } - structField struct { - pathStep - name string - idx int - - // These fields are used for forcibly accessing an unexported field. - // pvx, pvy, and field are only valid if unexported is true. - unexported bool - force bool // Forcibly allow visibility - pvx, pvy reflect.Value // Parent values - field reflect.StructField // Field information - } - indirect struct { - pathStep - } - transform struct { - pathStep - trans *transformer - } -) - -func (ps pathStep) Type() reflect.Type { return ps.typ } -func (ps pathStep) String() string { - if ps.typ == nil { - return "" - } - s := ps.typ.String() - if s == "" || strings.ContainsAny(s, "{}\n") { - return "root" // Type too simple or complex to print - } - return fmt.Sprintf("{%s}", s) -} - -func (si sliceIndex) String() string { - switch { - case si.xkey == si.ykey: - return fmt.Sprintf("[%d]", si.xkey) - case si.ykey == -1: - // [5->?] means "I don't know where X[5] went" - return fmt.Sprintf("[%d->?]", si.xkey) - case si.xkey == -1: - // [?->3] means "I don't know where Y[3] came from" - return fmt.Sprintf("[?->%d]", si.ykey) - default: - // [5->3] means "X[5] moved to Y[3]" - return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey) - } -} -func (mi mapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } -func (ta typeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) } -func (sf structField) String() string { return fmt.Sprintf(".%s", sf.name) } -func (in indirect) String() string { return "*" } -func (tf transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) } - -func (si sliceIndex) Key() int { - if si.xkey != si.ykey { - return -1 - } - return si.xkey -} -func (si sliceIndex) SplitKeys() (x, y int) { return si.xkey, si.ykey } -func (mi mapIndex) Key() reflect.Value { return mi.key } -func (sf structField) Name() string { return sf.name } -func (sf structField) Index() int { return sf.idx } -func (tf transform) Name() string { return tf.trans.name } -func (tf transform) Func() reflect.Value { return tf.trans.fnc } -func (tf transform) Option() Option { return tf.trans } - -func (pathStep) isPathStep() {} -func (sliceIndex) isSliceIndex() {} -func (mapIndex) isMapIndex() {} -func (typeAssertion) isTypeAssertion() {} -func (structField) isStructField() {} -func (indirect) isIndirect() {} -func (transform) isTransform() {} - -var ( - _ SliceIndex = sliceIndex{} - _ MapIndex = mapIndex{} - _ TypeAssertion = typeAssertion{} - _ StructField = structField{} - _ Indirect = indirect{} - _ Transform = transform{} - - _ PathStep = sliceIndex{} - _ PathStep = mapIndex{} - _ PathStep = typeAssertion{} - _ PathStep = structField{} - _ PathStep = indirect{} - _ PathStep = transform{} -) - -// isExported reports whether the identifier is exported. -func isExported(id string) bool { - r, _ := utf8.DecodeRuneInString(id) - return unicode.IsUpper(r) -} - -// isValid reports whether the identifier is valid. -// Empty and underscore-only strings are not valid. -func isValid(id string) bool { - ok := id != "" && id != "_" - for j, c := range id { - ok = ok && (j > 0 || !unicode.IsDigit(c)) - ok = ok && (c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c)) - } - return ok -} diff --git a/vendor/github.com/google/go-cmp/cmp/reporter.go b/vendor/github.com/google/go-cmp/cmp/reporter.go deleted file mode 100644 index 20e9f18e..00000000 --- a/vendor/github.com/google/go-cmp/cmp/reporter.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -package cmp - -import ( - "fmt" - "reflect" - "strings" - - "github.com/google/go-cmp/cmp/internal/value" -) - -type defaultReporter struct { - Option - diffs []string // List of differences, possibly truncated - ndiffs int // Total number of differences - nbytes int // Number of bytes in diffs - nlines int // Number of lines in diffs -} - -var _ reporter = (*defaultReporter)(nil) - -func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) { - if eq { - return // Ignore equal results - } - const maxBytes = 4096 - const maxLines = 256 - r.ndiffs++ - if r.nbytes < maxBytes && r.nlines < maxLines { - sx := value.Format(x, value.FormatConfig{UseStringer: true}) - sy := value.Format(y, value.FormatConfig{UseStringer: true}) - if sx == sy { - // Unhelpful output, so use more exact formatting. - sx = value.Format(x, value.FormatConfig{PrintPrimitiveType: true}) - sy = value.Format(y, value.FormatConfig{PrintPrimitiveType: true}) - } - s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy) - r.diffs = append(r.diffs, s) - r.nbytes += len(s) - r.nlines += strings.Count(s, "\n") - } -} - -func (r *defaultReporter) String() string { - s := strings.Join(r.diffs, "") - if r.ndiffs == len(r.diffs) { - return s - } - return fmt.Sprintf("%s... %d more differences ...", s, r.ndiffs-len(r.diffs)) -} diff --git a/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go b/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go deleted file mode 100644 index d1518eb3..00000000 --- a/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// +build purego appengine js - -package cmp - -import "reflect" - -const supportAllowUnexported = false - -func unsafeRetrieveField(reflect.Value, reflect.StructField) reflect.Value { - panic("unsafeRetrieveField is not implemented") -} diff --git a/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go b/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go deleted file mode 100644 index 579b6550..00000000 --- a/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// +build !purego,!appengine,!js - -package cmp - -import ( - "reflect" - "unsafe" -) - -const supportAllowUnexported = true - -// unsafeRetrieveField uses unsafe to forcibly retrieve any field from a struct -// such that the value has read-write permissions. -// -// The parent struct, v, must be addressable, while f must be a StructField -// describing the field to retrieve. -func unsafeRetrieveField(v reflect.Value, f reflect.StructField) reflect.Value { - return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem() -} diff --git a/vendor/gotest.tools/LICENSE b/vendor/gotest.tools/LICENSE deleted file mode 100644 index d6456956..00000000 --- a/vendor/gotest.tools/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/vendor/gotest.tools/assert/assert.go b/vendor/gotest.tools/assert/assert.go deleted file mode 100644 index 05d66354..00000000 --- a/vendor/gotest.tools/assert/assert.go +++ /dev/null @@ -1,311 +0,0 @@ -/*Package assert provides assertions for comparing expected values to actual -values. When an assertion fails a helpful error message is printed. - -Assert and Check - -Assert() and Check() both accept a Comparison, and fail the test when the -comparison fails. The one difference is that Assert() will end the test execution -immediately (using t.FailNow()) whereas Check() will fail the test (using t.Fail()), -return the value of the comparison, then proceed with the rest of the test case. - -Example usage - -The example below shows assert used with some common types. - - - import ( - "testing" - - "gotest.tools/assert" - is "gotest.tools/assert/cmp" - ) - - func TestEverything(t *testing.T) { - // booleans - assert.Assert(t, ok) - assert.Assert(t, !missing) - - // primitives - assert.Equal(t, count, 1) - assert.Equal(t, msg, "the message") - assert.Assert(t, total != 10) // NotEqual - - // errors - assert.NilError(t, closer.Close()) - assert.Error(t, err, "the exact error message") - assert.ErrorContains(t, err, "includes this") - assert.ErrorType(t, err, os.IsNotExist) - - // complex types - assert.DeepEqual(t, result, myStruct{Name: "title"}) - assert.Assert(t, is.Len(items, 3)) - assert.Assert(t, len(sequence) != 0) // NotEmpty - assert.Assert(t, is.Contains(mapping, "key")) - - // pointers and interface - assert.Assert(t, is.Nil(ref)) - assert.Assert(t, ref != nil) // NotNil - } - -Comparisons - -Package https://godoc.org/gotest.tools/assert/cmp provides -many common comparisons. Additional comparisons can be written to compare -values in other ways. See the example Assert (CustomComparison). - -Automated migration from testify - -gty-migrate-from-testify is a binary which can update source code which uses -testify assertions to use the assertions provided by this package. - -See http://bit.do/cmd-gty-migrate-from-testify. - - -*/ -package assert // import "gotest.tools/assert" - -import ( - "fmt" - "go/ast" - "go/token" - - gocmp "github.com/google/go-cmp/cmp" - "gotest.tools/assert/cmp" - "gotest.tools/internal/format" - "gotest.tools/internal/source" -) - -// BoolOrComparison can be a bool, or cmp.Comparison. See Assert() for usage. -type BoolOrComparison interface{} - -// TestingT is the subset of testing.T used by the assert package. -type TestingT interface { - FailNow() - Fail() - Log(args ...interface{}) -} - -type helperT interface { - Helper() -} - -const failureMessage = "assertion failed: " - -// nolint: gocyclo -func assert( - t TestingT, - failer func(), - argSelector argSelector, - comparison BoolOrComparison, - msgAndArgs ...interface{}, -) bool { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - var success bool - switch check := comparison.(type) { - case bool: - if check { - return true - } - logFailureFromBool(t, msgAndArgs...) - - // Undocumented legacy comparison without Result type - case func() (success bool, message string): - success = runCompareFunc(t, check, msgAndArgs...) - - case nil: - return true - - case error: - msg := "error is not nil: " - t.Log(format.WithCustomMessage(failureMessage+msg+check.Error(), msgAndArgs...)) - - case cmp.Comparison: - success = runComparison(t, argSelector, check, msgAndArgs...) - - case func() cmp.Result: - success = runComparison(t, argSelector, check, msgAndArgs...) - - default: - t.Log(fmt.Sprintf("invalid Comparison: %v (%T)", check, check)) - } - - if success { - return true - } - failer() - return false -} - -func runCompareFunc( - t TestingT, - f func() (success bool, message string), - msgAndArgs ...interface{}, -) bool { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - if success, message := f(); !success { - t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) - return false - } - return true -} - -func logFailureFromBool(t TestingT, msgAndArgs ...interface{}) { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - const stackIndex = 3 // Assert()/Check(), assert(), formatFailureFromBool() - const comparisonArgPos = 1 - args, err := source.CallExprArgs(stackIndex) - if err != nil { - t.Log(err.Error()) - return - } - - msg, err := boolFailureMessage(args[comparisonArgPos]) - if err != nil { - t.Log(err.Error()) - msg = "expression is false" - } - - t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...)) -} - -func boolFailureMessage(expr ast.Expr) (string, error) { - if binaryExpr, ok := expr.(*ast.BinaryExpr); ok && binaryExpr.Op == token.NEQ { - x, err := source.FormatNode(binaryExpr.X) - if err != nil { - return "", err - } - y, err := source.FormatNode(binaryExpr.Y) - if err != nil { - return "", err - } - return x + " is " + y, nil - } - - if unaryExpr, ok := expr.(*ast.UnaryExpr); ok && unaryExpr.Op == token.NOT { - x, err := source.FormatNode(unaryExpr.X) - if err != nil { - return "", err - } - return x + " is true", nil - } - - formatted, err := source.FormatNode(expr) - if err != nil { - return "", err - } - return "expression is false: " + formatted, nil -} - -// Assert performs a comparison. If the comparison fails the test is marked as -// failed, a failure message is logged, and execution is stopped immediately. -// -// The comparison argument may be one of three types: bool, cmp.Comparison or -// error. -// When called with a bool the failure message will contain the literal source -// code of the expression. -// When called with a cmp.Comparison the comparison is responsible for producing -// a helpful failure message. -// When called with an error a nil value is considered success. A non-nil error -// is a failure, and Error() is used as the failure message. -func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - assert(t, t.FailNow, argsFromComparisonCall, comparison, msgAndArgs...) -} - -// Check performs a comparison. If the comparison fails the test is marked as -// failed, a failure message is logged, and Check returns false. Otherwise returns -// true. -// -// See Assert for details about the comparison arg and failure messages. -func Check(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) bool { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - return assert(t, t.Fail, argsFromComparisonCall, comparison, msgAndArgs...) -} - -// NilError fails the test immediately if err is not nil. -// This is equivalent to Assert(t, err) -func NilError(t TestingT, err error, msgAndArgs ...interface{}) { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - assert(t, t.FailNow, argsAfterT, err, msgAndArgs...) -} - -// Equal uses the == operator to assert two values are equal and fails the test -// if they are not equal. -// -// If the comparison fails Equal will use the variable names for x and y as part -// of the failure message to identify the actual and expected values. -// -// If either x or y are a multi-line string the failure message will include a -// unified diff of the two values. If the values only differ by whitespace -// the unified diff will be augmented by replacing whitespace characters with -// visible characters to identify the whitespace difference. -// -// This is equivalent to Assert(t, cmp.Equal(x, y)). -func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - assert(t, t.FailNow, argsAfterT, cmp.Equal(x, y), msgAndArgs...) -} - -// DeepEqual uses google/go-cmp (http://bit.do/go-cmp) to assert two values are -// equal and fails the test if they are not equal. -// -// Package https://godoc.org/gotest.tools/assert/opt provides some additional -// commonly used Options. -// -// This is equivalent to Assert(t, cmp.DeepEqual(x, y)). -func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - assert(t, t.FailNow, argsAfterT, cmp.DeepEqual(x, y, opts...)) -} - -// Error fails the test if err is nil, or the error message is not the expected -// message. -// Equivalent to Assert(t, cmp.Error(err, message)). -func Error(t TestingT, err error, message string, msgAndArgs ...interface{}) { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - assert(t, t.FailNow, argsAfterT, cmp.Error(err, message), msgAndArgs...) -} - -// ErrorContains fails the test if err is nil, or the error message does not -// contain the expected substring. -// Equivalent to Assert(t, cmp.ErrorContains(err, substring)). -func ErrorContains(t TestingT, err error, substring string, msgAndArgs ...interface{}) { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - assert(t, t.FailNow, argsAfterT, cmp.ErrorContains(err, substring), msgAndArgs...) -} - -// ErrorType fails the test if err is nil, or err is not the expected type. -// -// Expected can be one of: -// a func(error) bool which returns true if the error is the expected type, -// an instance of (or a pointer to) a struct of the expected type, -// a pointer to an interface the error is expected to implement, -// a reflect.Type of the expected struct or interface. -// -// Equivalent to Assert(t, cmp.ErrorType(err, expected)). -func ErrorType(t TestingT, err error, expected interface{}, msgAndArgs ...interface{}) { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - assert(t, t.FailNow, argsAfterT, cmp.ErrorType(err, expected), msgAndArgs...) -} diff --git a/vendor/gotest.tools/assert/cmp/compare.go b/vendor/gotest.tools/assert/cmp/compare.go deleted file mode 100644 index ae03749e..00000000 --- a/vendor/gotest.tools/assert/cmp/compare.go +++ /dev/null @@ -1,312 +0,0 @@ -/*Package cmp provides Comparisons for Assert and Check*/ -package cmp // import "gotest.tools/assert/cmp" - -import ( - "fmt" - "reflect" - "strings" - - "github.com/google/go-cmp/cmp" - "gotest.tools/internal/format" -) - -// Comparison is a function which compares values and returns ResultSuccess if -// the actual value matches the expected value. If the values do not match the -// Result will contain a message about why it failed. -type Comparison func() Result - -// DeepEqual compares two values using google/go-cmp (http://bit.do/go-cmp) -// and succeeds if the values are equal. -// -// The comparison can be customized using comparison Options. -// Package https://godoc.org/gotest.tools/assert/opt provides some additional -// commonly used Options. -func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison { - return func() (result Result) { - defer func() { - if panicmsg, handled := handleCmpPanic(recover()); handled { - result = ResultFailure(panicmsg) - } - }() - diff := cmp.Diff(x, y, opts...) - if diff == "" { - return ResultSuccess - } - return multiLineDiffResult(diff) - } -} - -func handleCmpPanic(r interface{}) (string, bool) { - if r == nil { - return "", false - } - panicmsg, ok := r.(string) - if !ok { - panic(r) - } - switch { - case strings.HasPrefix(panicmsg, "cannot handle unexported field"): - return panicmsg, true - } - panic(r) -} - -func toResult(success bool, msg string) Result { - if success { - return ResultSuccess - } - return ResultFailure(msg) -} - -// Equal succeeds if x == y. See assert.Equal for full documentation. -func Equal(x, y interface{}) Comparison { - return func() Result { - switch { - case x == y: - return ResultSuccess - case isMultiLineStringCompare(x, y): - diff := format.UnifiedDiff(format.DiffConfig{A: x.(string), B: y.(string)}) - return multiLineDiffResult(diff) - } - return ResultFailureTemplate(` - {{- .Data.x}} ( - {{- with callArg 0 }}{{ formatNode . }} {{end -}} - {{- printf "%T" .Data.x -}} - ) != {{ .Data.y}} ( - {{- with callArg 1 }}{{ formatNode . }} {{end -}} - {{- printf "%T" .Data.y -}} - )`, - map[string]interface{}{"x": x, "y": y}) - } -} - -func isMultiLineStringCompare(x, y interface{}) bool { - strX, ok := x.(string) - if !ok { - return false - } - strY, ok := y.(string) - if !ok { - return false - } - return strings.Contains(strX, "\n") || strings.Contains(strY, "\n") -} - -func multiLineDiffResult(diff string) Result { - return ResultFailureTemplate(` ---- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}} -+++ {{ with callArg 1 }}{{ formatNode . }}{{else}}→{{end}} -{{ .Data.diff }}`, - map[string]interface{}{"diff": diff}) -} - -// Len succeeds if the sequence has the expected length. -func Len(seq interface{}, expected int) Comparison { - return func() (result Result) { - defer func() { - if e := recover(); e != nil { - result = ResultFailure(fmt.Sprintf("type %T does not have a length", seq)) - } - }() - value := reflect.ValueOf(seq) - length := value.Len() - if length == expected { - return ResultSuccess - } - msg := fmt.Sprintf("expected %s (length %d) to have length %d", seq, length, expected) - return ResultFailure(msg) - } -} - -// Contains succeeds if item is in collection. Collection may be a string, map, -// slice, or array. -// -// If collection is a string, item must also be a string, and is compared using -// strings.Contains(). -// If collection is a Map, contains will succeed if item is a key in the map. -// If collection is a slice or array, item is compared to each item in the -// sequence using reflect.DeepEqual(). -func Contains(collection interface{}, item interface{}) Comparison { - return func() Result { - colValue := reflect.ValueOf(collection) - if !colValue.IsValid() { - return ResultFailure(fmt.Sprintf("nil does not contain items")) - } - msg := fmt.Sprintf("%v does not contain %v", collection, item) - - itemValue := reflect.ValueOf(item) - switch colValue.Type().Kind() { - case reflect.String: - if itemValue.Type().Kind() != reflect.String { - return ResultFailure("string may only contain strings") - } - return toResult( - strings.Contains(colValue.String(), itemValue.String()), - fmt.Sprintf("string %q does not contain %q", collection, item)) - - case reflect.Map: - if itemValue.Type() != colValue.Type().Key() { - return ResultFailure(fmt.Sprintf( - "%v can not contain a %v key", colValue.Type(), itemValue.Type())) - } - return toResult(colValue.MapIndex(itemValue).IsValid(), msg) - - case reflect.Slice, reflect.Array: - for i := 0; i < colValue.Len(); i++ { - if reflect.DeepEqual(colValue.Index(i).Interface(), item) { - return ResultSuccess - } - } - return ResultFailure(msg) - default: - return ResultFailure(fmt.Sprintf("type %T does not contain items", collection)) - } - } -} - -// Panics succeeds if f() panics. -func Panics(f func()) Comparison { - return func() (result Result) { - defer func() { - if err := recover(); err != nil { - result = ResultSuccess - } - }() - f() - return ResultFailure("did not panic") - } -} - -// Error succeeds if err is a non-nil error, and the error message equals the -// expected message. -func Error(err error, message string) Comparison { - return func() Result { - switch { - case err == nil: - return ResultFailure("expected an error, got nil") - case err.Error() != message: - return ResultFailure(fmt.Sprintf( - "expected error %q, got %+v", message, err)) - } - return ResultSuccess - } -} - -// ErrorContains succeeds if err is a non-nil error, and the error message contains -// the expected substring. -func ErrorContains(err error, substring string) Comparison { - return func() Result { - switch { - case err == nil: - return ResultFailure("expected an error, got nil") - case !strings.Contains(err.Error(), substring): - return ResultFailure(fmt.Sprintf( - "expected error to contain %q, got %+v", substring, err)) - } - return ResultSuccess - } -} - -// Nil succeeds if obj is a nil interface, pointer, or function. -// -// Use NilError() for comparing errors. Use Len(obj, 0) for comparing slices, -// maps, and channels. -func Nil(obj interface{}) Comparison { - msgFunc := func(value reflect.Value) string { - return fmt.Sprintf("%v (type %s) is not nil", reflect.Indirect(value), value.Type()) - } - return isNil(obj, msgFunc) -} - -func isNil(obj interface{}, msgFunc func(reflect.Value) string) Comparison { - return func() Result { - if obj == nil { - return ResultSuccess - } - value := reflect.ValueOf(obj) - kind := value.Type().Kind() - if kind >= reflect.Chan && kind <= reflect.Slice { - if value.IsNil() { - return ResultSuccess - } - return ResultFailure(msgFunc(value)) - } - - return ResultFailure(fmt.Sprintf("%v (type %s) can not be nil", value, value.Type())) - } -} - -// ErrorType succeeds if err is not nil and is of the expected type. -// -// Expected can be one of: -// a func(error) bool which returns true if the error is the expected type, -// an instance of (or a pointer to) a struct of the expected type, -// a pointer to an interface the error is expected to implement, -// a reflect.Type of the expected struct or interface. -func ErrorType(err error, expected interface{}) Comparison { - return func() Result { - switch expectedType := expected.(type) { - case func(error) bool: - return cmpErrorTypeFunc(err, expectedType) - case reflect.Type: - if expectedType.Kind() == reflect.Interface { - return cmpErrorTypeImplementsType(err, expectedType) - } - return cmpErrorTypeEqualType(err, expectedType) - case nil: - return ResultFailure(fmt.Sprintf("invalid type for expected: nil")) - } - - expectedType := reflect.TypeOf(expected) - switch { - case expectedType.Kind() == reflect.Struct, isPtrToStruct(expectedType): - return cmpErrorTypeEqualType(err, expectedType) - case isPtrToInterface(expectedType): - return cmpErrorTypeImplementsType(err, expectedType.Elem()) - } - return ResultFailure(fmt.Sprintf("invalid type for expected: %T", expected)) - } -} - -func cmpErrorTypeFunc(err error, f func(error) bool) Result { - if f(err) { - return ResultSuccess - } - actual := "nil" - if err != nil { - actual = fmt.Sprintf("%s (%T)", err, err) - } - return ResultFailureTemplate(`error is {{ .Data.actual }} - {{- with callArg 1 }}, not {{ formatNode . }}{{end -}}`, - map[string]interface{}{"actual": actual}) -} - -func cmpErrorTypeEqualType(err error, expectedType reflect.Type) Result { - if err == nil { - return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType)) - } - errValue := reflect.ValueOf(err) - if errValue.Type() == expectedType { - return ResultSuccess - } - return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType)) -} - -func cmpErrorTypeImplementsType(err error, expectedType reflect.Type) Result { - if err == nil { - return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType)) - } - errValue := reflect.ValueOf(err) - if errValue.Type().Implements(expectedType) { - return ResultSuccess - } - return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType)) -} - -func isPtrToInterface(typ reflect.Type) bool { - return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Interface -} - -func isPtrToStruct(typ reflect.Type) bool { - return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct -} diff --git a/vendor/gotest.tools/assert/cmp/result.go b/vendor/gotest.tools/assert/cmp/result.go deleted file mode 100644 index 7c3c37dd..00000000 --- a/vendor/gotest.tools/assert/cmp/result.go +++ /dev/null @@ -1,94 +0,0 @@ -package cmp - -import ( - "bytes" - "fmt" - "go/ast" - "text/template" - - "gotest.tools/internal/source" -) - -// Result of a Comparison. -type Result interface { - Success() bool -} - -type result struct { - success bool - message string -} - -func (r result) Success() bool { - return r.success -} - -func (r result) FailureMessage() string { - return r.message -} - -// ResultSuccess is a constant which is returned by a ComparisonWithResult to -// indicate success. -var ResultSuccess = result{success: true} - -// ResultFailure returns a failed Result with a failure message. -func ResultFailure(message string) Result { - return result{message: message} -} - -// ResultFromError returns ResultSuccess if err is nil. Otherwise ResultFailure -// is returned with the error message as the failure message. -func ResultFromError(err error) Result { - if err == nil { - return ResultSuccess - } - return ResultFailure(err.Error()) -} - -type templatedResult struct { - success bool - template string - data map[string]interface{} -} - -func (r templatedResult) Success() bool { - return r.success -} - -func (r templatedResult) FailureMessage(args []ast.Expr) string { - msg, err := renderMessage(r, args) - if err != nil { - return fmt.Sprintf("failed to render failure message: %s", err) - } - return msg -} - -// ResultFailureTemplate returns a Result with a template string and data which -// can be used to format a failure message. The template may access data from .Data, -// the comparison args with the callArg function, and the formatNode function may -// be used to format the call args. -func ResultFailureTemplate(template string, data map[string]interface{}) Result { - return templatedResult{template: template, data: data} -} - -func renderMessage(result templatedResult, args []ast.Expr) (string, error) { - tmpl := template.New("failure").Funcs(template.FuncMap{ - "formatNode": source.FormatNode, - "callArg": func(index int) ast.Expr { - if index >= len(args) { - return nil - } - return args[index] - }, - }) - var err error - tmpl, err = tmpl.Parse(result.template) - if err != nil { - return "", err - } - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, map[string]interface{}{ - "Data": result.data, - }) - return buf.String(), err -} diff --git a/vendor/gotest.tools/assert/result.go b/vendor/gotest.tools/assert/result.go deleted file mode 100644 index 3900264d..00000000 --- a/vendor/gotest.tools/assert/result.go +++ /dev/null @@ -1,107 +0,0 @@ -package assert - -import ( - "fmt" - "go/ast" - - "gotest.tools/assert/cmp" - "gotest.tools/internal/format" - "gotest.tools/internal/source" -) - -func runComparison( - t TestingT, - argSelector argSelector, - f cmp.Comparison, - msgAndArgs ...interface{}, -) bool { - if ht, ok := t.(helperT); ok { - ht.Helper() - } - result := f() - if result.Success() { - return true - } - - var message string - switch typed := result.(type) { - case resultWithComparisonArgs: - const stackIndex = 3 // Assert/Check, assert, runComparison - args, err := source.CallExprArgs(stackIndex) - if err != nil { - t.Log(err.Error()) - } - message = typed.FailureMessage(filterPrintableExpr(argSelector(args))) - case resultBasic: - message = typed.FailureMessage() - default: - message = fmt.Sprintf("comparison returned invalid Result type: %T", result) - } - - t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) - return false -} - -type resultWithComparisonArgs interface { - FailureMessage(args []ast.Expr) string -} - -type resultBasic interface { - FailureMessage() string -} - -// filterPrintableExpr filters the ast.Expr slice to only include Expr that are -// easy to read when printed and contain relevant information to an assertion. -// -// Ident and SelectorExpr are included because they print nicely and the variable -// names may provide additional context to their values. -// BasicLit and CompositeLit are excluded because their source is equivalent to -// their value, which is already available. -// Other types are ignored for now, but could be added if they are relevant. -func filterPrintableExpr(args []ast.Expr) []ast.Expr { - result := make([]ast.Expr, len(args)) - for i, arg := range args { - if isShortPrintableExpr(arg) { - result[i] = arg - continue - } - - if starExpr, ok := arg.(*ast.StarExpr); ok { - result[i] = starExpr.X - continue - } - result[i] = nil - } - return result -} - -func isShortPrintableExpr(expr ast.Expr) bool { - switch expr.(type) { - case *ast.Ident, *ast.SelectorExpr, *ast.IndexExpr, *ast.SliceExpr: - return true - case *ast.BinaryExpr, *ast.UnaryExpr: - return true - default: - // CallExpr, ParenExpr, TypeAssertExpr, KeyValueExpr, StarExpr - return false - } -} - -type argSelector func([]ast.Expr) []ast.Expr - -func argsAfterT(args []ast.Expr) []ast.Expr { - if len(args) < 1 { - return nil - } - return args[1:] -} - -func argsFromComparisonCall(args []ast.Expr) []ast.Expr { - if len(args) < 1 { - return nil - } - if callExpr, ok := args[1].(*ast.CallExpr); ok { - return callExpr.Args - } - return nil -} diff --git a/vendor/gotest.tools/internal/difflib/LICENSE b/vendor/gotest.tools/internal/difflib/LICENSE deleted file mode 100644 index c67dad61..00000000 --- a/vendor/gotest.tools/internal/difflib/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2013, Patrick Mezard -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. - The names of its contributors may not be used to endorse or promote -products derived from this software without specific prior written -permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/gotest.tools/internal/difflib/difflib.go b/vendor/gotest.tools/internal/difflib/difflib.go deleted file mode 100644 index 5efa99c1..00000000 --- a/vendor/gotest.tools/internal/difflib/difflib.go +++ /dev/null @@ -1,420 +0,0 @@ -/* Package difflib is a partial port of Python difflib module. - -Original source: https://github.com/pmezard/go-difflib - -This file is trimmed to only the parts used by this repository. -*/ -package difflib // import "gotest.tools/internal/difflib" - -func min(a, b int) int { - if a < b { - return a - } - return b -} - -func max(a, b int) int { - if a > b { - return a - } - return b -} - -type Match struct { - A int - B int - Size int -} - -type OpCode struct { - Tag byte - I1 int - I2 int - J1 int - J2 int -} - -// SequenceMatcher compares sequence of strings. The basic -// algorithm predates, and is a little fancier than, an algorithm -// published in the late 1980's by Ratcliff and Obershelp under the -// hyperbolic name "gestalt pattern matching". The basic idea is to find -// the longest contiguous matching subsequence that contains no "junk" -// elements (R-O doesn't address junk). The same idea is then applied -// recursively to the pieces of the sequences to the left and to the right -// of the matching subsequence. This does not yield minimal edit -// sequences, but does tend to yield matches that "look right" to people. -// -// SequenceMatcher tries to compute a "human-friendly diff" between two -// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the -// longest *contiguous* & junk-free matching subsequence. That's what -// catches peoples' eyes. The Windows(tm) windiff has another interesting -// notion, pairing up elements that appear uniquely in each sequence. -// That, and the method here, appear to yield more intuitive difference -// reports than does diff. This method appears to be the least vulnerable -// to synching up on blocks of "junk lines", though (like blank lines in -// ordinary text files, or maybe "

" lines in HTML files). That may be -// because this is the only method of the 3 that has a *concept* of -// "junk" . -// -// Timing: Basic R-O is cubic time worst case and quadratic time expected -// case. SequenceMatcher is quadratic time for the worst case and has -// expected-case behavior dependent in a complicated way on how many -// elements the sequences have in common; best case time is linear. -type SequenceMatcher struct { - a []string - b []string - b2j map[string][]int - IsJunk func(string) bool - autoJunk bool - bJunk map[string]struct{} - matchingBlocks []Match - fullBCount map[string]int - bPopular map[string]struct{} - opCodes []OpCode -} - -func NewMatcher(a, b []string) *SequenceMatcher { - m := SequenceMatcher{autoJunk: true} - m.SetSeqs(a, b) - return &m -} - -// Set two sequences to be compared. -func (m *SequenceMatcher) SetSeqs(a, b []string) { - m.SetSeq1(a) - m.SetSeq2(b) -} - -// Set the first sequence to be compared. The second sequence to be compared is -// not changed. -// -// SequenceMatcher computes and caches detailed information about the second -// sequence, so if you want to compare one sequence S against many sequences, -// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other -// sequences. -// -// See also SetSeqs() and SetSeq2(). -func (m *SequenceMatcher) SetSeq1(a []string) { - if &a == &m.a { - return - } - m.a = a - m.matchingBlocks = nil - m.opCodes = nil -} - -// Set the second sequence to be compared. The first sequence to be compared is -// not changed. -func (m *SequenceMatcher) SetSeq2(b []string) { - if &b == &m.b { - return - } - m.b = b - m.matchingBlocks = nil - m.opCodes = nil - m.fullBCount = nil - m.chainB() -} - -func (m *SequenceMatcher) chainB() { - // Populate line -> index mapping - b2j := map[string][]int{} - for i, s := range m.b { - indices := b2j[s] - indices = append(indices, i) - b2j[s] = indices - } - - // Purge junk elements - m.bJunk = map[string]struct{}{} - if m.IsJunk != nil { - junk := m.bJunk - for s, _ := range b2j { - if m.IsJunk(s) { - junk[s] = struct{}{} - } - } - for s, _ := range junk { - delete(b2j, s) - } - } - - // Purge remaining popular elements - popular := map[string]struct{}{} - n := len(m.b) - if m.autoJunk && n >= 200 { - ntest := n/100 + 1 - for s, indices := range b2j { - if len(indices) > ntest { - popular[s] = struct{}{} - } - } - for s, _ := range popular { - delete(b2j, s) - } - } - m.bPopular = popular - m.b2j = b2j -} - -func (m *SequenceMatcher) isBJunk(s string) bool { - _, ok := m.bJunk[s] - return ok -} - -// Find longest matching block in a[alo:ahi] and b[blo:bhi]. -// -// If IsJunk is not defined: -// -// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where -// alo <= i <= i+k <= ahi -// blo <= j <= j+k <= bhi -// and for all (i',j',k') meeting those conditions, -// k >= k' -// i <= i' -// and if i == i', j <= j' -// -// In other words, of all maximal matching blocks, return one that -// starts earliest in a, and of all those maximal matching blocks that -// start earliest in a, return the one that starts earliest in b. -// -// If IsJunk is defined, first the longest matching block is -// determined as above, but with the additional restriction that no -// junk element appears in the block. Then that block is extended as -// far as possible by matching (only) junk elements on both sides. So -// the resulting block never matches on junk except as identical junk -// happens to be adjacent to an "interesting" match. -// -// If no blocks match, return (alo, blo, 0). -func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match { - // CAUTION: stripping common prefix or suffix would be incorrect. - // E.g., - // ab - // acab - // Longest matching block is "ab", but if common prefix is - // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so - // strip, so ends up claiming that ab is changed to acab by - // inserting "ca" in the middle. That's minimal but unintuitive: - // "it's obvious" that someone inserted "ac" at the front. - // Windiff ends up at the same place as diff, but by pairing up - // the unique 'b's and then matching the first two 'a's. - besti, bestj, bestsize := alo, blo, 0 - - // find longest junk-free match - // during an iteration of the loop, j2len[j] = length of longest - // junk-free match ending with a[i-1] and b[j] - j2len := map[int]int{} - for i := alo; i != ahi; i++ { - // look at all instances of a[i] in b; note that because - // b2j has no junk keys, the loop is skipped if a[i] is junk - newj2len := map[int]int{} - for _, j := range m.b2j[m.a[i]] { - // a[i] matches b[j] - if j < blo { - continue - } - if j >= bhi { - break - } - k := j2len[j-1] + 1 - newj2len[j] = k - if k > bestsize { - besti, bestj, bestsize = i-k+1, j-k+1, k - } - } - j2len = newj2len - } - - // Extend the best by non-junk elements on each end. In particular, - // "popular" non-junk elements aren't in b2j, which greatly speeds - // the inner loop above, but also means "the best" match so far - // doesn't contain any junk *or* popular non-junk elements. - for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && - m.a[besti-1] == m.b[bestj-1] { - besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 - } - for besti+bestsize < ahi && bestj+bestsize < bhi && - !m.isBJunk(m.b[bestj+bestsize]) && - m.a[besti+bestsize] == m.b[bestj+bestsize] { - bestsize += 1 - } - - // Now that we have a wholly interesting match (albeit possibly - // empty!), we may as well suck up the matching junk on each - // side of it too. Can't think of a good reason not to, and it - // saves post-processing the (possibly considerable) expense of - // figuring out what to do with it. In the case of an empty - // interesting match, this is clearly the right thing to do, - // because no other kind of match is possible in the regions. - for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && - m.a[besti-1] == m.b[bestj-1] { - besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 - } - for besti+bestsize < ahi && bestj+bestsize < bhi && - m.isBJunk(m.b[bestj+bestsize]) && - m.a[besti+bestsize] == m.b[bestj+bestsize] { - bestsize += 1 - } - - return Match{A: besti, B: bestj, Size: bestsize} -} - -// Return list of triples describing matching subsequences. -// -// Each triple is of the form (i, j, n), and means that -// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in -// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are -// adjacent triples in the list, and the second is not the last triple in the -// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe -// adjacent equal blocks. -// -// The last triple is a dummy, (len(a), len(b), 0), and is the only -// triple with n==0. -func (m *SequenceMatcher) GetMatchingBlocks() []Match { - if m.matchingBlocks != nil { - return m.matchingBlocks - } - - var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match - matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match { - match := m.findLongestMatch(alo, ahi, blo, bhi) - i, j, k := match.A, match.B, match.Size - if match.Size > 0 { - if alo < i && blo < j { - matched = matchBlocks(alo, i, blo, j, matched) - } - matched = append(matched, match) - if i+k < ahi && j+k < bhi { - matched = matchBlocks(i+k, ahi, j+k, bhi, matched) - } - } - return matched - } - matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) - - // It's possible that we have adjacent equal blocks in the - // matching_blocks list now. - nonAdjacent := []Match{} - i1, j1, k1 := 0, 0, 0 - for _, b := range matched { - // Is this block adjacent to i1, j1, k1? - i2, j2, k2 := b.A, b.B, b.Size - if i1+k1 == i2 && j1+k1 == j2 { - // Yes, so collapse them -- this just increases the length of - // the first block by the length of the second, and the first - // block so lengthened remains the block to compare against. - k1 += k2 - } else { - // Not adjacent. Remember the first block (k1==0 means it's - // the dummy we started with), and make the second block the - // new block to compare against. - if k1 > 0 { - nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) - } - i1, j1, k1 = i2, j2, k2 - } - } - if k1 > 0 { - nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) - } - - nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0}) - m.matchingBlocks = nonAdjacent - return m.matchingBlocks -} - -// Return list of 5-tuples describing how to turn a into b. -// -// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple -// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the -// tuple preceding it, and likewise for j1 == the previous j2. -// -// The tags are characters, with these meanings: -// -// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] -// -// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. -// -// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. -// -// 'e' (equal): a[i1:i2] == b[j1:j2] -func (m *SequenceMatcher) GetOpCodes() []OpCode { - if m.opCodes != nil { - return m.opCodes - } - i, j := 0, 0 - matching := m.GetMatchingBlocks() - opCodes := make([]OpCode, 0, len(matching)) - for _, m := range matching { - // invariant: we've pumped out correct diffs to change - // a[:i] into b[:j], and the next matching block is - // a[ai:ai+size] == b[bj:bj+size]. So we need to pump - // out a diff to change a[i:ai] into b[j:bj], pump out - // the matching block, and move (i,j) beyond the match - ai, bj, size := m.A, m.B, m.Size - tag := byte(0) - if i < ai && j < bj { - tag = 'r' - } else if i < ai { - tag = 'd' - } else if j < bj { - tag = 'i' - } - if tag > 0 { - opCodes = append(opCodes, OpCode{tag, i, ai, j, bj}) - } - i, j = ai+size, bj+size - // the list of matching blocks is terminated by a - // sentinel with size 0 - if size > 0 { - opCodes = append(opCodes, OpCode{'e', ai, i, bj, j}) - } - } - m.opCodes = opCodes - return m.opCodes -} - -// Isolate change clusters by eliminating ranges with no changes. -// -// Return a generator of groups with up to n lines of context. -// Each group is in the same format as returned by GetOpCodes(). -func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { - if n < 0 { - n = 3 - } - codes := m.GetOpCodes() - if len(codes) == 0 { - codes = []OpCode{OpCode{'e', 0, 1, 0, 1}} - } - // Fixup leading and trailing groups if they show no changes. - if codes[0].Tag == 'e' { - c := codes[0] - i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 - codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} - } - if codes[len(codes)-1].Tag == 'e' { - c := codes[len(codes)-1] - i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 - codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} - } - nn := n + n - groups := [][]OpCode{} - group := []OpCode{} - for _, c := range codes { - i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 - // End the current group and start a new one whenever - // there is a large range with no changes. - if c.Tag == 'e' && i2-i1 > nn { - group = append(group, OpCode{c.Tag, i1, min(i2, i1+n), - j1, min(j2, j1+n)}) - groups = append(groups, group) - group = []OpCode{} - i1, j1 = max(i1, i2-n), max(j1, j2-n) - } - group = append(group, OpCode{c.Tag, i1, i2, j1, j2}) - } - if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { - groups = append(groups, group) - } - return groups -} diff --git a/vendor/gotest.tools/internal/format/diff.go b/vendor/gotest.tools/internal/format/diff.go deleted file mode 100644 index c938c97b..00000000 --- a/vendor/gotest.tools/internal/format/diff.go +++ /dev/null @@ -1,161 +0,0 @@ -package format - -import ( - "bytes" - "fmt" - "strings" - "unicode" - - "gotest.tools/internal/difflib" -) - -const ( - contextLines = 2 -) - -// DiffConfig for a unified diff -type DiffConfig struct { - A string - B string - From string - To string -} - -// UnifiedDiff is a modified version of difflib.WriteUnifiedDiff with better -// support for showing the whitespace differences. -func UnifiedDiff(conf DiffConfig) string { - a := strings.SplitAfter(conf.A, "\n") - b := strings.SplitAfter(conf.B, "\n") - groups := difflib.NewMatcher(a, b).GetGroupedOpCodes(contextLines) - if len(groups) == 0 { - return "" - } - - buf := new(bytes.Buffer) - writeFormat := func(format string, args ...interface{}) { - buf.WriteString(fmt.Sprintf(format, args...)) - } - writeLine := func(prefix string, s string) { - buf.WriteString(prefix + s) - } - if hasWhitespaceDiffLines(groups, a, b) { - writeLine = visibleWhitespaceLine(writeLine) - } - formatHeader(writeFormat, conf) - for _, group := range groups { - formatRangeLine(writeFormat, group) - for _, opCode := range group { - in, out := a[opCode.I1:opCode.I2], b[opCode.J1:opCode.J2] - switch opCode.Tag { - case 'e': - formatLines(writeLine, " ", in) - case 'r': - formatLines(writeLine, "-", in) - formatLines(writeLine, "+", out) - case 'd': - formatLines(writeLine, "-", in) - case 'i': - formatLines(writeLine, "+", out) - } - } - } - return buf.String() -} - -// hasWhitespaceDiffLines returns true if any diff groups is only different -// because of whitespace characters. -func hasWhitespaceDiffLines(groups [][]difflib.OpCode, a, b []string) bool { - for _, group := range groups { - in, out := new(bytes.Buffer), new(bytes.Buffer) - for _, opCode := range group { - if opCode.Tag == 'e' { - continue - } - for _, line := range a[opCode.I1:opCode.I2] { - in.WriteString(line) - } - for _, line := range b[opCode.J1:opCode.J2] { - out.WriteString(line) - } - } - if removeWhitespace(in.String()) == removeWhitespace(out.String()) { - return true - } - } - return false -} - -func removeWhitespace(s string) string { - var result []rune - for _, r := range s { - if !unicode.IsSpace(r) { - result = append(result, r) - } - } - return string(result) -} - -func visibleWhitespaceLine(ws func(string, string)) func(string, string) { - mapToVisibleSpace := func(r rune) rune { - switch r { - case '\n': - case ' ': - return '·' - case '\t': - return '▷' - case '\v': - return '▽' - case '\r': - return '↵' - case '\f': - return '↓' - default: - if unicode.IsSpace(r) { - return '�' - } - } - return r - } - return func(prefix, s string) { - ws(prefix, strings.Map(mapToVisibleSpace, s)) - } -} - -func formatHeader(wf func(string, ...interface{}), conf DiffConfig) { - if conf.From != "" || conf.To != "" { - wf("--- %s\n", conf.From) - wf("+++ %s\n", conf.To) - } -} - -func formatRangeLine(wf func(string, ...interface{}), group []difflib.OpCode) { - first, last := group[0], group[len(group)-1] - range1 := formatRangeUnified(first.I1, last.I2) - range2 := formatRangeUnified(first.J1, last.J2) - wf("@@ -%s +%s @@\n", range1, range2) -} - -// Convert range to the "ed" format -func formatRangeUnified(start, stop int) string { - // Per the diff spec at http://www.unix.org/single_unix_specification/ - beginning := start + 1 // lines start numbering with one - length := stop - start - if length == 1 { - return fmt.Sprintf("%d", beginning) - } - if length == 0 { - beginning-- // empty ranges begin at line just before the range - } - return fmt.Sprintf("%d,%d", beginning, length) -} - -func formatLines(writeLine func(string, string), prefix string, lines []string) { - for _, line := range lines { - writeLine(prefix, line) - } - // Add a newline if the last line is missing one so that the diff displays - // properly. - if !strings.HasSuffix(lines[len(lines)-1], "\n") { - writeLine("", "\n") - } -} diff --git a/vendor/gotest.tools/internal/format/format.go b/vendor/gotest.tools/internal/format/format.go deleted file mode 100644 index 8f6494f9..00000000 --- a/vendor/gotest.tools/internal/format/format.go +++ /dev/null @@ -1,27 +0,0 @@ -package format // import "gotest.tools/internal/format" - -import "fmt" - -// Message accepts a msgAndArgs varargs and formats it using fmt.Sprintf -func Message(msgAndArgs ...interface{}) string { - switch len(msgAndArgs) { - case 0: - return "" - case 1: - return fmt.Sprintf("%v", msgAndArgs[0]) - default: - return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) - } -} - -// WithCustomMessage accepts one or two messages and formats them appropriately -func WithCustomMessage(source string, msgAndArgs ...interface{}) string { - custom := Message(msgAndArgs...) - switch { - case custom == "": - return source - case source == "": - return custom - } - return fmt.Sprintf("%s: %s", source, custom) -} diff --git a/vendor/gotest.tools/internal/source/source.go b/vendor/gotest.tools/internal/source/source.go deleted file mode 100644 index a05933cc..00000000 --- a/vendor/gotest.tools/internal/source/source.go +++ /dev/null @@ -1,163 +0,0 @@ -package source // import "gotest.tools/internal/source" - -import ( - "bytes" - "fmt" - "go/ast" - "go/format" - "go/parser" - "go/token" - "os" - "runtime" - "strconv" - "strings" - - "github.com/pkg/errors" -) - -const baseStackIndex = 1 - -// FormattedCallExprArg returns the argument from an ast.CallExpr at the -// index in the call stack. The argument is formatted using FormatNode. -func FormattedCallExprArg(stackIndex int, argPos int) (string, error) { - args, err := CallExprArgs(stackIndex + 1) - if err != nil { - return "", err - } - return FormatNode(args[argPos]) -} - -func getNodeAtLine(filename string, lineNum int) (ast.Node, error) { - fileset := token.NewFileSet() - astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors) - if err != nil { - return nil, errors.Wrapf(err, "failed to parse source file: %s", filename) - } - - node := scanToLine(fileset, astFile, lineNum) - if node == nil { - return nil, errors.Errorf( - "failed to find an expression on line %d in %s", lineNum, filename) - } - return node, nil -} - -func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node { - v := &scanToLineVisitor{lineNum: lineNum, fileset: fileset} - ast.Walk(v, node) - return v.matchedNode -} - -type scanToLineVisitor struct { - lineNum int - matchedNode ast.Node - fileset *token.FileSet -} - -func (v *scanToLineVisitor) Visit(node ast.Node) ast.Visitor { - if node == nil || v.matchedNode != nil { - return nil - } - if v.nodePosition(node).Line == v.lineNum { - v.matchedNode = node - return nil - } - return v -} - -// In golang 1.9 the line number changed from being the line where the statement -// ended to the line where the statement began. -func (v *scanToLineVisitor) nodePosition(node ast.Node) token.Position { - if goVersionBefore19 { - return v.fileset.Position(node.End()) - } - return v.fileset.Position(node.Pos()) -} - -var goVersionBefore19 = isGOVersionBefore19() - -func isGOVersionBefore19() bool { - version := runtime.Version() - // not a release version - if !strings.HasPrefix(version, "go") { - return false - } - version = strings.TrimPrefix(version, "go") - parts := strings.Split(version, ".") - if len(parts) < 2 { - return false - } - minor, err := strconv.ParseInt(parts[1], 10, 32) - return err == nil && parts[0] == "1" && minor < 9 -} - -func getCallExprArgs(node ast.Node) ([]ast.Expr, error) { - visitor := &callExprVisitor{} - ast.Walk(visitor, node) - if visitor.expr == nil { - return nil, errors.New("failed to find call expression") - } - return visitor.expr.Args, nil -} - -type callExprVisitor struct { - expr *ast.CallExpr -} - -func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor { - if v.expr != nil || node == nil { - return nil - } - debug("visit (%T): %s", node, debugFormatNode{node}) - - if callExpr, ok := node.(*ast.CallExpr); ok { - v.expr = callExpr - return nil - } - return v -} - -// FormatNode using go/format.Node and return the result as a string -func FormatNode(node ast.Node) (string, error) { - buf := new(bytes.Buffer) - err := format.Node(buf, token.NewFileSet(), node) - return buf.String(), err -} - -// CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at -// the index in the call stack. -func CallExprArgs(stackIndex int) ([]ast.Expr, error) { - _, filename, lineNum, ok := runtime.Caller(baseStackIndex + stackIndex) - if !ok { - return nil, errors.New("failed to get call stack") - } - debug("call stack position: %s:%d", filename, lineNum) - - node, err := getNodeAtLine(filename, lineNum) - if err != nil { - return nil, err - } - debug("found node (%T): %s", node, debugFormatNode{node}) - - return getCallExprArgs(node) -} - -var debugEnabled = os.Getenv("GOTESTYOURSELF_DEBUG") != "" - -func debug(format string, args ...interface{}) { - if debugEnabled { - fmt.Fprintf(os.Stderr, "DEBUG: "+format+"\n", args...) - } -} - -type debugFormatNode struct { - ast.Node -} - -func (n debugFormatNode) String() string { - out, err := FormatNode(n.Node) - if err != nil { - return fmt.Sprintf("failed to format %s: %s", n.Node, err) - } - return out -} -- cgit From 56c6147eb6012252cf0b723b9eb6d1e841fc2f94 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Fri, 1 Feb 2019 12:22:00 +0100 Subject: identity: more refactoring progress --- bug/bug.go | 9 ++++ bug/identity.go | 27 +++++++++++ bug/operation_iterator_test.go | 13 ++++++ identity/common.go | 15 +++--- identity/identity.go | 54 ++++++--------------- identity/identity_stub.go | 85 ++++++++++++++++++++++++++++++++++ identity/identity_test.go | 33 +++++++++++++ identity/resolver.go | 22 +++++++++ misc/random_bugs/create_random_bugs.go | 38 ++++++++------- 9 files changed, 231 insertions(+), 65 deletions(-) create mode 100644 bug/identity.go create mode 100644 identity/identity_stub.go create mode 100644 identity/resolver.go diff --git a/bug/bug.go b/bug/bug.go index be3e2661..aec9a12e 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -6,6 +6,8 @@ import ( "fmt" "strings" + "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" @@ -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 } diff --git a/bug/identity.go b/bug/identity.go new file mode 100644 index 00000000..2eb2bcaf --- /dev/null +++ b/bug/identity.go @@ -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 +} diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go index e1aa8911..a41120e2 100644 --- a/bug/operation_iterator_test.go +++ b/bug/operation_iterator_test.go @@ -20,6 +20,19 @@ var ( labelChangeOp = NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"}) ) +func ExampleOperationIterator() { + b := NewBug() + + // add operations + + it := NewOperationIterator(b) + + for it.Next() { + // do something with each operations + _ = it.Value() + } +} + func TestOpIterator(t *testing.T) { mockRepo := repository.NewMockRepoForTest() diff --git a/identity/common.go b/identity/common.go index 5301471a..00feaa2d 100644 --- a/identity/common.go +++ b/identity/common.go @@ -23,12 +23,13 @@ func (e ErrMultipleMatch) Error() string { // // If the given message has a "id" field, it's considered being a proper Identity. func UnmarshalJSON(raw json.RawMessage) (Interface, error) { - // First try to decode as a normal Identity - var i Identity + aux := &IdentityStub{} - err := json.Unmarshal(raw, &i) - if err == nil && i.id != "" { - return &i, nil + // First try to decode and load as a normal Identity + err := json.Unmarshal(raw, &aux) + if err == nil && aux.Id() != "" { + return aux, nil + // return identityResolver.ResolveIdentity(aux.Id) } // abort if we have an error other than the wrong type @@ -51,7 +52,3 @@ func UnmarshalJSON(raw json.RawMessage) (Interface, error) { return nil, fmt.Errorf("unknown identity type") } - -type Resolver interface { - ResolveIdentity(id string) (Interface, error) -} diff --git a/identity/identity.go b/identity/identity.go index 2a422789..2dafb353 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -48,14 +48,10 @@ func NewIdentityFull(name string, email string, login string, avatarUrl string) } } -type identityJSON struct { - Id string `json:"id"` -} - // MarshalJSON will only serialize the id func (i *Identity) MarshalJSON() ([]byte, error) { - return json.Marshal(identityJSON{ - Id: i.Id(), + return json.Marshal(&IdentityStub{ + id: i.Id(), }) } @@ -63,35 +59,12 @@ func (i *Identity) MarshalJSON() ([]byte, error) { // Users of this package are expected to run Load() to load // the remaining data from the identities data in git. func (i *Identity) UnmarshalJSON(data []byte) error { - aux := identityJSON{} - - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - i.id = aux.Id - - return nil + panic("identity should be loaded with identity.UnmarshalJSON") } // Read load an Identity from the identities data available in git func Read(repo repository.Repo, id string) (*Identity, error) { - i := &Identity{ - id: id, - } - - err := i.Load(repo) - if err != nil { - return nil, err - } - - return i, nil -} - -// Load will read the corresponding identity data from git and replace any -// data already loaded if any. -func (i *Identity) Load(repo repository.Repo) error { - ref := fmt.Sprintf("%s%s", identityRefPattern, i.Id()) + ref := fmt.Sprintf("%s%s", identityRefPattern, id) hashes, err := repo.ListCommits(ref) @@ -99,35 +72,35 @@ func (i *Identity) Load(repo repository.Repo) error { // TODO: this is not perfect, it might be a command invoke error if err != nil { - return ErrIdentityNotExist + return nil, ErrIdentityNotExist } for _, hash := range hashes { entries, err := repo.ListEntries(hash) if err != nil { - return errors.Wrap(err, "can't list git tree entries") + return nil, errors.Wrap(err, "can't list git tree entries") } if len(entries) != 1 { - return fmt.Errorf("invalid identity data at hash %s", hash) + return nil, fmt.Errorf("invalid identity data at hash %s", hash) } entry := entries[0] if entry.Name != versionEntryName { - return fmt.Errorf("invalid identity data at hash %s", hash) + return nil, fmt.Errorf("invalid identity data at hash %s", hash) } data, err := repo.ReadData(entry.Hash) if err != nil { - return errors.Wrap(err, "failed to read git blob data") + return nil, errors.Wrap(err, "failed to read git blob data") } var version Version err = json.Unmarshal(data, &version) if err != nil { - return errors.Wrapf(err, "failed to decode Identity version json %s", hash) + return nil, errors.Wrapf(err, "failed to decode Identity version json %s", hash) } // tag the version with the commit hash @@ -136,9 +109,10 @@ func (i *Identity) Load(repo repository.Repo) error { versions = append(versions, &version) } - i.Versions = versions - - return nil + return &Identity{ + id: id, + Versions: versions, + }, nil } // NewFromGitUser will query the repository for user detail and diff --git a/identity/identity_stub.go b/identity/identity_stub.go new file mode 100644 index 00000000..0163e9d4 --- /dev/null +++ b/identity/identity_stub.go @@ -0,0 +1,85 @@ +package identity + +import ( + "encoding/json" + + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" +) + +var _ Interface = &IdentityStub{} + +// IdentityStub is an almost empty Identity, holding only the id. +// When a normal Identity is serialized into JSON, only the id is serialized. +// All the other data are stored in git in a chain of commit + a ref. +// When this JSON is deserialized, an IdentityStub is returned instead, to be replaced +// later by the proper Identity, loaded from the Repo. +type IdentityStub struct { + id string +} + +func (i *IdentityStub) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Id string `json:"id"` + }{ + Id: i.id, + }) +} + +func (i *IdentityStub) UnmarshalJSON(data []byte) error { + aux := struct { + Id string `json:"id"` + }{} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + i.id = aux.Id + + return nil +} + +func (i *IdentityStub) Id() string { + return i.id +} + +func (IdentityStub) Name() string { + panic("identities needs to be properly loaded with identity.Read()") +} + +func (IdentityStub) Email() string { + panic("identities needs to be properly loaded with identity.Read()") +} + +func (IdentityStub) Login() string { + panic("identities needs to be properly loaded with identity.Read()") +} + +func (IdentityStub) AvatarUrl() string { + panic("identities needs to be properly loaded with identity.Read()") +} + +func (IdentityStub) Keys() []Key { + panic("identities needs to be properly loaded with identity.Read()") +} + +func (IdentityStub) ValidKeysAtTime(time lamport.Time) []Key { + panic("identities needs to be properly loaded with identity.Read()") +} + +func (IdentityStub) DisplayName() string { + panic("identities needs to be properly loaded with identity.Read()") +} + +func (IdentityStub) Validate() error { + panic("identities needs to be properly loaded with identity.Read()") +} + +func (IdentityStub) Commit(repo repository.Repo) error { + panic("identities needs to be properly loaded with identity.Read()") +} + +func (IdentityStub) IsProtected() bool { + panic("identities needs to be properly loaded with identity.Read()") +} diff --git a/identity/identity_test.go b/identity/identity_test.go index afb804fc..f1c07e79 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -1,6 +1,7 @@ package identity import ( + "encoding/json" "testing" "github.com/MichaelMure/git-bug/repository" @@ -220,3 +221,35 @@ func assertHasKeyValue(t *testing.T, metadata map[string]string, key, value stri assert.True(t, ok) assert.Equal(t, val, value) } + +func TestJSON(t *testing.T) { + mockRepo := repository.NewMockRepoForTest() + + identity := &Identity{ + Versions: []*Version{ + { + Name: "René Descartes", + Email: "rene.descartes@example.com", + }, + }, + } + + // commit to make sure we have an ID + err := identity.Commit(mockRepo) + assert.Nil(t, err) + assert.NotEmpty(t, identity.id) + + // serialize + data, err := json.Marshal(identity) + assert.NoError(t, err) + + // deserialize, got a IdentityStub with the same id + var i Interface + i, err = UnmarshalJSON(data) + assert.NoError(t, err) + assert.Equal(t, identity.id, i.Id()) + + // make sure we can load the identity properly + i, err = Read(mockRepo, i.Id()) + assert.NoError(t, err) +} diff --git a/identity/resolver.go b/identity/resolver.go new file mode 100644 index 00000000..63dc994f --- /dev/null +++ b/identity/resolver.go @@ -0,0 +1,22 @@ +package identity + +import "github.com/MichaelMure/git-bug/repository" + +// Resolver define the interface of an Identity resolver, able to load +// an identity from, for example, a repo or a cache. +type Resolver interface { + ResolveIdentity(id string) (Interface, error) +} + +// DefaultResolver is a Resolver loading Identities directly from a Repo +type SimpleResolver struct { + repo repository.Repo +} + +func NewSimpleResolver(repo repository.Repo) *SimpleResolver { + return &SimpleResolver{repo: repo} +} + +func (r *SimpleResolver) ResolveIdentity(id string) (Interface, error) { + return Read(r.repo, id) +} diff --git a/misc/random_bugs/create_random_bugs.go b/misc/random_bugs/create_random_bugs.go index 085e89f0..0657c808 100644 --- a/misc/random_bugs/create_random_bugs.go +++ b/misc/random_bugs/create_random_bugs.go @@ -34,7 +34,9 @@ func CommitRandomBugs(repo repository.ClockedRepo, opts Options) { } func CommitRandomBugsWithSeed(repo repository.ClockedRepo, opts Options, seed int64) { - bugs := GenerateRandomBugsWithSeed(opts, seed) + generateRandomPersons(repo, opts.PersonNumber) + + bugs := generateRandomBugsWithSeed(opts, seed) for _, b := range bugs { err := b.Commit(repo) @@ -44,11 +46,7 @@ func CommitRandomBugsWithSeed(repo repository.ClockedRepo, opts Options, seed in } } -func GenerateRandomBugs(opts Options) []*bug.Bug { - return GenerateRandomBugsWithSeed(opts, time.Now().UnixNano()) -} - -func GenerateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug { +func generateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug { rand.Seed(seed) fake.Seed(seed) @@ -67,7 +65,7 @@ func GenerateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug { addedLabels = []string{} b, _, err := bug.Create( - randomPerson(opts.PersonNumber), + randomPerson(), time.Now().Unix(), fake.Sentence(), paragraphs(), @@ -85,7 +83,7 @@ func GenerateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug { for j := 0; j < nOps; j++ { index := rand.Intn(len(opsGenerators)) - opsGenerators[index](b, randomPerson(opts.PersonNumber)) + opsGenerators[index](b, randomPerson()) } result[i] = b @@ -101,6 +99,9 @@ func GenerateRandomOperationPacks(packNumber int, opNumber int) []*bug.Operation func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int64) []*bug.OperationPack { // Note: this is a bit crude, only generate a Create + Comments + panic("this piece of code needs to be updated to make sure that the identities " + + "are properly commit before usage. That is, generateRandomPersons() need to be called.") + rand.Seed(seed) fake.Seed(seed) @@ -112,7 +113,7 @@ func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int var op bug.Operation op = bug.NewCreateOp( - randomPerson(5), + randomPerson(), time.Now().Unix(), fake.Sentence(), paragraphs(), @@ -123,7 +124,7 @@ func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int for j := 0; j < opNumber-1; j++ { op = bug.NewAddCommentOp( - randomPerson(5), + randomPerson(), time.Now().Unix(), paragraphs(), nil, @@ -143,15 +144,20 @@ func person() identity.Interface { var persons []identity.Interface -func randomPerson(personNumber int) identity.Interface { - if len(persons) == 0 { - persons = make([]identity.Interface, personNumber) - for i := range persons { - persons[i] = person() +func generateRandomPersons(repo repository.ClockedRepo, n int) { + persons = make([]identity.Interface, n) + for i := range persons { + p := person() + err := p.Commit(repo) + if err != nil { + panic(err) } + persons[i] = p } +} - index := rand.Intn(personNumber) +func randomPerson() identity.Interface { + index := rand.Intn(len(persons)) return persons[index] } -- cgit From 328a4e5abf3ec8ea41f89575fcfb83cf9f086c80 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 3 Feb 2019 19:55:35 +0100 Subject: identity: wip push/pull --- bug/bug.go | 4 +- bug/bug_actions.go | 12 ++- cache/repo_cache.go | 2 +- commands/id.go | 2 +- identity/identity.go | 24 ++++- identity/identity_actions.go | 211 +++++++++++++++++++++++++++++++++++++++++++ identity/identity_stub.go | 20 ++-- identity/identity_test.go | 10 +- identity/resolver.go | 2 +- identity/version.go | 2 + 10 files changed, 262 insertions(+), 27 deletions(-) create mode 100644 identity/identity_actions.go diff --git a/bug/bug.go b/bug/bug.go index aec9a12e..1717dd66 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -6,12 +6,12 @@ import ( "fmt" "strings" - "github.com/MichaelMure/git-bug/identity" + "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/" diff --git a/bug/bug_actions.go b/bug/bug_actions.go index a21db826..6b9135b0 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" ) -// Fetch retrieve update from a remote +// Fetch retrieve updates from a remote // This does not change the local bugs state func Fetch(repo repository.Repo, remote string) (string, error) { remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote) @@ -23,7 +23,7 @@ func Push(repo repository.Repo, remote string) (string, error) { } // Pull will do a Fetch + MergeAll -// This function won't give details on the underlying process. If you need more +// This function won't give details on the underlying process. If you need more, // use Fetch and MergeAll separately. func Pull(repo repository.ClockedRepo, remote string) error { _, err := Fetch(repo, remote) @@ -45,7 +45,13 @@ func Pull(repo repository.ClockedRepo, remote string) error { 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) diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 3d8b352b..f207e984 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -555,7 +555,7 @@ func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) { return cached, nil } - i, err := identity.Read(c.repo, id) + i, err := identity.ReadLocal(c.repo, id) if err != nil { return nil, err } diff --git a/commands/id.go b/commands/id.go index 19d040ee..7eacd986 100644 --- a/commands/id.go +++ b/commands/id.go @@ -17,7 +17,7 @@ func runId(cmd *cobra.Command, args []string) error { var err error if len(args) == 1 { - id, err = identity.Read(repo, args[0]) + id, err = identity.ReadLocal(repo, args[0]) } else { id, err = identity.GetUserIdentity(repo) } diff --git a/identity/identity.go b/identity/identity.go index 2dafb353..3877e346 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -4,14 +4,17 @@ package identity import ( "encoding/json" "fmt" + "strings" + + "github.com/pkg/errors" "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 identityRefPattern = "refs/identities/" +const identityRemoteRefPattern = "refs/remotes/%s/identities/" const versionEntryName = "version" const identityConfigKey = "git-bug.identity" @@ -62,9 +65,22 @@ func (i *Identity) UnmarshalJSON(data []byte) error { panic("identity should be loaded with identity.UnmarshalJSON") } -// Read load an Identity from the identities data available in git -func Read(repo repository.Repo, id string) (*Identity, error) { +// ReadLocal load a local Identity from the identities data available in git +func ReadLocal(repo repository.Repo, id string) (*Identity, error) { ref := fmt.Sprintf("%s%s", identityRefPattern, id) + return read(repo, ref) +} + +// ReadRemote load a remote Identity from the identities data available in git +func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, error) { + ref := fmt.Sprintf(identityRemoteRefPattern, remote) + id + return read(repo, ref) +} + +// read will load and parse an identity frdm git +func read(repo repository.Repo, ref string) (*Identity, error) { + refSplit := strings.Split(ref, "/") + id := refSplit[len(refSplit)-1] hashes, err := repo.ListCommits(ref) @@ -162,7 +178,7 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) { id = val } - return Read(repo, id) + return ReadLocal(repo, id) } func (i *Identity) AddVersion(version *Version) { diff --git a/identity/identity_actions.go b/identity/identity_actions.go new file mode 100644 index 00000000..69f77a2b --- /dev/null +++ b/identity/identity_actions.go @@ -0,0 +1,211 @@ +package identity + +import ( + "fmt" + "strings" + + "github.com/MichaelMure/git-bug/repository" + "github.com/pkg/errors" +) + +// Fetch retrieve updates from a remote +// This does not change the local identities state +func Fetch(repo repository.Repo, remote string) (string, error) { + remoteRefSpec := fmt.Sprintf(identityRemoteRefPattern, remote) + fetchRefSpec := fmt.Sprintf("%s:%s*", identityRefPattern, remoteRefSpec) + + return repo.FetchRefs(remote, fetchRefSpec) +} + +// Push update a remote with the local changes +func Push(repo repository.Repo, remote string) (string, error) { + return repo.PushRefs(remote, identityRefPattern+"*") +} + +// 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. +func Pull(repo repository.ClockedRepo, remote string) error { + _, err := Fetch(repo, remote) + if err != nil { + return err + } + + for merge := range MergeAll(repo, remote) { + if merge.Err != nil { + return merge.Err + } + if merge.Status == MergeStatusInvalid { + // Not awesome: simply output the merge failure here as this function + // is only used in tests for now. + fmt.Println(merge) + } + } + + return nil +} + +// MergeAll will merge all the available remote identity +// To make sure that an Identity history can't be altered, a strict fast-forward +// only policy is applied here. As an Identity should be tied to a single user, this +// should work in practice but it does leave a possibility that a user would edit his +// Identity from two different repo concurrently and push the changes in a non-centralized +// network of repositories. In this case, it would result some of the repo accepting one +// version, some other accepting another, preventing the network in general to converge +// to the same result. This would create a sort of partition of the network, and manual +// cleaning would be required. +// +// An alternative approach would be to have a determinist rebase: +// - any commits present in both local and remote version would be kept, never changed. +// - newer commits would be merged in a linear chain of commits, ordered based on the +// Lamport time +// +// However, this approach leave the possibility, in the case of a compromised crypto keys, +// of forging a new version with a bogus Lamport time to be inserted before a legit version, +// invalidating the correct version and hijacking the Identity. There would only be a short +// period of time where this would be possible (before the network converge) but I'm not +// confident enough to implement that. I choose the strict fast-forward only approach, +// despite it's potential problem with two different version as mentioned above. +func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { + out := make(chan MergeResult) + + go func() { + defer close(out) + + remoteRefSpec := fmt.Sprintf(identityRemoteRefPattern, remote) + remoteRefs, err := repo.ListRefs(remoteRefSpec) + + if err != nil { + out <- MergeResult{Err: err} + return + } + + for _, remoteRef := range remoteRefs { + refSplitted := strings.Split(remoteRef, "/") + id := refSplitted[len(refSplitted)-1] + + remoteIdentity, err := ReadLocal(repo, remoteRef) + remoteBug, err := readBug(repo, remoteRef) + + if err != nil { + out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error()) + continue + } + + // Check for error in remote data + if err := remoteBug.Validate(); err != nil { + out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error()) + continue + } + + localRef := bugsRefPattern + remoteBug.Id() + localExist, err := repo.RefExist(localRef) + + if err != nil { + out <- newMergeError(err, id) + continue + } + + // the bug is not local yet, simply create the reference + if !localExist { + err := repo.CopyRef(remoteRef, localRef) + + if err != nil { + out <- newMergeError(err, id) + return + } + + out <- newMergeStatus(MergeStatusNew, id, remoteBug) + continue + } + + localBug, err := readBug(repo, localRef) + + if err != nil { + out <- newMergeError(errors.Wrap(err, "local bug is not readable"), id) + return + } + + updated, err := localBug.Merge(repo, remoteBug) + + if err != nil { + out <- newMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error()) + return + } + + if updated { + out <- newMergeStatus(MergeStatusUpdated, id, localBug) + } else { + out <- newMergeStatus(MergeStatusNothing, id, localBug) + } + } + }() + + return out +} + +// MergeStatus represent the result of a merge operation of a bug +type MergeStatus int + +const ( + _ MergeStatus = iota + MergeStatusNew + MergeStatusInvalid + MergeStatusUpdated + MergeStatusNothing +) + +// Todo: share a generalized MergeResult with the bug package ? +type MergeResult struct { + // Err is set when a terminal error occur in the process + Err error + + Id string + Status MergeStatus + + // Only set for invalid status + Reason string + + // Not set for invalid status + Identity *Identity +} + +func (mr MergeResult) String() string { + switch mr.Status { + case MergeStatusNew: + return "new" + case MergeStatusInvalid: + return fmt.Sprintf("invalid data: %s", mr.Reason) + case MergeStatusUpdated: + return "updated" + case MergeStatusNothing: + return "nothing to do" + default: + panic("unknown merge status") + } +} + +func newMergeError(err error, id string) MergeResult { + return MergeResult{ + Err: err, + Id: id, + } +} + +func newMergeStatus(status MergeStatus, id string, identity *Identity) MergeResult { + return MergeResult{ + Id: id, + Status: status, + + // Identity is not set for an invalid merge result + Identity: identity, + } +} + +func newMergeInvalidStatus(id string, reason string) MergeResult { + return MergeResult{ + Id: id, + Status: MergeStatusInvalid, + Reason: reason, + } +} diff --git a/identity/identity_stub.go b/identity/identity_stub.go index 0163e9d4..6788ce33 100644 --- a/identity/identity_stub.go +++ b/identity/identity_stub.go @@ -45,41 +45,41 @@ func (i *IdentityStub) Id() string { } func (IdentityStub) Name() string { - panic("identities needs to be properly loaded with identity.Read()") + panic("identities needs to be properly loaded with identity.ReadLocal()") } func (IdentityStub) Email() string { - panic("identities needs to be properly loaded with identity.Read()") + panic("identities needs to be properly loaded with identity.ReadLocal()") } func (IdentityStub) Login() string { - panic("identities needs to be properly loaded with identity.Read()") + panic("identities needs to be properly loaded with identity.ReadLocal()") } func (IdentityStub) AvatarUrl() string { - panic("identities needs to be properly loaded with identity.Read()") + panic("identities needs to be properly loaded with identity.ReadLocal()") } func (IdentityStub) Keys() []Key { - panic("identities needs to be properly loaded with identity.Read()") + panic("identities needs to be properly loaded with identity.ReadLocal()") } func (IdentityStub) ValidKeysAtTime(time lamport.Time) []Key { - panic("identities needs to be properly loaded with identity.Read()") + panic("identities needs to be properly loaded with identity.ReadLocal()") } func (IdentityStub) DisplayName() string { - panic("identities needs to be properly loaded with identity.Read()") + panic("identities needs to be properly loaded with identity.ReadLocal()") } func (IdentityStub) Validate() error { - panic("identities needs to be properly loaded with identity.Read()") + panic("identities needs to be properly loaded with identity.ReadLocal()") } func (IdentityStub) Commit(repo repository.Repo) error { - panic("identities needs to be properly loaded with identity.Read()") + panic("identities needs to be properly loaded with identity.ReadLocal()") } func (IdentityStub) IsProtected() bool { - panic("identities needs to be properly loaded with identity.Read()") + panic("identities needs to be properly loaded with identity.ReadLocal()") } diff --git a/identity/identity_test.go b/identity/identity_test.go index f1c07e79..3ab49d76 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -29,7 +29,7 @@ func TestIdentityCommitLoad(t *testing.T) { assert.Nil(t, err) assert.NotEmpty(t, identity.id) - loaded, err := Read(mockRepo, identity.id) + loaded, err := ReadLocal(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) equivalentIdentity(t, identity, loaded) @@ -70,7 +70,7 @@ func TestIdentityCommitLoad(t *testing.T) { assert.Nil(t, err) assert.NotEmpty(t, identity.id) - loaded, err = Read(mockRepo, identity.id) + loaded, err = ReadLocal(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) equivalentIdentity(t, identity, loaded) @@ -100,7 +100,7 @@ func TestIdentityCommitLoad(t *testing.T) { assert.Nil(t, err) assert.NotEmpty(t, identity.id) - loaded, err = Read(mockRepo, identity.id) + loaded, err = ReadLocal(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) equivalentIdentity(t, identity, loaded) @@ -209,7 +209,7 @@ func TestMetadata(t *testing.T) { assert.NoError(t, err) // reload - loaded, err := Read(mockRepo, identity.id) + loaded, err := ReadLocal(mockRepo, identity.id) assert.Nil(t, err) assertHasKeyValue(t, loaded.ImmutableMetadata(), "key1", "value1") @@ -250,6 +250,6 @@ func TestJSON(t *testing.T) { assert.Equal(t, identity.id, i.Id()) // make sure we can load the identity properly - i, err = Read(mockRepo, i.Id()) + i, err = ReadLocal(mockRepo, i.Id()) assert.NoError(t, err) } diff --git a/identity/resolver.go b/identity/resolver.go index 63dc994f..7facfc0c 100644 --- a/identity/resolver.go +++ b/identity/resolver.go @@ -18,5 +18,5 @@ func NewSimpleResolver(repo repository.Repo) *SimpleResolver { } func (r *SimpleResolver) ResolveIdentity(id string) (Interface, error) { - return Read(r.repo, id) + return ReadLocal(r.repo, id) } diff --git a/identity/version.go b/identity/version.go index d4afc893..f8b9cc73 100644 --- a/identity/version.go +++ b/identity/version.go @@ -20,6 +20,8 @@ type Version struct { // Not serialized commitHash git.Hash + // Todo: add unix timestamp for ordering with identical lamport time ? + // The lamport time at which this version become effective // The reference time is the bug edition lamport clock Time lamport.Time -- cgit From 21048e785d976a04e26798e4a385ee675c95b88f Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Wed, 6 Feb 2019 22:06:42 +0100 Subject: identity: wip --- bug/bug.go | 4 +- bug/bug_test.go | 36 ++++++---- identity/identity.go | 162 ++++++++++++++++++++++++++++++------------- identity/identity_actions.go | 43 +++--------- identity/identity_test.go | 119 +++++++++++++++---------------- identity/version.go | 78 ++++++++++----------- 6 files changed, 242 insertions(+), 200 deletions(-) diff --git a/bug/bug.go b/bug/bug.go index 1717dd66..f84753fa 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -459,6 +459,7 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { return err } + bug.staging.commitHash = hash bug.packs = append(bug.packs, bug.staging) bug.staging = OperationPack{} @@ -513,9 +514,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 diff --git a/bug/bug_test.go b/bug/bug_test.go index 41a5b03d..001bfc56 100644 --- a/bug/bug_test.go +++ b/bug/bug_test.go @@ -55,7 +55,7 @@ func TestBugValidity(t *testing.T) { } } -func TestBugSerialisation(t *testing.T) { +func TestBugCommitLoad(t *testing.T) { bug1 := NewBug() bug1.Append(createOp) @@ -69,22 +69,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 + + 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)) - // check hashes - for i := range bug1.packs[0].Operations { - if !bug2.packs[0].Operations[i].base().hash.IsValid() { - t.Fatal("invalid hash") + 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 } } - assert.Equal(t, bug1, bug2) + assert.Equal(t, expected, actual) } diff --git a/identity/identity.go b/identity/identity.go index 3877e346..59973489 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -21,17 +21,22 @@ const identityConfigKey = "git-bug.identity" var _ Interface = &Identity{} type Identity struct { - id string - Versions []*Version + // Id used as unique identifier + id string + + lastCommit git.Hash + + // all the successive version of the identity + versions []*Version } func NewIdentity(name string, email string) *Identity { return &Identity{ - Versions: []*Version{ + versions: []*Version{ { - Name: name, - Email: email, - Nonce: makeNonce(20), + name: name, + email: email, + nonce: makeNonce(20), }, }, } @@ -39,13 +44,13 @@ func NewIdentity(name string, email string) *Identity { func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity { return &Identity{ - Versions: []*Version{ + versions: []*Version{ { - Name: name, - Email: email, - Login: login, - AvatarUrl: avatarUrl, - Nonce: makeNonce(20), + name: name, + email: email, + login: login, + avatarURL: avatarUrl, + nonce: makeNonce(20), }, }, } @@ -84,13 +89,15 @@ func read(repo repository.Repo, ref string) (*Identity, error) { hashes, err := repo.ListCommits(ref) - var versions []*Version - // TODO: this is not perfect, it might be a command invoke error if err != nil { return nil, ErrIdentityNotExist } + i := &Identity{ + id: id, + } + for _, hash := range hashes { entries, err := repo.ListEntries(hash) if err != nil { @@ -121,14 +128,12 @@ func read(repo repository.Repo, ref string) (*Identity, error) { // tag the version with the commit hash version.commitHash = hash + i.lastCommit = hash - versions = append(versions, &version) + i.versions = append(i.versions, &version) } - return &Identity{ - id: id, - Versions: versions, - }, nil + return i, nil } // NewFromGitUser will query the repository for user detail and @@ -182,7 +187,7 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) { } func (i *Identity) AddVersion(version *Version) { - i.Versions = append(i.Versions, version) + i.versions = append(i.versions, version) } // Write the identity into the Repository. In particular, this ensure that @@ -190,11 +195,9 @@ func (i *Identity) AddVersion(version *Version) { func (i *Identity) Commit(repo repository.Repo) error { // Todo: check for mismatch between memory and commited data - var lastCommit git.Hash = "" - - for _, v := range i.Versions { + for _, v := range i.versions { if v.commitHash != "" { - lastCommit = v.commitHash + i.lastCommit = v.commitHash // ignore already commited versions continue } @@ -215,8 +218,8 @@ func (i *Identity) Commit(repo repository.Repo) error { } var commitHash git.Hash - if lastCommit != "" { - commitHash, err = repo.StoreCommitWithParent(treeHash, lastCommit) + if i.lastCommit != "" { + commitHash, err = repo.StoreCommitWithParent(treeHash, i.lastCommit) } else { commitHash, err = repo.StoreCommit(treeHash) } @@ -225,7 +228,8 @@ func (i *Identity) Commit(repo repository.Repo) error { return err } - lastCommit = commitHash + i.lastCommit = commitHash + v.commitHash = commitHash // if it was the first commit, use the commit hash as the Identity id if i.id == "" { @@ -238,7 +242,7 @@ func (i *Identity) Commit(repo repository.Repo) error { } ref := fmt.Sprintf("%s%s", identityRefPattern, i.id) - err := repo.UpdateRef(ref, lastCommit) + err := repo.UpdateRef(ref, i.lastCommit) if err != nil { return err @@ -247,31 +251,93 @@ func (i *Identity) Commit(repo repository.Repo) error { return nil } +// Merge will merge a different version of the same Identity +// +// To make sure that an Identity history can't be altered, a strict fast-forward +// only policy is applied here. As an Identity should be tied to a single user, this +// should work in practice but it does leave a possibility that a user would edit his +// Identity from two different repo concurrently and push the changes in a non-centralized +// network of repositories. In this case, it would result in some of the repo accepting one +// version and some other accepting another, preventing the network in general to converge +// to the same result. This would create a sort of partition of the network, and manual +// cleaning would be required. +// +// An alternative approach would be to have a determinist rebase: +// - any commits present in both local and remote version would be kept, never changed. +// - newer commits would be merged in a linear chain of commits, ordered based on the +// Lamport time +// +// However, this approach leave the possibility, in the case of a compromised crypto keys, +// of forging a new version with a bogus Lamport time to be inserted before a legit version, +// invalidating the correct version and hijacking the Identity. There would only be a short +// period of time where this would be possible (before the network converge) but I'm not +// confident enough to implement that. I choose the strict fast-forward only approach, +// despite it's potential problem with two different version as mentioned above. +func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) { + if i.id != other.id { + return false, errors.New("merging unrelated identities is not supported") + } + + if i.lastCommit == "" || other.lastCommit == "" { + return false, errors.New("can't merge identities that has never been stored") + } + + /*ancestor, err := repo.FindCommonAncestor(i.lastCommit, other.lastCommit) + if err != nil { + return false, errors.Wrap(err, "can't find common ancestor") + }*/ + + modified := false + for j, otherVersion := range other.versions { + // if there is more version in other, take them + if len(i.versions) == j { + i.versions = append(i.versions, otherVersion) + i.lastCommit = otherVersion.commitHash + modified = true + } + + // we have a non fast-forward merge. + // as explained in the doc above, refusing to merge + if i.versions[j].commitHash != otherVersion.commitHash { + return false, errors.New("non fast-forward identity merge") + } + } + + if modified { + err := repo.UpdateRef(identityRefPattern+i.id, i.lastCommit) + if err != nil { + return false, err + } + } + + return false, nil +} + // Validate check if the Identity data is valid func (i *Identity) Validate() error { lastTime := lamport.Time(0) - for _, v := range i.Versions { + for _, v := range i.versions { if err := v.Validate(); err != nil { return err } - if v.Time < lastTime { - return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.Time) + if v.time < lastTime { + return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.time) } - lastTime = v.Time + lastTime = v.time } return nil } func (i *Identity) lastVersion() *Version { - if len(i.Versions) <= 0 { + if len(i.versions) <= 0 { panic("no version at all") } - return i.Versions[len(i.Versions)-1] + return i.versions[len(i.versions)-1] } // Id return the Identity identifier @@ -286,27 +352,27 @@ func (i *Identity) Id() string { // Name return the last version of the name func (i *Identity) Name() string { - return i.lastVersion().Name + return i.lastVersion().name } // Email return the last version of the email func (i *Identity) Email() string { - return i.lastVersion().Email + return i.lastVersion().email } // Login return the last version of the login func (i *Identity) Login() string { - return i.lastVersion().Login + return i.lastVersion().login } -// Login return the last version of the Avatar URL +// AvatarUrl return the last version of the Avatar URL func (i *Identity) AvatarUrl() string { - return i.lastVersion().AvatarUrl + return i.lastVersion().avatarURL } -// Login return the last version of the valid keys +// Keys return the last version of the valid keys func (i *Identity) Keys() []Key { - return i.lastVersion().Keys + return i.lastVersion().keys } // IsProtected return true if the chain of git commits started to be signed. @@ -320,12 +386,12 @@ func (i *Identity) IsProtected() bool { func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key { var result []Key - for _, v := range i.Versions { - if v.Time > time { + for _, v := range i.versions { + if v.time > time { return result } - result = v.Keys + result = v.keys } return result @@ -357,8 +423,8 @@ func (i *Identity) SetMetadata(key string, value string) { func (i *Identity) ImmutableMetadata() map[string]string { metadata := make(map[string]string) - for _, version := range i.Versions { - for key, value := range version.Metadata { + for _, version := range i.versions { + for key, value := range version.metadata { if _, has := metadata[key]; !has { metadata[key] = value } @@ -373,8 +439,8 @@ func (i *Identity) ImmutableMetadata() map[string]string { func (i *Identity) MutableMetadata() map[string]string { metadata := make(map[string]string) - for _, version := range i.Versions { - for key, value := range version.Metadata { + for _, version := range i.versions { + for key, value := range version.metadata { metadata[key] = value } } diff --git a/identity/identity_actions.go b/identity/identity_actions.go index 69f77a2b..da7a064c 100644 --- a/identity/identity_actions.go +++ b/identity/identity_actions.go @@ -46,26 +46,6 @@ func Pull(repo repository.ClockedRepo, remote string) error { } // MergeAll will merge all the available remote identity -// To make sure that an Identity history can't be altered, a strict fast-forward -// only policy is applied here. As an Identity should be tied to a single user, this -// should work in practice but it does leave a possibility that a user would edit his -// Identity from two different repo concurrently and push the changes in a non-centralized -// network of repositories. In this case, it would result some of the repo accepting one -// version, some other accepting another, preventing the network in general to converge -// to the same result. This would create a sort of partition of the network, and manual -// cleaning would be required. -// -// An alternative approach would be to have a determinist rebase: -// - any commits present in both local and remote version would be kept, never changed. -// - newer commits would be merged in a linear chain of commits, ordered based on the -// Lamport time -// -// However, this approach leave the possibility, in the case of a compromised crypto keys, -// of forging a new version with a bogus Lamport time to be inserted before a legit version, -// invalidating the correct version and hijacking the Identity. There would only be a short -// period of time where this would be possible (before the network converge) but I'm not -// confident enough to implement that. I choose the strict fast-forward only approach, -// despite it's potential problem with two different version as mentioned above. func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { out := make(chan MergeResult) @@ -85,20 +65,19 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { id := refSplitted[len(refSplitted)-1] remoteIdentity, err := ReadLocal(repo, remoteRef) - remoteBug, err := readBug(repo, remoteRef) if err != nil { - out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error()) + out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote identity is not readable").Error()) continue } // Check for error in remote data - if err := remoteBug.Validate(); err != nil { - out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error()) + if err := remoteIdentity.Validate(); err != nil { + out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote identity is invalid").Error()) continue } - localRef := bugsRefPattern + remoteBug.Id() + localRef := identityRefPattern + remoteIdentity.Id() localExist, err := repo.RefExist(localRef) if err != nil { @@ -106,7 +85,7 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { continue } - // the bug is not local yet, simply create the reference + // the identity is not local yet, simply create the reference if !localExist { err := repo.CopyRef(remoteRef, localRef) @@ -115,18 +94,18 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { return } - out <- newMergeStatus(MergeStatusNew, id, remoteBug) + out <- newMergeStatus(MergeStatusNew, id, remoteIdentity) continue } - localBug, err := readBug(repo, localRef) + localIdentity, err := read(repo, localRef) if err != nil { - out <- newMergeError(errors.Wrap(err, "local bug is not readable"), id) + out <- newMergeError(errors.Wrap(err, "local identity is not readable"), id) return } - updated, err := localBug.Merge(repo, remoteBug) + updated, err := localIdentity.Merge(repo, remoteIdentity) if err != nil { out <- newMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error()) @@ -134,9 +113,9 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { } if updated { - out <- newMergeStatus(MergeStatusUpdated, id, localBug) + out <- newMergeStatus(MergeStatusUpdated, id, localIdentity) } else { - out <- newMergeStatus(MergeStatusNothing, id, localBug) + out <- newMergeStatus(MergeStatusNothing, id, localIdentity) } } }() diff --git a/identity/identity_test.go b/identity/identity_test.go index 3ab49d76..2ddb64ea 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -6,7 +6,6 @@ import ( "github.com/MichaelMure/git-bug/repository" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) // Test the commit and load of an Identity with multiple versions @@ -16,10 +15,10 @@ func TestIdentityCommitLoad(t *testing.T) { // single version identity := &Identity{ - Versions: []*Version{ + versions: []*Version{ { - Name: "René Descartes", - Email: "rene.descartes@example.com", + name: "René Descartes", + email: "rene.descartes@example.com", }, }, } @@ -32,33 +31,33 @@ func TestIdentityCommitLoad(t *testing.T) { loaded, err := ReadLocal(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) - equivalentIdentity(t, identity, loaded) + assert.Equal(t, identity, loaded) // multiple version identity = &Identity{ - Versions: []*Version{ + versions: []*Version{ { - Time: 100, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 100, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyA"}, }, }, { - Time: 200, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 200, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyB"}, }, }, { - Time: 201, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 201, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyC"}, }, }, @@ -73,24 +72,24 @@ func TestIdentityCommitLoad(t *testing.T) { loaded, err = ReadLocal(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) - equivalentIdentity(t, identity, loaded) + assert.Equal(t, identity, loaded) // add more version identity.AddVersion(&Version{ - Time: 201, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 201, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyD"}, }, }) identity.AddVersion(&Version{ - Time: 300, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 300, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyE"}, }, }) @@ -103,66 +102,56 @@ func TestIdentityCommitLoad(t *testing.T) { loaded, err = ReadLocal(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) - equivalentIdentity(t, identity, loaded) + assert.Equal(t, identity, loaded) } func commitsAreSet(t *testing.T, identity *Identity) { - for _, version := range identity.Versions { + for _, version := range identity.versions { assert.NotEmpty(t, version.commitHash) } } -func equivalentIdentity(t *testing.T, expected, actual *Identity) { - require.Equal(t, len(expected.Versions), len(actual.Versions)) - - for i, version := range expected.Versions { - actual.Versions[i].commitHash = version.commitHash - } - - assert.Equal(t, expected, actual) -} - // Test that the correct crypto keys are returned for a given lamport time func TestIdentity_ValidKeysAtTime(t *testing.T) { identity := Identity{ - Versions: []*Version{ + versions: []*Version{ { - Time: 100, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 100, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyA"}, }, }, { - Time: 200, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 200, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyB"}, }, }, { - Time: 201, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 201, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyC"}, }, }, { - Time: 201, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 201, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyD"}, }, }, { - Time: 300, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 300, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyE"}, }, }, @@ -197,8 +186,8 @@ func TestMetadata(t *testing.T) { // try override identity.AddVersion(&Version{ - Name: "René Descartes", - Email: "rene.descartes@example.com", + name: "René Descartes", + email: "rene.descartes@example.com", }) identity.SetMetadata("key1", "value2") @@ -226,10 +215,10 @@ func TestJSON(t *testing.T) { mockRepo := repository.NewMockRepoForTest() identity := &Identity{ - Versions: []*Version{ + versions: []*Version{ { - Name: "René Descartes", - Email: "rene.descartes@example.com", + name: "René Descartes", + email: "rene.descartes@example.com", }, }, } diff --git a/identity/version.go b/identity/version.go index f8b9cc73..90bf83f2 100644 --- a/identity/version.go +++ b/identity/version.go @@ -24,25 +24,25 @@ type Version struct { // The lamport time at which this version become effective // The reference time is the bug edition lamport clock - Time lamport.Time + time lamport.Time - Name string - Email string - Login string - AvatarUrl string + name string + email string + login string + avatarURL string // The set of keys valid at that time, from this version onward, until they get removed // in a new version. This allow to have multiple key for the same identity (e.g. one per // device) as well as revoke key. - Keys []Key + keys []Key // This optional array is here to ensure a better randomness of the identity id to avoid collisions. // It has no functional purpose and should be ignored. // It is advised to fill this array if there is not enough entropy, e.g. if there is no keys. - Nonce []byte + nonce []byte // A set of arbitrary key/value to store metadata about a version or about an Identity in general. - Metadata map[string]string + metadata map[string]string } type VersionJSON struct { @@ -62,14 +62,14 @@ type VersionJSON struct { func (v *Version) MarshalJSON() ([]byte, error) { return json.Marshal(VersionJSON{ FormatVersion: formatVersion, - Time: v.Time, - Name: v.Name, - Email: v.Email, - Login: v.Login, - AvatarUrl: v.AvatarUrl, - Keys: v.Keys, - Nonce: v.Nonce, - Metadata: v.Metadata, + Time: v.time, + Name: v.name, + Email: v.email, + Login: v.login, + AvatarUrl: v.avatarURL, + Keys: v.keys, + Nonce: v.nonce, + Metadata: v.metadata, }) } @@ -84,56 +84,56 @@ func (v *Version) UnmarshalJSON(data []byte) error { return fmt.Errorf("unknown format version %v", aux.FormatVersion) } - v.Time = aux.Time - v.Name = aux.Name - v.Email = aux.Email - v.Login = aux.Login - v.AvatarUrl = aux.AvatarUrl - v.Keys = aux.Keys - v.Nonce = aux.Nonce - v.Metadata = aux.Metadata + v.time = aux.Time + v.name = aux.Name + v.email = aux.Email + v.login = aux.Login + v.avatarURL = aux.AvatarUrl + v.keys = aux.Keys + v.nonce = aux.Nonce + v.metadata = aux.Metadata return nil } func (v *Version) Validate() error { - if text.Empty(v.Name) && text.Empty(v.Login) { + if text.Empty(v.name) && text.Empty(v.login) { return fmt.Errorf("either name or login should be set") } - if strings.Contains(v.Name, "\n") { + if strings.Contains(v.name, "\n") { return fmt.Errorf("name should be a single line") } - if !text.Safe(v.Name) { + if !text.Safe(v.name) { return fmt.Errorf("name is not fully printable") } - if strings.Contains(v.Login, "\n") { + if strings.Contains(v.login, "\n") { return fmt.Errorf("login should be a single line") } - if !text.Safe(v.Login) { + if !text.Safe(v.login) { return fmt.Errorf("login is not fully printable") } - if strings.Contains(v.Email, "\n") { + if strings.Contains(v.email, "\n") { return fmt.Errorf("email should be a single line") } - if !text.Safe(v.Email) { + if !text.Safe(v.email) { return fmt.Errorf("email is not fully printable") } - if v.AvatarUrl != "" && !text.ValidUrl(v.AvatarUrl) { + if v.avatarURL != "" && !text.ValidUrl(v.avatarURL) { return fmt.Errorf("avatarUrl is not a valid URL") } - if len(v.Nonce) > 64 { + if len(v.nonce) > 64 { return fmt.Errorf("nonce is too big") } - for _, k := range v.Keys { + for _, k := range v.keys { if err := k.Validate(); err != nil { return errors.Wrap(err, "invalid key") } @@ -178,20 +178,20 @@ func makeNonce(len int) []byte { // SetMetadata store arbitrary metadata about a version or an Identity in general // If the Version has been commit to git already, it won't be overwritten. func (v *Version) SetMetadata(key string, value string) { - if v.Metadata == nil { - v.Metadata = make(map[string]string) + if v.metadata == nil { + v.metadata = make(map[string]string) } - v.Metadata[key] = value + v.metadata[key] = value } // GetMetadata retrieve arbitrary metadata about the Version func (v *Version) GetMetadata(key string) (string, bool) { - val, ok := v.Metadata[key] + val, ok := v.metadata[key] return val, ok } // AllMetadata return all metadata for this Identity func (v *Version) AllMetadata() map[string]string { - return v.Metadata + return v.metadata } -- cgit From cd7ed7ff9e3250c10e97fe16c934b5a6151527bb Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 16 Feb 2019 13:48:46 +0100 Subject: identity: add more test for serialisation and push/pull/merge + fixes --- bug/bug_actions.go | 7 +- bug/bug_actions_test.go | 84 +++------------------ bug/operation_test.go | 3 +- graphql/graphql_test.go | 16 +++- identity/bare_test.go | 19 +++++ identity/identity.go | 64 +++++++++++++++- identity/identity_actions.go | 11 +-- identity/identity_actions_test.go | 151 ++++++++++++++++++++++++++++++++++++++ identity/identity_stub_test.go | 23 ++++++ identity/version_test.go | 42 +++++++++++ tests/read_bugs_test.go | 18 ++++- util/test/repo.go | 43 +++++++++-- 12 files changed, 381 insertions(+), 100 deletions(-) create mode 100644 identity/identity_actions_test.go create mode 100644 identity/identity_stub_test.go create mode 100644 identity/version_test.go diff --git a/bug/bug_actions.go b/bug/bug_actions.go index 6b9135b0..f214716d 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -23,8 +23,7 @@ func Push(repo repository.Repo, remote string) (string, error) { } // 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) if err != nil { @@ -36,9 +35,7 @@ func Pull(repo repository.ClockedRepo, remote string) error { return merge.Err } if merge.Status == MergeStatusInvalid { - // Not awesome: simply output the merge failure here as this function - // is only used in tests for now. - fmt.Println(merge) + return errors.Errorf("merge failure: %s", merge.Reason) } } diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go index af561bf6..39438ec7 100644 --- a/bug/bug_actions_test.go +++ b/bug/bug_actions_test.go @@ -1,81 +1,15 @@ package bug import ( - "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/test" "github.com/stretchr/testify/assert" - "io/ioutil" - "log" - "os" "testing" ) -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) err := rene.Commit(repoA) assert.NoError(t, err) @@ -139,8 +73,8 @@ 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.NoError(t, err) @@ -200,8 +134,8 @@ 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.NoError(t, err) @@ -281,8 +215,8 @@ 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.NoError(t, err) diff --git a/bug/operation_test.go b/bug/operation_test.go index 083ccb1e..d5cb5090 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -6,6 +6,7 @@ import ( "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" ) @@ -76,7 +77,7 @@ func TestMetadata(t *testing.T) { func TestHash(t *testing.T) { repos := []repository.ClockedRepo{ repository.NewMockRepoForTest(), - createRepo(false), + test.CreateRepo(false), } for _, repo := range repos { diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go index 90381987..d571ce51 100644 --- a/graphql/graphql_test.go +++ b/graphql/graphql_test.go @@ -5,12 +5,26 @@ import ( "testing" "github.com/MichaelMure/git-bug/graphql/models" + "github.com/MichaelMure/git-bug/misc/random_bugs" + "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/test" "github.com/vektah/gqlgen/client" ) +func CreateFilledRepo(bugNumber int) repository.ClockedRepo { + repo := test.CreateRepo(false) + + var seed int64 = 42 + options := random_bugs.DefaultOptions() + + options.BugNumber = bugNumber + + random_bugs.CommitRandomBugsWithSeed(repo, options, seed) + return repo +} + func TestQueries(t *testing.T) { - repo := test.CreateFilledRepo(10) + repo := CreateFilledRepo(10) handler, err := NewHandler(repo) if err != nil { diff --git a/identity/bare_test.go b/identity/bare_test.go index 4b28c7ad..7db9f644 100644 --- a/identity/bare_test.go +++ b/identity/bare_test.go @@ -1,6 +1,7 @@ package identity import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" @@ -11,3 +12,21 @@ func TestBare_Id(t *testing.T) { id := i.Id() assert.Equal(t, "7b226e616d65223a226e616d65222c22", id) } + +func TestBareSerialize(t *testing.T) { + before := &Bare{ + login: "login", + email: "email", + name: "name", + avatarUrl: "avatar", + } + + data, err := json.Marshal(before) + assert.NoError(t, err) + + var after Bare + err = json.Unmarshal(data, &after) + assert.NoError(t, err) + + assert.Equal(t, before, &after) +} diff --git a/identity/identity.go b/identity/identity.go index 59973489..725362f9 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -18,6 +18,8 @@ const identityRemoteRefPattern = "refs/remotes/%s/identities/" const versionEntryName = "version" const identityConfigKey = "git-bug.identity" +var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge") + var _ Interface = &Identity{} type Identity struct { @@ -136,6 +138,50 @@ func read(repo repository.Repo, ref string) (*Identity, error) { return i, nil } +type StreamedIdentity struct { + Identity *Identity + Err error +} + +// ReadAllLocalIdentities read and parse all local Identity +func ReadAllLocalIdentities(repo repository.ClockedRepo) <-chan StreamedIdentity { + return readAllIdentities(repo, identityRefPattern) +} + +// ReadAllRemoteIdentities read and parse all remote Identity for a given remote +func ReadAllRemoteIdentities(repo repository.ClockedRepo, remote string) <-chan StreamedIdentity { + refPrefix := fmt.Sprintf(identityRemoteRefPattern, remote) + return readAllIdentities(repo, refPrefix) +} + +// Read and parse all available bug with a given ref prefix +func readAllIdentities(repo repository.ClockedRepo, refPrefix string) <-chan StreamedIdentity { + out := make(chan StreamedIdentity) + + go func() { + defer close(out) + + refs, err := repo.ListRefs(refPrefix) + if err != nil { + out <- StreamedIdentity{Err: err} + return + } + + for _, ref := range refs { + b, err := read(repo, ref) + + if err != nil { + out <- StreamedIdentity{Err: err} + return + } + + out <- StreamedIdentity{Identity: b} + } + }() + + return out +} + // NewFromGitUser will query the repository for user detail and // build the corresponding Identity func NewFromGitUser(repo repository.Repo) (*Identity, error) { @@ -195,6 +241,22 @@ func (i *Identity) AddVersion(version *Version) { func (i *Identity) Commit(repo repository.Repo) error { // Todo: check for mismatch between memory and commited data + needCommit := false + for _, v := range i.versions { + if v.commitHash == "" { + needCommit = true + break + } + } + + if !needCommit { + return fmt.Errorf("can't commit an identity with no pending version") + } + + if err := i.Validate(); err != nil { + return errors.Wrap(err, "can't commit an identity with invalid data") + } + for _, v := range i.versions { if v.commitHash != "" { i.lastCommit = v.commitHash @@ -299,7 +361,7 @@ func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) { // we have a non fast-forward merge. // as explained in the doc above, refusing to merge if i.versions[j].commitHash != otherVersion.commitHash { - return false, errors.New("non fast-forward identity merge") + return false, ErrNonFastForwardMerge } } diff --git a/identity/identity_actions.go b/identity/identity_actions.go index da7a064c..53997eef 100644 --- a/identity/identity_actions.go +++ b/identity/identity_actions.go @@ -12,7 +12,7 @@ import ( // This does not change the local identities state func Fetch(repo repository.Repo, remote string) (string, error) { remoteRefSpec := fmt.Sprintf(identityRemoteRefPattern, remote) - fetchRefSpec := fmt.Sprintf("%s:%s*", identityRefPattern, remoteRefSpec) + fetchRefSpec := fmt.Sprintf("%s*:%s*", identityRefPattern, remoteRefSpec) return repo.FetchRefs(remote, fetchRefSpec) } @@ -23,8 +23,7 @@ func Push(repo repository.Repo, remote string) (string, error) { } // 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) if err != nil { @@ -36,9 +35,7 @@ func Pull(repo repository.ClockedRepo, remote string) error { return merge.Err } if merge.Status == MergeStatusInvalid { - // Not awesome: simply output the merge failure here as this function - // is only used in tests for now. - fmt.Println(merge) + return errors.Errorf("merge failure: %s", merge.Reason) } } @@ -64,7 +61,7 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { refSplitted := strings.Split(remoteRef, "/") id := refSplitted[len(refSplitted)-1] - remoteIdentity, err := ReadLocal(repo, remoteRef) + remoteIdentity, err := read(repo, remoteRef) if err != nil { out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote identity is not readable").Error()) diff --git a/identity/identity_actions_test.go b/identity/identity_actions_test.go new file mode 100644 index 00000000..42563374 --- /dev/null +++ b/identity/identity_actions_test.go @@ -0,0 +1,151 @@ +package identity + +import ( + "testing" + + "github.com/MichaelMure/git-bug/util/test" + "github.com/stretchr/testify/require" +) + +func TestPushPull(t *testing.T) { + repoA, repoB, remote := test.SetupReposAndRemote(t) + defer test.CleanupRepos(repoA, repoB, remote) + + identity1 := NewIdentity("name1", "email1") + err := identity1.Commit(repoA) + require.NoError(t, err) + + // A --> remote --> B + _, err = Push(repoA, "origin") + require.NoError(t, err) + + err = Pull(repoB, "origin") + require.NoError(t, err) + + identities := allIdentities(t, ReadAllLocalIdentities(repoB)) + + if len(identities) != 1 { + t.Fatal("Unexpected number of bugs") + } + + // B --> remote --> A + identity2 := NewIdentity("name2", "email2") + err = identity2.Commit(repoB) + require.NoError(t, err) + + _, err = Push(repoB, "origin") + require.NoError(t, err) + + err = Pull(repoA, "origin") + require.NoError(t, err) + + identities = allIdentities(t, ReadAllLocalIdentities(repoA)) + + if len(identities) != 2 { + t.Fatal("Unexpected number of bugs") + } + + // Update both + + identity1.AddVersion(&Version{ + name: "name1b", + email: "email1b", + }) + err = identity1.Commit(repoA) + require.NoError(t, err) + + identity2.AddVersion(&Version{ + name: "name2b", + email: "email2b", + }) + err = identity2.Commit(repoB) + require.NoError(t, err) + + // A --> remote --> B + + _, err = Push(repoA, "origin") + require.NoError(t, err) + + err = Pull(repoB, "origin") + require.NoError(t, err) + + identities = allIdentities(t, ReadAllLocalIdentities(repoB)) + + if len(identities) != 2 { + t.Fatal("Unexpected number of bugs") + } + + // B --> remote --> A + + _, err = Push(repoB, "origin") + require.NoError(t, err) + + err = Pull(repoA, "origin") + require.NoError(t, err) + + identities = allIdentities(t, ReadAllLocalIdentities(repoA)) + + if len(identities) != 2 { + t.Fatal("Unexpected number of bugs") + } + + // Concurrent update + + identity1.AddVersion(&Version{ + name: "name1c", + email: "email1c", + }) + err = identity1.Commit(repoA) + require.NoError(t, err) + + identity1B, err := ReadLocal(repoB, identity1.Id()) + require.NoError(t, err) + + identity1B.AddVersion(&Version{ + name: "name1concurrent", + email: "email1concurrent", + }) + err = identity1B.Commit(repoB) + require.NoError(t, err) + + // A --> remote --> B + + _, err = Push(repoA, "origin") + require.NoError(t, err) + + // Pulling a non-fast-forward update should fail + err = Pull(repoB, "origin") + require.Error(t, err) + + identities = allIdentities(t, ReadAllLocalIdentities(repoB)) + + if len(identities) != 2 { + t.Fatal("Unexpected number of bugs") + } + + // B --> remote --> A + + // Pushing a non-fast-forward update should fail + _, err = Push(repoB, "origin") + require.Error(t, err) + + err = Pull(repoA, "origin") + require.NoError(t, err) + + identities = allIdentities(t, ReadAllLocalIdentities(repoA)) + + if len(identities) != 2 { + t.Fatal("Unexpected number of bugs") + } +} + +func allIdentities(t testing.TB, identities <-chan StreamedIdentity) []*Identity { + var result []*Identity + for streamed := range identities { + if streamed.Err != nil { + t.Fatal(streamed.Err) + } + result = append(result, streamed.Identity) + } + return result +} diff --git a/identity/identity_stub_test.go b/identity/identity_stub_test.go new file mode 100644 index 00000000..3d94cd3e --- /dev/null +++ b/identity/identity_stub_test.go @@ -0,0 +1,23 @@ +package identity + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIdentityStubSerialize(t *testing.T) { + before := &IdentityStub{ + id: "id1234", + } + + data, err := json.Marshal(before) + assert.NoError(t, err) + + var after IdentityStub + err = json.Unmarshal(data, &after) + assert.NoError(t, err) + + assert.Equal(t, before, &after) +} diff --git a/identity/version_test.go b/identity/version_test.go new file mode 100644 index 00000000..8c4c8d99 --- /dev/null +++ b/identity/version_test.go @@ -0,0 +1,42 @@ +package identity + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestVersionSerialize(t *testing.T) { + before := &Version{ + login: "login", + name: "name", + email: "email", + avatarURL: "avatarUrl", + keys: []Key{ + { + Fingerprint: "fingerprint1", + PubKey: "pubkey1", + }, + { + Fingerprint: "fingerprint2", + PubKey: "pubkey2", + }, + }, + nonce: makeNonce(20), + metadata: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + time: 3, + } + + data, err := json.Marshal(before) + assert.NoError(t, err) + + var after Version + err = json.Unmarshal(data, &after) + assert.NoError(t, err) + + assert.Equal(t, before, &after) +} diff --git a/tests/read_bugs_test.go b/tests/read_bugs_test.go index 80d6cc1f..8b4379e7 100644 --- a/tests/read_bugs_test.go +++ b/tests/read_bugs_test.go @@ -4,11 +4,25 @@ import ( "testing" "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/misc/random_bugs" + "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/test" ) +func CreateFilledRepo(bugNumber int) repository.ClockedRepo { + repo := test.CreateRepo(false) + + var seed int64 = 42 + options := random_bugs.DefaultOptions() + + options.BugNumber = bugNumber + + random_bugs.CommitRandomBugsWithSeed(repo, options, seed) + return repo +} + func TestReadBugs(t *testing.T) { - repo := test.CreateFilledRepo(15) + repo := CreateFilledRepo(15) bugs := bug.ReadAllLocalBugs(repo) for b := range bugs { if b.Err != nil { @@ -18,7 +32,7 @@ func TestReadBugs(t *testing.T) { } func benchmarkReadBugs(bugNumber int, t *testing.B) { - repo := test.CreateFilledRepo(bugNumber) + repo := CreateFilledRepo(bugNumber) t.ResetTimer() for n := 0; n < t.N; n++ { diff --git a/util/test/repo.go b/util/test/repo.go index 8f0d2e5d..c5d3c000 100644 --- a/util/test/repo.go +++ b/util/test/repo.go @@ -3,8 +3,9 @@ package test import ( "io/ioutil" "log" + "os" + "testing" - "github.com/MichaelMure/git-bug/misc/random_bugs" "github.com/MichaelMure/git-bug/repository" ) @@ -39,14 +40,40 @@ func CreateRepo(bare bool) *repository.GitRepo { return repo } -func CreateFilledRepo(bugNumber int) repository.ClockedRepo { - repo := CreateRepo(false) +func CleanupRepo(repo repository.Repo) error { + path := repo.GetPath() + // fmt.Println("Cleaning repo:", path) + return os.RemoveAll(path) +} - var seed int64 = 42 - options := random_bugs.DefaultOptions() +func SetupReposAndRemote(t testing.TB) (repoA, repoB, remote *repository.GitRepo) { + repoA = CreateRepo(false) + repoB = CreateRepo(false) + remote = CreateRepo(true) - options.BugNumber = bugNumber + remoteAddr := "file://" + remote.GetPath() - random_bugs.CommitRandomBugsWithSeed(repo, options, seed) - return repo + 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) { + if err := CleanupRepo(repoA); err != nil { + log.Println(err) + } + if err := CleanupRepo(repoB); err != nil { + log.Println(err) + } + if err := CleanupRepo(remote); err != nil { + log.Println(err) + } } -- cgit From d2483d83dd52365741f51eca106aa18c4e8d6420 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 16 Feb 2019 17:32:30 +0100 Subject: identity: I can compile again !! --- bug/bug.go | 19 ++- bug/bug_actions.go | 46 +++++++- bug/bug_actions_test.go | 259 ++++++++++++++++++++++------------------- bug/bug_test.go | 14 +++ bug/op_create_test.go | 3 +- bug/op_edit_comment_test.go | 12 +- bug/op_set_metadata_test.go | 12 +- bug/operation_iterator_test.go | 28 +++-- bug/operation_pack.go | 2 +- bug/operation_pack_test.go | 25 +++- bug/operation_test.go | 11 +- cache/bug_cache.go | 5 +- commands/select/select_test.go | 125 +++++++------------- doc/man/git-bug.1 | 2 +- doc/md/git-bug.md | 1 + identity/bare.go | 5 + identity/common.go | 1 - identity/identity.go | 36 ++++-- identity/identity_stub.go | 4 + identity/interface.go | 4 + repository/git.go | 2 +- 21 files changed, 361 insertions(+), 255 deletions(-) diff --git a/bug/bug.go b/bug/bug.go index f84753fa..f1bd1114 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -321,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 @@ -349,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") } @@ -466,6 +472,17 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { 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 diff --git a/bug/bug_actions.go b/bug/bug_actions.go index f214716d..b906b938 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -4,28 +4,68 @@ import ( "fmt" "strings" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/pkg/errors" ) +// Note: +// +// For the actions (fetch/push/pull/merge), 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 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 } diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go index 39438ec7..4d42fb1d 100644 --- a/bug/bug_actions_test.go +++ b/bug/bug_actions_test.go @@ -1,8 +1,11 @@ package bug import ( + "time" + + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/test" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" ) @@ -11,20 +14,19 @@ func TestPushPull(t *testing.T) { repoA, repoB, remote := test.SetupReposAndRemote(t) defer test.CleanupRepos(repoA, repoB, remote) - err := rene.Commit(repoA) - assert.NoError(t, err) + reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr") - bug1, _, err := Create(rene, unix, "bug1", "message") - assert.NoError(t, err) + bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") + require.NoError(t, err) err = bug1.Commit(repoA) - assert.NoError(t, err) + require.NoError(t, err) // A --> remote --> B _, err = Push(repoA, "origin") - assert.NoError(t, err) + require.NoError(t, err) err = Pull(repoB, "origin") - assert.NoError(t, err) + require.NoError(t, err) bugs := allBugs(t, ReadAllLocalBugs(repoB)) @@ -33,16 +35,19 @@ func TestPushPull(t *testing.T) { } // B --> remote --> A - bug2, _, err := Create(rene, unix, "bug2", "message") - assert.NoError(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.NoError(t, err) + require.NoError(t, err) _, err = Push(repoB, "origin") - assert.NoError(t, err) + require.NoError(t, err) err = Pull(repoA, "origin") - assert.NoError(t, err) + require.NoError(t, err) bugs = allBugs(t, ReadAllLocalBugs(repoA)) @@ -76,38 +81,43 @@ func _RebaseTheirs(t testing.TB) { repoA, repoB, remote := test.SetupReposAndRemote(t) defer test.CleanupRepos(repoA, repoB, remote) - bug1, _, err := Create(rene, unix, "bug1", "message") - assert.NoError(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.NoError(t, err) + require.NoError(t, err) // A --> remote _, err = Push(repoA, "origin") - assert.NoError(t, err) + require.NoError(t, err) // remote --> B err = Pull(repoB, "origin") - assert.NoError(t, err) + require.NoError(t, err) bug2, err := ReadLocalBug(repoB, bug1.Id()) - assert.NoError(t, err) - - _, err = AddComment(bug2, rene, unix, "message2") - assert.NoError(t, err) - _, err = AddComment(bug2, rene, unix, "message3") - assert.NoError(t, err) - _, err = AddComment(bug2, rene, unix, "message4") - assert.NoError(t, err) + require.NoError(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.NoError(t, err) + require.NoError(t, err) // B --> remote _, err = Push(repoB, "origin") - assert.NoError(t, err) + require.NoError(t, err) // remote --> A err = Pull(repoA, "origin") - assert.NoError(t, err) + require.NoError(t, err) bugs := allBugs(t, ReadAllLocalBugs(repoB)) @@ -116,7 +126,7 @@ func _RebaseTheirs(t testing.TB) { } bug3, err := ReadLocalBug(repoA, bug1.Id()) - assert.NoError(t, err) + require.NoError(t, err) if nbOps(bug3) != 4 { t.Fatal("Unexpected number of operations") @@ -137,49 +147,51 @@ func _RebaseOurs(t testing.TB) { repoA, repoB, remote := test.SetupReposAndRemote(t) defer test.CleanupRepos(repoA, repoB, remote) - bug1, _, err := Create(rene, unix, "bug1", "message") - assert.NoError(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.NoError(t, err) + require.NoError(t, err) // A --> remote _, err = Push(repoA, "origin") - assert.NoError(t, err) + require.NoError(t, err) // remote --> B err = Pull(repoB, "origin") - assert.NoError(t, err) - - _, err = AddComment(bug1, rene, unix, "message2") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message3") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message4") - assert.NoError(t, err) + require.NoError(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.NoError(t, err) - - _, err = AddComment(bug1, rene, unix, "message5") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message6") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message7") - assert.NoError(t, err) + require.NoError(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.NoError(t, err) - - _, err = AddComment(bug1, rene, unix, "message8") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message9") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message10") - assert.NoError(t, err) + require.NoError(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.NoError(t, err) + require.NoError(t, err) // remote --> A err = Pull(repoA, "origin") - assert.NoError(t, err) + require.NoError(t, err) bugs := allBugs(t, ReadAllLocalBugs(repoA)) @@ -188,7 +200,7 @@ func _RebaseOurs(t testing.TB) { } bug2, err := ReadLocalBug(repoA, bug1.Id()) - assert.NoError(t, err) + require.NoError(t, err) if nbOps(bug2) != 10 { t.Fatal("Unexpected number of operations") @@ -218,83 +230,88 @@ func _RebaseConflict(t testing.TB) { repoA, repoB, remote := test.SetupReposAndRemote(t) defer test.CleanupRepos(repoA, repoB, remote) - bug1, _, err := Create(rene, unix, "bug1", "message") - assert.NoError(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.NoError(t, err) + require.NoError(t, err) // A --> remote _, err = Push(repoA, "origin") - assert.NoError(t, err) + require.NoError(t, err) // remote --> B err = Pull(repoB, "origin") - assert.NoError(t, err) - - _, err = AddComment(bug1, rene, unix, "message2") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message3") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message4") - assert.NoError(t, err) + require.NoError(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.NoError(t, err) - - _, err = AddComment(bug1, rene, unix, "message5") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message6") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message7") - assert.NoError(t, err) + require.NoError(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.NoError(t, err) - - _, err = AddComment(bug1, rene, unix, "message8") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message9") - assert.NoError(t, err) - _, err = AddComment(bug1, rene, unix, "message10") - assert.NoError(t, err) + require.NoError(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.NoError(t, err) + require.NoError(t, err) bug2, err := ReadLocalBug(repoB, bug1.Id()) - assert.NoError(t, err) - - _, err = AddComment(bug2, rene, unix, "message11") - assert.NoError(t, err) - _, err = AddComment(bug2, rene, unix, "message12") - assert.NoError(t, err) - _, err = AddComment(bug2, rene, unix, "message13") - assert.NoError(t, err) + require.NoError(t, err) + + reneB, err := identity.ReadLocal(repoA, reneA.Id()) + require.NoError(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.NoError(t, err) - - _, err = AddComment(bug2, rene, unix, "message14") - assert.NoError(t, err) - _, err = AddComment(bug2, rene, unix, "message15") - assert.NoError(t, err) - _, err = AddComment(bug2, rene, unix, "message16") - assert.NoError(t, err) + require.NoError(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.NoError(t, err) - - _, err = AddComment(bug2, rene, unix, "message17") - assert.NoError(t, err) - _, err = AddComment(bug2, rene, unix, "message18") - assert.NoError(t, err) - _, err = AddComment(bug2, rene, unix, "message19") - assert.NoError(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) - assert.NoError(t, err) + require.NoError(t, err) // A --> remote _, err = Push(repoA, "origin") - assert.NoError(t, err) + require.NoError(t, err) // remote --> B err = Pull(repoB, "origin") - assert.NoError(t, err) + require.NoError(t, err) bugs := allBugs(t, ReadAllLocalBugs(repoB)) @@ -303,7 +320,7 @@ func _RebaseConflict(t testing.TB) { } bug3, err := ReadLocalBug(repoB, bug1.Id()) - assert.NoError(t, err) + require.NoError(t, err) if nbOps(bug3) != 19 { t.Fatal("Unexpected number of operations") @@ -311,11 +328,11 @@ func _RebaseConflict(t testing.TB) { // B --> remote _, err = Push(repoB, "origin") - assert.NoError(t, err) + require.NoError(t, err) // remote --> A err = Pull(repoA, "origin") - assert.NoError(t, err) + require.NoError(t, err) bugs = allBugs(t, ReadAllLocalBugs(repoA)) @@ -324,7 +341,7 @@ func _RebaseConflict(t testing.TB) { } bug4, err := ReadLocalBug(repoA, bug1.Id()) - assert.NoError(t, err) + require.NoError(t, err) if nbOps(bug4) != 19 { t.Fatal("Unexpected number of operations") diff --git a/bug/bug_test.go b/bug/bug_test.go index 001bfc56..e104f921 100644 --- a/bug/bug_test.go +++ b/bug/bug_test.go @@ -1,6 +1,9 @@ package bug import ( + "time" + + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/stretchr/testify/assert" @@ -12,6 +15,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) @@ -28,6 +34,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") } @@ -58,6 +67,11 @@ func TestBugValidity(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) diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 065b81c5..c41c5687 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -12,8 +12,7 @@ import ( func TestCreate(t *testing.T) { snapshot := Snapshot{} - var rene = identity.NewIdentity("René Descartes", "rene@descartes.fr") - + rene := identity.NewBare("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() create := NewCreateOp(rene, unix, "title", "message", nil) diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index dbdf341d..72f8a168 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -7,30 +7,26 @@ import ( "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 = identity.NewIdentity("René Descartes", "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) diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index 847164f3..a7f9f313 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -7,13 +7,13 @@ import ( "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 = identity.NewIdentity("René Descartes", "rene@descartes.fr") - + rene := identity.NewBare("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() create := NewCreateOp(rene, unix, "title", "create", nil) @@ -22,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") @@ -32,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", diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go index a41120e2..2865d25d 100644 --- a/bug/operation_iterator_test.go +++ b/bug/operation_iterator_test.go @@ -10,14 +10,17 @@ import ( ) var ( - 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"}) +// Beware, don't those test data in multi-repo situation ! +// As an example, the Identity would be considered commited after a commit +// in one repo, +// 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"}) ) func ExampleOperationIterator() { @@ -36,6 +39,15 @@ func ExampleOperationIterator() { 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 diff --git a/bug/operation_pack.go b/bug/operation_pack.go index 1ffc1d1a..55fc018e 100644 --- a/bug/operation_pack.go +++ b/bug/operation_pack.go @@ -147,7 +147,7 @@ func (opp *OperationPack) Write(repo repository.Repo) (git.Hash, error) { // First, make sure that all the identities are properly Commit as well for _, op := range opp.Operations { - err := op.base().Author.Commit(repo) + err := op.base().Author.CommitAsNeeded(repo) if err != nil { return "", err } diff --git a/bug/operation_pack_test.go b/bug/operation_pack_test.go index 8a8c7e62..09d159af 100644 --- a/bug/operation_pack_test.go +++ b/bug/operation_pack_test.go @@ -3,27 +3,37 @@ package bug import ( "encoding/json" "testing" + "time" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/git" "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) 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", }) @@ -36,7 +46,16 @@ func TestOperationPackSerialize(t *testing.T) { var opp2 *OperationPack err = json.Unmarshal(data, &opp2) - assert.NoError(t, err) + + 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) + } +} diff --git a/bug/operation_test.go b/bug/operation_test.go index d5cb5090..f9a7d191 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -2,6 +2,7 @@ package bug import ( "testing" + "time" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" @@ -11,6 +12,9 @@ import ( ) 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"), @@ -65,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") @@ -81,7 +86,9 @@ func TestHash(t *testing.T) { } 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() diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 53c1db64..1d161c76 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -234,8 +234,5 @@ func (c *BugCache) Commit() error { } func (c *BugCache) CommitAsNeeded() error { - if c.bug.HasPendingOp() { - return c.bug.Commit(c.repoCache.repo) - } - return nil + return c.bug.CommitAsNeeded(c.repoCache.repo) } diff --git a/commands/select/select_test.go b/commands/select/select_test.go index fe501d76..003dfc81 100644 --- a/commands/select/select_test.go +++ b/commands/select/select_test.go @@ -1,113 +1,74 @@ package _select import ( - "io/ioutil" - "log" "testing" "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) + defer test.CleanupRepo(repo) - _, _, 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 for i := 0; i < 10; i++ { - _, err := repo.NewBug("title", "message") - checkErr(t, err) + _, err := repoCache.NewBug("title", "message") + 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.NewBug("title", "message") + require.NoError(t, err) + b2, err := repoCache.NewBug("title", "message") + 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") - } - - err = Clear(repo) - checkErr(t, err) + b7, _, err := ResolveBug(repoCache, []string{b2.Id()}) + require.NoError(t, err) + require.Equal(t, b2.Id(), b7.Id()) - _, _, err = ResolveBug(repo, []string{}) - if err == nil { - t.Fatal("expected invalid bug error") - } -} + err = Clear(repoCache) + require.NoError(t, err) -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) } diff --git a/doc/man/git-bug.1 b/doc/man/git-bug.1 index 123f9a33..f3ad4729 100644 --- a/doc/man/git-bug.1 +++ b/doc/man/git-bug.1 @@ -31,4 +31,4 @@ the same git remote your are already using to collaborate with other peoples. .SH SEE ALSO .PP -\fBgit\-bug\-add(1)\fP, \fBgit\-bug\-bridge(1)\fP, \fBgit\-bug\-commands(1)\fP, \fBgit\-bug\-comment(1)\fP, \fBgit\-bug\-deselect(1)\fP, \fBgit\-bug\-label(1)\fP, \fBgit\-bug\-ls(1)\fP, \fBgit\-bug\-ls\-label(1)\fP, \fBgit\-bug\-pull(1)\fP, \fBgit\-bug\-push(1)\fP, \fBgit\-bug\-select(1)\fP, \fBgit\-bug\-show(1)\fP, \fBgit\-bug\-status(1)\fP, \fBgit\-bug\-termui(1)\fP, \fBgit\-bug\-title(1)\fP, \fBgit\-bug\-version(1)\fP, \fBgit\-bug\-webui(1)\fP +\fBgit\-bug\-add(1)\fP, \fBgit\-bug\-bridge(1)\fP, \fBgit\-bug\-commands(1)\fP, \fBgit\-bug\-comment(1)\fP, \fBgit\-bug\-deselect(1)\fP, \fBgit\-bug\-id(1)\fP, \fBgit\-bug\-label(1)\fP, \fBgit\-bug\-ls(1)\fP, \fBgit\-bug\-ls\-label(1)\fP, \fBgit\-bug\-pull(1)\fP, \fBgit\-bug\-push(1)\fP, \fBgit\-bug\-select(1)\fP, \fBgit\-bug\-show(1)\fP, \fBgit\-bug\-status(1)\fP, \fBgit\-bug\-termui(1)\fP, \fBgit\-bug\-title(1)\fP, \fBgit\-bug\-version(1)\fP, \fBgit\-bug\-webui(1)\fP diff --git a/doc/md/git-bug.md b/doc/md/git-bug.md index a1c1c885..7183dd76 100644 --- a/doc/md/git-bug.md +++ b/doc/md/git-bug.md @@ -29,6 +29,7 @@ git-bug [flags] * [git-bug commands](git-bug_commands.md) - Display available commands * [git-bug comment](git-bug_comment.md) - Display or add comments * [git-bug deselect](git-bug_deselect.md) - Clear the implicitly selected bug +* [git-bug id](git-bug_id.md) - Display or change the user identity * [git-bug label](git-bug_label.md) - Display, add or remove labels * [git-bug ls](git-bug_ls.md) - List bugs * [git-bug ls-id](git-bug_ls-id.md) - List Bug Id diff --git a/identity/bare.go b/identity/bare.go index 5aaa0166..c54277a0 100644 --- a/identity/bare.go +++ b/identity/bare.go @@ -169,6 +169,11 @@ func (i *Bare) Commit(repo repository.Repo) error { return nil } +func (i *Bare) CommitAsNeeded(repo repository.Repo) error { + // Nothing to do, everything is directly embedded + return nil +} + // IsProtected return true if the chain of git commits started to be signed. // If that's the case, only signed commit with a valid key for this identity can be added. func (i *Bare) IsProtected() bool { diff --git a/identity/common.go b/identity/common.go index 00feaa2d..2f2b9042 100644 --- a/identity/common.go +++ b/identity/common.go @@ -29,7 +29,6 @@ func UnmarshalJSON(raw json.RawMessage) (Interface, error) { err := json.Unmarshal(raw, &aux) if err == nil && aux.Id() != "" { return aux, nil - // return identityResolver.ResolveIdentity(aux.Id) } // abort if we have an error other than the wrong type diff --git a/identity/identity.go b/identity/identity.go index 725362f9..a0800bcd 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -241,15 +241,7 @@ func (i *Identity) AddVersion(version *Version) { func (i *Identity) Commit(repo repository.Repo) error { // Todo: check for mismatch between memory and commited data - needCommit := false - for _, v := range i.versions { - if v.commitHash == "" { - needCommit = true - break - } - } - - if !needCommit { + if !i.NeedCommit() { return fmt.Errorf("can't commit an identity with no pending version") } @@ -313,6 +305,23 @@ func (i *Identity) Commit(repo repository.Repo) error { return nil } +func (i *Identity) CommitAsNeeded(repo repository.Repo) error { + if !i.NeedCommit() { + return nil + } + return i.Commit(repo) +} + +func (i *Identity) NeedCommit() bool { + for _, v := range i.versions { + if v.commitHash == "" { + return true + } + } + + return false +} + // Merge will merge a different version of the same Identity // // To make sure that an Identity history can't be altered, a strict fast-forward @@ -379,6 +388,10 @@ func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) { func (i *Identity) Validate() error { lastTime := lamport.Time(0) + if len(i.versions) == 0 { + return fmt.Errorf("no version") + } + for _, v := range i.versions { if err := v.Validate(); err != nil { return err @@ -391,6 +404,11 @@ func (i *Identity) Validate() error { lastTime = v.time } + // The identity ID should be the hash of the first commit + if i.versions[0].commitHash != "" && string(i.versions[0].commitHash) != i.id { + return fmt.Errorf("identity id should be the first commit hash") + } + return nil } diff --git a/identity/identity_stub.go b/identity/identity_stub.go index 6788ce33..e91600b0 100644 --- a/identity/identity_stub.go +++ b/identity/identity_stub.go @@ -80,6 +80,10 @@ func (IdentityStub) Commit(repo repository.Repo) error { panic("identities needs to be properly loaded with identity.ReadLocal()") } +func (i *IdentityStub) CommitAsNeeded(repo repository.Repo) error { + panic("identities needs to be properly loaded with identity.ReadLocal()") +} + func (IdentityStub) IsProtected() bool { panic("identities needs to be properly loaded with identity.ReadLocal()") } diff --git a/identity/interface.go b/identity/interface.go index 1d534eb8..55877c02 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -30,6 +30,10 @@ type Interface interface { // the Id is properly set. Commit(repo repository.Repo) error + // If needed, write the identity into the Repository. In particular, this + // ensure that the Id is properly set. + CommitAsNeeded(repo repository.Repo) error + // IsProtected return true if the chain of git commits started to be signed. // If that's the case, only signed commit with a valid key for this identity can be added. IsProtected() bool diff --git a/repository/git.go b/repository/git.go index c2f0da0a..10fddac3 100644 --- a/repository/git.go +++ b/repository/git.go @@ -29,7 +29,7 @@ type GitRepo struct { // Run the given git command with the given I/O reader/writers, returning an error if it fails. func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error { - //fmt.Println("Running git", strings.Join(args, " ")) + // fmt.Printf("[%s] Running git %s\n", repo.Path, strings.Join(args, " ")) cmd := exec.Command("git", args...) cmd.Dir = repo.Path -- cgit From da558b05ef79f4c80df10c6969a9ae5f4f764f96 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 16 Feb 2019 19:34:52 +0100 Subject: identity: all tests green o/ --- cache/bug_cache.go | 12 ++++++------ cache/repo_cache.go | 2 +- commands/select/select_test.go | 14 ++++++++++---- commands/show.go | 2 +- doc/man/git-bug-id.1 | 2 +- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 1d161c76..2a570667 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -101,7 +101,7 @@ func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error { return c.AddCommentRaw(author, time.Now().Unix(), message, files, nil) } -func (c *BugCache) AddCommentRaw(author *identity.Identity, unixTime int64, message string, files []git.Hash, metadata map[string]string) error { +func (c *BugCache) AddCommentRaw(author identity.Interface, unixTime int64, message string, files []git.Hash, metadata map[string]string) error { op, err := bug.AddCommentWithFiles(c.bug, author, unixTime, message, files) if err != nil { return err @@ -123,7 +123,7 @@ func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelCh return c.ChangeLabelsRaw(author, time.Now().Unix(), added, removed, nil) } -func (c *BugCache) ChangeLabelsRaw(author *identity.Identity, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, error) { +func (c *BugCache) ChangeLabelsRaw(author identity.Interface, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, error) { changes, op, err := bug.ChangeLabels(c.bug, author, unixTime, added, removed) if err != nil { return changes, err @@ -150,7 +150,7 @@ func (c *BugCache) Open() error { return c.OpenRaw(author, time.Now().Unix(), nil) } -func (c *BugCache) OpenRaw(author *identity.Identity, unixTime int64, metadata map[string]string) error { +func (c *BugCache) OpenRaw(author identity.Interface, unixTime int64, metadata map[string]string) error { op, err := bug.Open(c.bug, author, unixTime) if err != nil { return err @@ -172,7 +172,7 @@ func (c *BugCache) Close() error { return c.CloseRaw(author, time.Now().Unix(), nil) } -func (c *BugCache) CloseRaw(author *identity.Identity, unixTime int64, metadata map[string]string) error { +func (c *BugCache) CloseRaw(author identity.Interface, unixTime int64, metadata map[string]string) error { op, err := bug.Close(c.bug, author, unixTime) if err != nil { return err @@ -194,7 +194,7 @@ func (c *BugCache) SetTitle(title string) error { return c.SetTitleRaw(author, time.Now().Unix(), title, nil) } -func (c *BugCache) SetTitleRaw(author *identity.Identity, unixTime int64, title string, metadata map[string]string) error { +func (c *BugCache) SetTitleRaw(author identity.Interface, unixTime int64, title string, metadata map[string]string) error { op, err := bug.SetTitle(c.bug, author, unixTime, title) if err != nil { return err @@ -216,7 +216,7 @@ func (c *BugCache) EditComment(target git.Hash, message string) error { return c.EditCommentRaw(author, time.Now().Unix(), target, message, nil) } -func (c *BugCache) EditCommentRaw(author *identity.Identity, unixTime int64, target git.Hash, message string, metadata map[string]string) error { +func (c *BugCache) EditCommentRaw(author identity.Interface, unixTime int64, target git.Hash, message string, metadata map[string]string) error { op, err := bug.EditComment(c.bug, author, unixTime, target, message) if err != nil { return err diff --git a/cache/repo_cache.go b/cache/repo_cache.go index f207e984..74f04e11 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -405,7 +405,7 @@ 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 *identity.Identity, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) { +func (c *RepoCache) NewBugRaw(author identity.Interface, 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) if err != nil { return nil, err diff --git a/commands/select/select_test.go b/commands/select/select_test.go index 003dfc81..f26350f8 100644 --- a/commands/select/select_test.go +++ b/commands/select/select_test.go @@ -2,15 +2,16 @@ package _select import ( "testing" + "time" "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/test" "github.com/stretchr/testify/require" ) func TestSelect(t *testing.T) { repo := test.CreateRepo(false) - defer test.CleanupRepo(repo) repoCache, err := cache.NewRepoCache(repo) require.NoError(t, err) @@ -26,15 +27,18 @@ func TestSelect(t *testing.T) { require.Error(t, err) // generate a bunch of bugs + + rene := identity.NewBare("René Descartes", "rene@descartes.fr") + for i := 0; i < 10; i++ { - _, err := repoCache.NewBug("title", "message") + _, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil) require.NoError(t, err) } // and two more for testing - b1, err := repoCache.NewBug("title", "message") + b1, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil) require.NoError(t, err) - b2, err := repoCache.NewBug("title", "message") + b2, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil) require.NoError(t, err) err = Select(repoCache, b1.Id()) @@ -71,4 +75,6 @@ func TestSelect(t *testing.T) { // 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)) } diff --git a/commands/show.go b/commands/show.go index 123a46dc..3adc8b52 100644 --- a/commands/show.go +++ b/commands/show.go @@ -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": diff --git a/doc/man/git-bug-id.1 b/doc/man/git-bug-id.1 index 259c4c48..b3baf589 100644 --- a/doc/man/git-bug-id.1 +++ b/doc/man/git-bug-id.1 @@ -1,4 +1,4 @@ -.TH "GIT-BUG" "1" "Jan 2019" "Generated from git-bug's source code" "" +.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" .nh .ad l -- cgit From 864eae0d6bd0732260c0c56583bb77f9b25b60f6 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 17 Feb 2019 16:12:06 +0100 Subject: identity: work on higher level now, cache, first two identity commands --- bridge/github/import.go | 4 +-- bridge/launchpad/import.go | 2 +- cache/bug_cache.go | 38 ++++++++++----------- cache/identity_cache.go | 26 ++++++++++++++ cache/repo_cache.go | 68 +++++++++++++++++++++++++++--------- commands/id.go | 49 -------------------------- commands/user.go | 56 ++++++++++++++++++++++++++++++ commands/user_create.go | 76 +++++++++++++++++++++++++++++++++++++++++ doc/man/git-bug-bridge-bridge.1 | 29 ---------------- doc/man/git-bug-id.1 | 29 ---------------- doc/man/git-bug-user-create.1 | 29 ++++++++++++++++ doc/man/git-bug-user.1 | 29 ++++++++++++++++ doc/man/git-bug.1 | 2 +- doc/md/git-bug.md | 2 +- doc/md/git-bug_bridge_bridge.md | 22 ------------ doc/md/git-bug_close.md | 22 ------------ doc/md/git-bug_id.md | 22 ------------ doc/md/git-bug_new.md | 25 -------------- doc/md/git-bug_open.md | 22 ------------ doc/md/git-bug_user.md | 23 +++++++++++++ doc/md/git-bug_user_create.md | 22 ++++++++++++ identity/identity.go | 16 ++++++++- input/prompt.go | 44 ++++++++++++++++++++++++ misc/bash_completion/git-bug | 63 ++++++++++++++++++++++------------ misc/zsh_completion/git-bug | 5 ++- 25 files changed, 441 insertions(+), 284 deletions(-) create mode 100644 cache/identity_cache.go delete mode 100644 commands/id.go create mode 100644 commands/user.go create mode 100644 commands/user_create.go delete mode 100644 doc/man/git-bug-bridge-bridge.1 delete mode 100644 doc/man/git-bug-id.1 create mode 100644 doc/man/git-bug-user-create.1 create mode 100644 doc/man/git-bug-user.1 delete mode 100644 doc/md/git-bug_bridge_bridge.md delete mode 100644 doc/md/git-bug_close.md delete mode 100644 doc/md/git-bug_id.md delete mode 100644 doc/md/git-bug_new.md delete mode 100644 doc/md/git-bug_open.md create mode 100644 doc/md/git-bug_user.md create mode 100644 doc/md/git-bug_user_create.md create mode 100644 input/prompt.go diff --git a/bridge/github/import.go b/bridge/github/import.go index 43a8e3b5..38278911 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -599,7 +599,7 @@ func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugC } // makePerson create a bug.Person from the Github data -func (gi *githubImporter) makePerson(repo *cache.RepoCache, actor *actor) (*identity.Identity, error) { +func (gi *githubImporter) makePerson(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 { @@ -645,7 +645,7 @@ func (gi *githubImporter) makePerson(repo *cache.RepoCache, actor *actor) (*iden ) } -func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*identity.Identity, 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 { diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go index e65186ed..b70d34f0 100644 --- a/bridge/launchpad/import.go +++ b/bridge/launchpad/import.go @@ -23,7 +23,7 @@ func (li *launchpadImporter) Init(conf core.Configuration) error { const keyLaunchpadID = "launchpad-id" const keyLaunchpadLogin = "launchpad-login" -func (li *launchpadImporter) makePerson(repo *cache.RepoCache, owner LPPerson) (*identity.Identity, error) { +func (li *launchpadImporter) makePerson(repo *cache.RepoCache, owner LPPerson) (*cache.IdentityCache, error) { // Look first in the cache i, err := repo.ResolveIdentityImmutableMetadata(keyLaunchpadLogin, owner.Login) if err == nil { diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 2a570667..ce46837a 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -5,8 +5,6 @@ import ( "strings" "time" - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/util/git" ) @@ -93,7 +91,7 @@ func (c *BugCache) AddComment(message string) error { } func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error { - author, err := identity.GetUserIdentity(c.repoCache.repo) + author, err := c.repoCache.GetUserIdentity() if err != nil { return err } @@ -101,8 +99,8 @@ func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error { return c.AddCommentRaw(author, time.Now().Unix(), message, files, nil) } -func (c *BugCache) AddCommentRaw(author identity.Interface, 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) error { + op, err := bug.AddCommentWithFiles(c.bug, author.Identity, unixTime, message, files) if err != nil { return err } @@ -115,7 +113,7 @@ func (c *BugCache) AddCommentRaw(author identity.Interface, unixTime int64, mess } func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelChangeResult, error) { - author, err := identity.GetUserIdentity(c.repoCache.repo) + author, err := c.repoCache.GetUserIdentity() if err != nil { return nil, err } @@ -123,8 +121,8 @@ func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelCh return c.ChangeLabelsRaw(author, time.Now().Unix(), added, removed, nil) } -func (c *BugCache) ChangeLabelsRaw(author identity.Interface, 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, error) { + changes, op, err := bug.ChangeLabels(c.bug, author.Identity, unixTime, added, removed) if err != nil { return changes, err } @@ -142,7 +140,7 @@ func (c *BugCache) ChangeLabelsRaw(author identity.Interface, unixTime int64, ad } func (c *BugCache) Open() error { - author, err := identity.GetUserIdentity(c.repoCache.repo) + author, err := c.repoCache.GetUserIdentity() if err != nil { return err } @@ -150,8 +148,8 @@ func (c *BugCache) Open() error { return c.OpenRaw(author, time.Now().Unix(), nil) } -func (c *BugCache) OpenRaw(author identity.Interface, 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) error { + op, err := bug.Open(c.bug, author.Identity, unixTime) if err != nil { return err } @@ -164,7 +162,7 @@ func (c *BugCache) OpenRaw(author identity.Interface, unixTime int64, metadata m } func (c *BugCache) Close() error { - author, err := identity.GetUserIdentity(c.repoCache.repo) + author, err := c.repoCache.GetUserIdentity() if err != nil { return err } @@ -172,8 +170,8 @@ func (c *BugCache) Close() error { return c.CloseRaw(author, time.Now().Unix(), nil) } -func (c *BugCache) CloseRaw(author identity.Interface, 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) error { + op, err := bug.Close(c.bug, author.Identity, unixTime) if err != nil { return err } @@ -186,7 +184,7 @@ func (c *BugCache) CloseRaw(author identity.Interface, unixTime int64, metadata } func (c *BugCache) SetTitle(title string) error { - author, err := identity.GetUserIdentity(c.repoCache.repo) + author, err := c.repoCache.GetUserIdentity() if err != nil { return err } @@ -194,8 +192,8 @@ func (c *BugCache) SetTitle(title string) error { return c.SetTitleRaw(author, time.Now().Unix(), title, nil) } -func (c *BugCache) SetTitleRaw(author identity.Interface, 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) error { + op, err := bug.SetTitle(c.bug, author.Identity, unixTime, title) if err != nil { return err } @@ -208,7 +206,7 @@ func (c *BugCache) SetTitleRaw(author identity.Interface, unixTime int64, title } func (c *BugCache) EditComment(target git.Hash, message string) error { - author, err := identity.GetUserIdentity(c.repoCache.repo) + author, err := c.repoCache.GetUserIdentity() if err != nil { return err } @@ -216,8 +214,8 @@ func (c *BugCache) EditComment(target git.Hash, message string) error { return c.EditCommentRaw(author, time.Now().Unix(), target, message, nil) } -func (c *BugCache) EditCommentRaw(author identity.Interface, 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) error { + op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message) if err != nil { return err } diff --git a/cache/identity_cache.go b/cache/identity_cache.go new file mode 100644 index 00000000..93b2dc4b --- /dev/null +++ b/cache/identity_cache.go @@ -0,0 +1,26 @@ +package cache + +import ( + "github.com/MichaelMure/git-bug/identity" +) + +// IdentityCache is a wrapper around an Identity. It provide multiple functions: +type IdentityCache struct { + *identity.Identity + repoCache *RepoCache +} + +func NewIdentityCache(repoCache *RepoCache, id *identity.Identity) *IdentityCache { + return &IdentityCache{ + Identity: id, + repoCache: repoCache, + } +} + +func (i *IdentityCache) Commit() error { + return i.Identity.Commit(i.repoCache.repo) +} + +func (i *IdentityCache) CommitAsNeeded() error { + return i.Identity.CommitAsNeeded(i.repoCache.repo) +} diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 74f04e11..f64a1b76 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -45,13 +45,16 @@ type RepoCache struct { // bug loaded in memory bugs map[string]*BugCache // identities loaded in memory - identities map[string]*identity.Identity + 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() @@ -394,7 +397,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 := identity.GetUserIdentity(c.repo) + author, err := c.GetUserIdentity() if err != nil { return nil, err } @@ -405,8 +408,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 identity.Interface, 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 } @@ -549,7 +552,7 @@ func repoIsAvailable(repo repository.Repo) error { } // ResolveIdentity retrieve an identity matching the exact given id -func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) { +func (c *RepoCache) ResolveIdentity(id string) (*IdentityCache, error) { cached, ok := c.identities[id] if ok { return cached, nil @@ -560,14 +563,15 @@ func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) { return nil, err } - c.identities[id] = i + cached = NewIdentityCache(c, i) + c.identities[id] = cached - return i, nil + return cached, nil } // ResolveIdentityPrefix retrieve an Identity matching an id prefix. // It fails if multiple identities match. -func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*identity.Identity, error) { +func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) { // preallocate but empty matching := make([]string, 0, 5) @@ -590,7 +594,7 @@ func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*identity.Identity, er // 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) (*identity.Identity, error) { +func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*IdentityCache, error) { // preallocate but empty matching := make([]string, 0, 5) @@ -611,19 +615,50 @@ func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) ( return c.ResolveIdentity(matching[0]) } +func (c *RepoCache) SetUserIdentity(i *IdentityCache) error { + err := identity.SetUserIdentity(c.repo, i.Identity) + if err != nil { + return err + } + + 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) (*identity.Identity, error) { +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) (*identity.Identity, error) { +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) (*identity.Identity, error) { +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 { @@ -639,7 +674,8 @@ func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avat return nil, fmt.Errorf("identity %s already exist in the cache", i.Id()) } - c.identities[i.Id()] = i + cached := NewIdentityCache(c, i) + c.identities[i.Id()] = cached - return i, nil + return cached, nil } diff --git a/commands/id.go b/commands/id.go deleted file mode 100644 index 7eacd986..00000000 --- a/commands/id.go +++ /dev/null @@ -1,49 +0,0 @@ -package commands - -import ( - "errors" - "fmt" - - "github.com/MichaelMure/git-bug/identity" - "github.com/spf13/cobra" -) - -func runId(cmd *cobra.Command, args []string) error { - if len(args) > 1 { - return errors.New("only one identity can be displayed at a time") - } - - var id *identity.Identity - var err error - - if len(args) == 1 { - id, err = identity.ReadLocal(repo, args[0]) - } else { - id, err = identity.GetUserIdentity(repo) - } - - if err != nil { - return err - } - - fmt.Printf("Id: %s\n", id.Id()) - fmt.Printf("Identity: %s\n", id.DisplayName()) - fmt.Printf("Name: %s\n", id.Name()) - fmt.Printf("Login: %s\n", id.Login()) - fmt.Printf("Email: %s\n", id.Email()) - fmt.Printf("Protected: %v\n", id.IsProtected()) - - return nil -} - -var idCmd = &cobra.Command{ - Use: "id []", - Short: "Display or change the user identity", - PreRunE: loadRepo, - RunE: runId, -} - -func init() { - RootCmd.AddCommand(idCmd) - selectCmd.Flags().SortFlags = false -} diff --git a/commands/user.go b/commands/user.go new file mode 100644 index 00000000..55ad813c --- /dev/null +++ b/commands/user.go @@ -0,0 +1,56 @@ +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 { + // TODO + return errors.New("this is not working yet, cache need to be hacked on") + 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("Protected: %v\n", id.IsProtected()) + + return nil +} + +var userCmd = &cobra.Command{ + Use: "user []", + Short: "Display or change the user identity", + PreRunE: loadRepo, + RunE: runUser, +} + +func init() { + RootCmd.AddCommand(userCmd) + userCmd.Flags().SortFlags = false +} diff --git a/commands/user_create.go b/commands/user_create.go new file mode 100644 index 00000000..50acac3e --- /dev/null +++ b/commands/user_create.go @@ -0,0 +1,76 @@ +package commands + +import ( + "fmt" + + "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) + + 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.Println() + 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 +} diff --git a/doc/man/git-bug-bridge-bridge.1 b/doc/man/git-bug-bridge-bridge.1 deleted file mode 100644 index edd0a5d3..00000000 --- a/doc/man/git-bug-bridge-bridge.1 +++ /dev/null @@ -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 diff --git a/doc/man/git-bug-id.1 b/doc/man/git-bug-id.1 deleted file mode 100644 index b3baf589..00000000 --- a/doc/man/git-bug-id.1 +++ /dev/null @@ -1,29 +0,0 @@ -.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" -.nh -.ad l - - -.SH NAME -.PP -git\-bug\-id \- Display or change the user identity - - -.SH SYNOPSIS -.PP -\fBgit\-bug id [] [flags]\fP - - -.SH DESCRIPTION -.PP -Display or change the user identity - - -.SH OPTIONS -.PP -\fB\-h\fP, \fB\-\-help\fP[=false] - help for id - - -.SH SEE ALSO -.PP -\fBgit\-bug(1)\fP diff --git a/doc/man/git-bug-user-create.1 b/doc/man/git-bug-user-create.1 new file mode 100644 index 00000000..a54adca4 --- /dev/null +++ b/doc/man/git-bug-user-create.1 @@ -0,0 +1,29 @@ +.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" +.nh +.ad l + + +.SH NAME +.PP +git\-bug\-user\-create \- Create a new identity + + +.SH SYNOPSIS +.PP +\fBgit\-bug user create [flags]\fP + + +.SH DESCRIPTION +.PP +Create a new identity + + +.SH OPTIONS +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for create + + +.SH SEE ALSO +.PP +\fBgit\-bug\-user(1)\fP diff --git a/doc/man/git-bug-user.1 b/doc/man/git-bug-user.1 new file mode 100644 index 00000000..eb074973 --- /dev/null +++ b/doc/man/git-bug-user.1 @@ -0,0 +1,29 @@ +.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" +.nh +.ad l + + +.SH NAME +.PP +git\-bug\-user \- Display or change the user identity + + +.SH SYNOPSIS +.PP +\fBgit\-bug user [] [flags]\fP + + +.SH DESCRIPTION +.PP +Display or change the user identity + + +.SH OPTIONS +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for user + + +.SH SEE ALSO +.PP +\fBgit\-bug(1)\fP, \fBgit\-bug\-user\-create(1)\fP diff --git a/doc/man/git-bug.1 b/doc/man/git-bug.1 index f3ad4729..523dafe5 100644 --- a/doc/man/git-bug.1 +++ b/doc/man/git-bug.1 @@ -31,4 +31,4 @@ the same git remote your are already using to collaborate with other peoples. .SH SEE ALSO .PP -\fBgit\-bug\-add(1)\fP, \fBgit\-bug\-bridge(1)\fP, \fBgit\-bug\-commands(1)\fP, \fBgit\-bug\-comment(1)\fP, \fBgit\-bug\-deselect(1)\fP, \fBgit\-bug\-id(1)\fP, \fBgit\-bug\-label(1)\fP, \fBgit\-bug\-ls(1)\fP, \fBgit\-bug\-ls\-label(1)\fP, \fBgit\-bug\-pull(1)\fP, \fBgit\-bug\-push(1)\fP, \fBgit\-bug\-select(1)\fP, \fBgit\-bug\-show(1)\fP, \fBgit\-bug\-status(1)\fP, \fBgit\-bug\-termui(1)\fP, \fBgit\-bug\-title(1)\fP, \fBgit\-bug\-version(1)\fP, \fBgit\-bug\-webui(1)\fP +\fBgit\-bug\-add(1)\fP, \fBgit\-bug\-bridge(1)\fP, \fBgit\-bug\-commands(1)\fP, \fBgit\-bug\-comment(1)\fP, \fBgit\-bug\-deselect(1)\fP, \fBgit\-bug\-label(1)\fP, \fBgit\-bug\-ls(1)\fP, \fBgit\-bug\-ls\-label(1)\fP, \fBgit\-bug\-pull(1)\fP, \fBgit\-bug\-push(1)\fP, \fBgit\-bug\-select(1)\fP, \fBgit\-bug\-show(1)\fP, \fBgit\-bug\-status(1)\fP, \fBgit\-bug\-termui(1)\fP, \fBgit\-bug\-title(1)\fP, \fBgit\-bug\-user(1)\fP, \fBgit\-bug\-version(1)\fP, \fBgit\-bug\-webui(1)\fP diff --git a/doc/md/git-bug.md b/doc/md/git-bug.md index 7183dd76..a5f61032 100644 --- a/doc/md/git-bug.md +++ b/doc/md/git-bug.md @@ -29,7 +29,6 @@ git-bug [flags] * [git-bug commands](git-bug_commands.md) - Display available commands * [git-bug comment](git-bug_comment.md) - Display or add comments * [git-bug deselect](git-bug_deselect.md) - Clear the implicitly selected bug -* [git-bug id](git-bug_id.md) - Display or change the user identity * [git-bug label](git-bug_label.md) - Display, add or remove labels * [git-bug ls](git-bug_ls.md) - List bugs * [git-bug ls-id](git-bug_ls-id.md) - List Bug Id @@ -41,6 +40,7 @@ git-bug [flags] * [git-bug status](git-bug_status.md) - Display or change a bug status * [git-bug termui](git-bug_termui.md) - Launch the terminal UI * [git-bug title](git-bug_title.md) - Display or change a title +* [git-bug user](git-bug_user.md) - Display or change the user identity * [git-bug version](git-bug_version.md) - Show git-bug version information * [git-bug webui](git-bug_webui.md) - Launch the web UI diff --git a/doc/md/git-bug_bridge_bridge.md b/doc/md/git-bug_bridge_bridge.md deleted file mode 100644 index d5803190..00000000 --- a/doc/md/git-bug_bridge_bridge.md +++ /dev/null @@ -1,22 +0,0 @@ -## git-bug bridge bridge - -Configure and use bridges to other bug trackers - -### Synopsis - -Configure and use bridges to other bug trackers - -``` -git-bug bridge bridge [flags] -``` - -### Options - -``` - -h, --help help for bridge -``` - -### SEE ALSO - -* [git-bug bridge](git-bug_bridge.md) - Configure and use bridges to other bug trackers - diff --git a/doc/md/git-bug_close.md b/doc/md/git-bug_close.md deleted file mode 100644 index ab95706d..00000000 --- a/doc/md/git-bug_close.md +++ /dev/null @@ -1,22 +0,0 @@ -## git-bug close - -Mark the bug as closed - -### Synopsis - -Mark the bug as closed - -``` -git-bug close [flags] -``` - -### Options - -``` - -h, --help help for close -``` - -### SEE ALSO - -* [git-bug](git-bug.md) - A bugtracker embedded in Git - diff --git a/doc/md/git-bug_id.md b/doc/md/git-bug_id.md deleted file mode 100644 index 09f8f276..00000000 --- a/doc/md/git-bug_id.md +++ /dev/null @@ -1,22 +0,0 @@ -## git-bug id - -Display or change the user identity - -### Synopsis - -Display or change the user identity - -``` -git-bug id [] [flags] -``` - -### Options - -``` - -h, --help help for id -``` - -### SEE ALSO - -* [git-bug](git-bug.md) - A bug tracker embedded in Git - diff --git a/doc/md/git-bug_new.md b/doc/md/git-bug_new.md deleted file mode 100644 index de6cffbf..00000000 --- a/doc/md/git-bug_new.md +++ /dev/null @@ -1,25 +0,0 @@ -## git-bug new - -Create a new bug - -### Synopsis - -Create a new bug - -``` -git-bug new [flags] -``` - -### Options - -``` - -t, --title string Provide a title to describe the issue - -m, --message string Provide a message to describe the issue - -F, --file string Take the message from the given file. Use - to read the message from the standard input - -h, --help help for new -``` - -### SEE ALSO - -* [git-bug](git-bug.md) - A bugtracker embedded in Git - diff --git a/doc/md/git-bug_open.md b/doc/md/git-bug_open.md deleted file mode 100644 index 38c6d6fa..00000000 --- a/doc/md/git-bug_open.md +++ /dev/null @@ -1,22 +0,0 @@ -## git-bug open - -Mark the bug as open - -### Synopsis - -Mark the bug as open - -``` -git-bug open [flags] -``` - -### Options - -``` - -h, --help help for open -``` - -### SEE ALSO - -* [git-bug](git-bug.md) - A bugtracker embedded in Git - diff --git a/doc/md/git-bug_user.md b/doc/md/git-bug_user.md new file mode 100644 index 00000000..5692b40a --- /dev/null +++ b/doc/md/git-bug_user.md @@ -0,0 +1,23 @@ +## git-bug user + +Display or change the user identity + +### Synopsis + +Display or change the user identity + +``` +git-bug user [] [flags] +``` + +### Options + +``` + -h, --help help for user +``` + +### SEE ALSO + +* [git-bug](git-bug.md) - A bug tracker embedded in Git +* [git-bug user create](git-bug_user_create.md) - Create a new identity + diff --git a/doc/md/git-bug_user_create.md b/doc/md/git-bug_user_create.md new file mode 100644 index 00000000..55a1159c --- /dev/null +++ b/doc/md/git-bug_user_create.md @@ -0,0 +1,22 @@ +## git-bug user create + +Create a new identity + +### Synopsis + +Create a new identity + +``` +git-bug user create [flags] +``` + +### Options + +``` + -h, --help help for create +``` + +### SEE ALSO + +* [git-bug user](git-bug_user.md) - Display or change the user identity + diff --git a/identity/identity.go b/identity/identity.go index a0800bcd..35edca18 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -204,8 +204,22 @@ func NewFromGitUser(repo repository.Repo) (*Identity, error) { return NewIdentity(name, email), nil } +// IsUserIdentitySet tell if the user identity is correctly set. +func IsUserIdentitySet(repo repository.RepoCommon) (bool, error) { + configs, err := repo.ReadConfigs(identityConfigKey) + if err != nil { + return false, err + } + + if len(configs) > 1 { + return false, fmt.Errorf("multiple identity config exist") + } + + return len(configs) == 1, nil +} + // SetUserIdentity store the user identity's id in the git config -func SetUserIdentity(repo repository.RepoCommon, identity Identity) error { +func SetUserIdentity(repo repository.RepoCommon, identity *Identity) error { return repo.StoreConfig(identityConfigKey, identity.Id()) } diff --git a/input/prompt.go b/input/prompt.go new file mode 100644 index 00000000..7a059b1a --- /dev/null +++ b/input/prompt.go @@ -0,0 +1,44 @@ +package input + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func PromptValue(name string, preValue string) (string, error) { + return promptValue(name, preValue, false) +} + +func PromptValueRequired(name string, preValue string) (string, error) { + return promptValue(name, preValue, true) +} + +func promptValue(name string, preValue string, required bool) (string, error) { + for { + if preValue != "" { + fmt.Printf("%s [%s]: ", name, preValue) + } else { + fmt.Printf("%s: ", name) + } + + line, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + return "", err + } + + line = strings.TrimSpace(line) + + if preValue != "" && line == "" { + return preValue, nil + } + + if required && line == "" { + fmt.Printf("%s is empty\n", name) + continue + } + + return line, nil + } +} diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 98d94a35..4ec1e472 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -450,26 +450,6 @@ _git-bug_deselect() noun_aliases=() } -_git-bug_id() -{ - last_command="git-bug_id" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - _git-bug_label_add() { last_command="git-bug_label_add" @@ -819,6 +799,47 @@ _git-bug_title() noun_aliases=() } +_git-bug_user_create() +{ + last_command="git-bug_user_create" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_git-bug_user() +{ + last_command="git-bug_user" + + command_aliases=() + + commands=() + commands+=("create") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _git-bug_version() { last_command="git-bug_version" @@ -883,7 +904,6 @@ _git-bug_root_command() commands+=("commands") commands+=("comment") commands+=("deselect") - commands+=("id") commands+=("label") commands+=("ls") commands+=("ls-id") @@ -895,6 +915,7 @@ _git-bug_root_command() commands+=("status") commands+=("termui") commands+=("title") + commands+=("user") commands+=("version") commands+=("webui") diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index d966b9be..1a705f7d 100644 --- a/misc/zsh_completion/git-bug +++ b/misc/zsh_completion/git-bug @@ -8,7 +8,7 @@ case $state in level1) case $words[1] in git-bug) - _arguments '1: :(add bridge commands comment deselect id label ls ls-label pull push select show status termui title version webui)' + _arguments '1: :(add bridge commands comment deselect label ls ls-label pull push select show status termui title user version webui)' ;; *) _arguments '*: :_files' @@ -32,6 +32,9 @@ case $state in title) _arguments '2: :(edit)' ;; + user) + _arguments '2: :(create)' + ;; *) _arguments '*: :_files' ;; -- cgit From 976af3a4e8382d03e9f2ccb57e2ed3b783294138 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 17 Feb 2019 17:59:54 +0100 Subject: identity: fix tests --- commands/select/select_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/select/select_test.go b/commands/select/select_test.go index f26350f8..29fdb3b8 100644 --- a/commands/select/select_test.go +++ b/commands/select/select_test.go @@ -5,7 +5,6 @@ import ( "time" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/test" "github.com/stretchr/testify/require" ) @@ -28,7 +27,8 @@ func TestSelect(t *testing.T) { // generate a bunch of bugs - rene := identity.NewBare("René Descartes", "rene@descartes.fr") + rene, err := repoCache.NewIdentity("René Descartes", "rene@descartes.fr") + require.NoError(t, err) for i := 0; i < 10; i++ { _, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil) -- cgit From 947ea63522610bd16c32cf70812c129eda9bbb02 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Mon, 18 Feb 2019 14:11:37 +0100 Subject: identity: wip caching --- cache/bug_cache.go | 12 +++++-- cache/bug_excerpt.go | 20 ++++------- cache/identity_cache.go | 6 +++- cache/identity_excerpt.go | 48 ++++++++++++++++++++++++++ cache/repo_cache.go | 88 ++++++++++++++++++++++++++++++++++------------- 5 files changed, 133 insertions(+), 41 deletions(-) create mode 100644 cache/identity_excerpt.go diff --git a/cache/bug_cache.go b/cache/bug_cache.go index ce46837a..53a96275 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -228,9 +228,17 @@ func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target } 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 { - return c.bug.CommitAsNeeded(c.repoCache.repo) + err := c.bug.CommitAsNeeded(c.repoCache.repo) + if err != nil { + return err + } + return c.notifyUpdated() } diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index daf89c4f..d3645322 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -17,18 +17,13 @@ type BugExcerpt struct { CreateUnixTime int64 EditUnixTime int64 - Status bug.Status - Author AuthorExcerpt - Labels []bug.Label + Status bug.Status + AuthorId string + Labels []bug.Label CreateMetadata map[string]string } -type AuthorExcerpt struct { - Name string - Login string -} - func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { return &BugExcerpt{ Id: b.Id(), @@ -37,12 +32,9 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { CreateUnixTime: b.FirstOp().GetUnixTime(), EditUnixTime: snap.LastEditUnix(), Status: snap.Status, - Author: AuthorExcerpt{ - Login: snap.Author.Login(), - Name: snap.Author.Name(), - }, - Labels: snap.Labels, - CreateMetadata: b.FirstOp().AllMetadata(), + AuthorId: snap.Author.Id(), + Labels: snap.Labels, + CreateMetadata: b.FirstOp().AllMetadata(), } } diff --git a/cache/identity_cache.go b/cache/identity_cache.go index 93b2dc4b..c49e9519 100644 --- a/cache/identity_cache.go +++ b/cache/identity_cache.go @@ -4,7 +4,7 @@ import ( "github.com/MichaelMure/git-bug/identity" ) -// IdentityCache is a wrapper around an Identity. It provide multiple functions: +// IdentityCache is a wrapper around an Identity for caching. type IdentityCache struct { *identity.Identity repoCache *RepoCache @@ -17,6 +17,10 @@ func NewIdentityCache(repoCache *RepoCache, id *identity.Identity) *IdentityCach } } +func (i *IdentityCache) notifyUpdated() error { + return i.repoCache.identityUpdated(i.Identity.Id()) +} + func (i *IdentityCache) Commit() error { return i.Identity.Commit(i.repoCache.repo) } diff --git a/cache/identity_excerpt.go b/cache/identity_excerpt.go new file mode 100644 index 00000000..7bc660b6 --- /dev/null +++ b/cache/identity_excerpt.go @@ -0,0 +1,48 @@ +package cache + +import ( + "encoding/gob" + + "github.com/MichaelMure/git-bug/identity" +) + +// 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 +} + +func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt { + return &IdentityExcerpt{ + Id: i.Id(), + Name: i.Name(), + Login: i.Login(), + } +} + +// Package initialisation used to register the type for (de)serialization +func init() { + gob.Register(IdentityExcerpt{}) +} + +/* + * 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] +} diff --git a/cache/repo_cache.go b/cache/repo_cache.go index f64a1b76..cec6f8b5 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -20,8 +20,12 @@ import ( "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 // RepoCache is a cache for a Repository. This cache has multiple functions: // @@ -40,12 +44,17 @@ const formatVersion = 1 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 } @@ -145,14 +154,27 @@ 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() +} + +// 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) return c.write() } // load will try to read from the disk the bug cache file func (c *RepoCache) load() error { - f, err := os.Open(cacheFilePath(c.repo)) + f, err := os.Open(bugCacheFilePath(c.repo)) if err != nil { return err } @@ -173,7 +195,7 @@ func (c *RepoCache) load() error { return fmt.Errorf("unknown cache format version %v", aux.Version) } - c.excerpts = aux.Excerpts + c.bugExcerpts = aux.Excerpts return nil } @@ -186,7 +208,7 @@ func (c *RepoCache) write() error { Excerpts map[string]*BugExcerpt }{ Version: formatVersion, - Excerpts: c.excerpts, + Excerpts: c.bugExcerpts, } encoder := gob.NewEncoder(&data) @@ -196,7 +218,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 } @@ -209,14 +231,18 @@ func (c *RepoCache) write() error { return f.Close() } -func cacheFilePath(repo repository.Repo) string { - return path.Join(repo.GetPath(), ".git", "git-bug", cacheFile) +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", bugCacheFile) } func (c *RepoCache) buildCache() error { _, _ = fmt.Fprintf(os.Stderr, "Building bug cache... ") - c.excerpts = make(map[string]*BugExcerpt) + c.bugExcerpts = make(map[string]*BugExcerpt) allBugs := bug.ReadAllLocalBugs(c.repo) @@ -226,7 +252,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.") @@ -257,7 +283,7 @@ 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) } @@ -281,7 +307,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) } @@ -306,7 +332,7 @@ func (c *RepoCache) QueryBugs(query *Query) []string { var filtered []*BugExcerpt - for _, excerpt := range c.excerpts { + for _, excerpt := range c.bugExcerpts { if query.Match(excerpt) { filtered = append(filtered, excerpt) } @@ -342,10 +368,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++ } @@ -353,11 +379,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 @@ -366,7 +387,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 } @@ -467,7 +488,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) } } @@ -615,6 +636,19 @@ func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) ( 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 { @@ -677,5 +711,11 @@ func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avat 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 } -- cgit From 54f9838f0ab22ce5285f21cdd117ad81c737d822 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Mon, 18 Feb 2019 23:16:47 +0100 Subject: identity: working identity cache --- cache/bug_excerpt.go | 35 +++++++++++-- cache/filter.go | 44 ++++++++++------ cache/identity_cache.go | 17 +++++- cache/repo_cache.go | 137 +++++++++++++++++++++++++++++++++++++++++++++--- commands/user.go | 2 +- identity/identity.go | 2 +- 6 files changed, 204 insertions(+), 33 deletions(-) diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index d3645322..e39c8310 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -4,6 +4,7 @@ import ( "encoding/gob" "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -17,25 +18,49 @@ type BugExcerpt struct { CreateUnixTime int64 EditUnixTime int64 - Status bug.Status - AuthorId string - Labels []bug.Label + Status bug.Status + 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, - AuthorId: snap.Author.Id(), 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 diff --git a/cache/filter.go b/cache/filter.go index 3cf4a991..3cbc132a 100644 --- a/cache/filter.go +++ b/cache/filter.go @@ -7,7 +7,7 @@ import ( ) // Filter is a functor that match a subset of bugs -type Filter func(excerpt *BugExcerpt) bool +type Filter func(repoCache *RepoCache, excerpt *BugExcerpt) bool // StatusFilter return a Filter that match a bug status func StatusFilter(query string) (Filter, error) { @@ -16,24 +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 func(repoCache *RepoCache, excerpt *BugExcerpt) bool { query = strings.ToLower(query) - return strings.Contains(strings.ToLower(excerpt.Author.Name), query) || - strings.Contains(strings.ToLower(excerpt.Author.Login), 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 @@ -45,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 } } @@ -59,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 } @@ -80,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 diff --git a/cache/identity_cache.go b/cache/identity_cache.go index c49e9519..2ae55f2d 100644 --- a/cache/identity_cache.go +++ b/cache/identity_cache.go @@ -21,10 +21,23 @@ 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 { - return i.Identity.Commit(i.repoCache.repo) + err := i.Identity.Commit(i.repoCache.repo) + if err != nil { + return err + } + return i.notifyUpdated() } func (i *IdentityCache) CommitAsNeeded() error { - return i.Identity.CommitAsNeeded(i.repoCache.repo) + err := i.Identity.CommitAsNeeded(i.repoCache.repo) + if err != nil { + return err + } + return i.notifyUpdated() } diff --git a/cache/repo_cache.go b/cache/repo_cache.go index cec6f8b5..d5768125 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -27,6 +27,14 @@ const identityCacheFile = "identity-cache" // 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 @@ -75,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 { @@ -156,7 +167,8 @@ func (c *RepoCache) bugUpdated(id string) error { 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 @@ -169,11 +181,21 @@ func (c *RepoCache) identityUpdated(id string) error { c.identitiesExcerpts[id] = NewIdentityExcerpt(i.Identity) - return c.write() + // we only need to write the identity cache + return c.writeIdentityCache() } -// load will try to read from the disk the bug cache file +// 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) loadBugCache() error { f, err := os.Open(bugCacheFilePath(c.repo)) if err != nil { return err @@ -191,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.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 { @@ -231,15 +293,63 @@ func (c *RepoCache) write() error { return f.Close() } +// 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", bugCacheFile) + 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.bugExcerpts = make(map[string]*BugExcerpt) @@ -333,7 +443,7 @@ func (c *RepoCache) QueryBugs(query *Query) []string { var filtered []*BugExcerpt for _, excerpt := range c.bugExcerpts { - if query.Match(excerpt) { + if query.Match(c, excerpt) { filtered = append(filtered, excerpt) } } @@ -463,11 +573,15 @@ func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title strin // Fetch retrieve update from a remote // This does not change the local bugs state func (c *RepoCache) Fetch(remote string) (string, error) { + // TODO: add identities + return bug.Fetch(c.repo, remote) } // 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 @@ -505,6 +619,8 @@ func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult { // Push update a remote with the local changes func (c *RepoCache) Push(remote string) (string, error) { + // TODO: add identities + return bug.Push(c.repo, remote) } @@ -655,6 +771,11 @@ func (c *RepoCache) SetUserIdentity(i *IdentityCache) error { 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 diff --git a/commands/user.go b/commands/user.go index 55ad813c..e7a1da63 100644 --- a/commands/user.go +++ b/commands/user.go @@ -38,7 +38,7 @@ func runUser(cmd *cobra.Command, args []string) error { fmt.Printf("Name: %s\n", id.Name()) fmt.Printf("Login: %s\n", id.Login()) fmt.Printf("Email: %s\n", id.Email()) - fmt.Printf("Protected: %v\n", id.IsProtected()) + // fmt.Printf("Protected: %v\n", id.IsProtected()) return nil } diff --git a/identity/identity.go b/identity/identity.go index 35edca18..193a3013 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -84,7 +84,7 @@ func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, erro return read(repo, ref) } -// read will load and parse an identity frdm git +// read will load and parse an identity from git func read(repo repository.Repo, ref string) (*Identity, error) { refSplit := strings.Split(ref, "/") id := refSplit[len(refSplit)-1] -- cgit From ffe35fece1b1526949107f154abc21a1a02fc74d Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Tue, 19 Feb 2019 00:10:40 +0100 Subject: identity: complete the graphql api --- graphql/graph/gen_graph.go | 105 ++++++++++++++++++++++++++++++++++++++-- graphql/resolvers/identity.go | 8 +++ graphql/schema/identity.graphql | 5 ++ 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/graphql/graph/gen_graph.go b/graphql/graph/gen_graph.go index fa8e4acb..548e808a 100644 --- a/graphql/graph/gen_graph.go +++ b/graphql/graph/gen_graph.go @@ -160,11 +160,13 @@ type ComplexityRoot struct { } Identity struct { + Id func(childComplexity int) int Name func(childComplexity int) int Email func(childComplexity int) int Login func(childComplexity int) int DisplayName func(childComplexity int) int AvatarUrl func(childComplexity int) int + IsProtected func(childComplexity int) int } LabelChangeOperation struct { @@ -294,11 +296,13 @@ type EditCommentOperationResolver interface { Date(ctx context.Context, obj *bug.EditCommentOperation) (time.Time, error) } type IdentityResolver interface { + ID(ctx context.Context, obj *identity.Interface) (string, error) Name(ctx context.Context, obj *identity.Interface) (*string, error) Email(ctx context.Context, obj *identity.Interface) (*string, error) Login(ctx context.Context, obj *identity.Interface) (*string, error) DisplayName(ctx context.Context, obj *identity.Interface) (string, error) AvatarURL(ctx context.Context, obj *identity.Interface) (*string, error) + IsProtected(ctx context.Context, obj *identity.Interface) (bool, error) } type LabelChangeOperationResolver interface { Date(ctx context.Context, obj *bug.LabelChangeOperation) (time.Time, error) @@ -1454,6 +1458,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.EditCommentOperation.Files(childComplexity), true + case "Identity.id": + if e.complexity.Identity.Id == nil { + break + } + + return e.complexity.Identity.Id(childComplexity), true + case "Identity.name": if e.complexity.Identity.Name == nil { break @@ -1489,6 +1500,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Identity.AvatarUrl(childComplexity), true + case "Identity.isProtected": + if e.complexity.Identity.IsProtected == nil { + break + } + + return e.complexity.Identity.IsProtected(childComplexity), true + case "LabelChangeOperation.hash": if e.complexity.LabelChangeOperation.Hash == nil { break @@ -4653,6 +4671,15 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Identity") + case "id": + wg.Add(1) + go func(i int, field graphql.CollectedField) { + out.Values[i] = ec._Identity_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + wg.Done() + }(i, field) case "name": wg.Add(1) go func(i int, field graphql.CollectedField) { @@ -4686,6 +4713,15 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, out.Values[i] = ec._Identity_avatarUrl(ctx, field, obj) wg.Done() }(i, field) + case "isProtected": + wg.Add(1) + go func(i int, field graphql.CollectedField) { + out.Values[i] = ec._Identity_isProtected(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + wg.Done() + }(i, field) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -4697,6 +4733,33 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, return out } +// nolint: vetshadow +func (ec *executionContext) _Identity_id(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Identity", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Identity().ID(rctx, obj) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return graphql.MarshalString(res) +} + // nolint: vetshadow func (ec *executionContext) _Identity_name(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) @@ -4836,6 +4899,33 @@ func (ec *executionContext) _Identity_avatarUrl(ctx context.Context, field graph return graphql.MarshalString(*res) } +// nolint: vetshadow +func (ec *executionContext) _Identity_isProtected(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Identity", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Identity().IsProtected(rctx, obj) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return graphql.MarshalBoolean(res) +} + var labelChangeOperationImplementors = []string{"LabelChangeOperation", "Operation", "Authored"} // nolint: gocyclo, errcheck, gas, goconst @@ -8974,10 +9064,11 @@ type Repository { ): BugConnection! bug(prefix: String!): Bug } - `}, &ast.Source{Name: "schema/identity.graphql", Input: `"""Represents an identity""" type Identity { + """The identifier for this identity""" + id: String! """The name of the person, if known.""" name: String """The email of the person, if known.""" @@ -8988,6 +9079,9 @@ type Identity { displayName: String! """An url to an avatar""" avatarUrl: String + """isProtected is true if the chain of git commits started to be signed. + If that's the case, only signed commit with a valid key for this identity can be added.""" + isProtected: Boolean! }`}, &ast.Source{Name: "schema/operations.graphql", Input: `"""An operation applied to a bug.""" interface Operation { @@ -9088,7 +9182,8 @@ type LabelChangeOperation implements Operation & Authored { added: [Label!]! removed: [Label!]! -}`}, +} +`}, &ast.Source{Name: "schema/root.graphql", Input: `scalar Time scalar Label scalar Hash @@ -9126,7 +9221,8 @@ type Mutation { setTitle(repoRef: String, prefix: String!, title: String!): Bug! commit(repoRef: String, prefix: String!): Bug! -}`}, +} +`}, &ast.Source{Name: "schema/timeline.graphql", Input: `"""An item in the timeline of events""" interface TimelineItem { """The hash of the source operation""" @@ -9212,5 +9308,6 @@ type SetTitleTimelineItem implements TimelineItem { date: Time! title: String! was: String! -}`}, +} +`}, ) diff --git a/graphql/resolvers/identity.go b/graphql/resolvers/identity.go index cc68197f..d4f9bba2 100644 --- a/graphql/resolvers/identity.go +++ b/graphql/resolvers/identity.go @@ -8,6 +8,10 @@ import ( type identityResolver struct{} +func (identityResolver) ID(ctx context.Context, obj *identity.Interface) (string, error) { + return (*obj).Id(), nil +} + func (identityResolver) Name(ctx context.Context, obj *identity.Interface) (*string, error) { return nilIfEmpty((*obj).Name()) } @@ -28,6 +32,10 @@ func (identityResolver) AvatarURL(ctx context.Context, obj *identity.Interface) return nilIfEmpty((*obj).AvatarUrl()) } +func (identityResolver) IsProtected(ctx context.Context, obj *identity.Interface) (bool, error) { + return (*obj).IsProtected(), nil +} + func nilIfEmpty(s string) (*string, error) { if s == "" { return nil, nil diff --git a/graphql/schema/identity.graphql b/graphql/schema/identity.graphql index 7c5ef126..18666f76 100644 --- a/graphql/schema/identity.graphql +++ b/graphql/schema/identity.graphql @@ -1,5 +1,7 @@ """Represents an identity""" type Identity { + """The identifier for this identity""" + id: String! """The name of the person, if known.""" name: String """The email of the person, if known.""" @@ -10,4 +12,7 @@ type Identity { displayName: String! """An url to an avatar""" avatarUrl: String + """isProtected is true if the chain of git commits started to be signed. + If that's the case, only signed commit with a valid key for this identity can be added.""" + isProtected: Boolean! } -- cgit From 71f9290fdae7551f3d3ada2179ece4084304d734 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Tue, 19 Feb 2019 00:19:27 +0100 Subject: identity: store the times properly --- bug/operation_pack.go | 2 +- cache/repo_cache.go | 2 +- commands/user.go | 2 -- identity/bare.go | 11 +++++++++-- identity/identity.go | 13 +++++++++---- identity/identity_stub.go | 4 ++-- identity/interface.go | 10 +++++++--- identity/version.go | 19 +++++++++++++------ repository/git.go | 12 ++++++++++++ repository/mock_repo.go | 10 ++++++++++ repository/repo.go | 7 +++++++ 11 files changed, 71 insertions(+), 21 deletions(-) diff --git a/bug/operation_pack.go b/bug/operation_pack.go index 55fc018e..5f3e9da8 100644 --- a/bug/operation_pack.go +++ b/bug/operation_pack.go @@ -138,7 +138,7 @@ 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 { diff --git a/cache/repo_cache.go b/cache/repo_cache.go index d5768125..e87119fe 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -712,7 +712,7 @@ func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) // preallocate but empty matching := make([]string, 0, 5) - for id := range c.identities { + for id := range c.identitiesExcerpts { if strings.HasPrefix(id, prefix) { matching = append(matching, id) } diff --git a/commands/user.go b/commands/user.go index e7a1da63..64482555 100644 --- a/commands/user.go +++ b/commands/user.go @@ -23,8 +23,6 @@ func runUser(cmd *cobra.Command, args []string) error { var id *cache.IdentityCache if len(args) == 1 { - // TODO - return errors.New("this is not working yet, cache need to be hacked on") id, err = backend.ResolveIdentityPrefix(args[0]) } else { id, err = backend.GetUserIdentity() diff --git a/identity/bare.go b/identity/bare.go index c54277a0..d3f7655a 100644 --- a/identity/bare.go +++ b/identity/bare.go @@ -65,6 +65,7 @@ func (i *Bare) UnmarshalJSON(data []byte) error { return nil } +// Id return the Identity identifier func (i *Bare) Id() string { // We don't have a proper ID at hand, so let's hash all the data to get one. // Hopefully the @@ -84,18 +85,22 @@ func (i *Bare) Id() string { return i.id } +// Name return the last version of the name func (i *Bare) Name() string { return i.name } +// Email return the last version of the email func (i *Bare) Email() string { return i.email } +// Login return the last version of the login func (i *Bare) Login() string { return i.login } +// AvatarUrl return the last version of the Avatar URL func (i *Bare) AvatarUrl() string { return i.avatarUrl } @@ -164,12 +169,14 @@ func (i *Bare) Validate() error { // Write the identity into the Repository. In particular, this ensure that // the Id is properly set. -func (i *Bare) Commit(repo repository.Repo) error { +func (i *Bare) Commit(repo repository.ClockedRepo) error { // Nothing to do, everything is directly embedded return nil } -func (i *Bare) CommitAsNeeded(repo repository.Repo) error { +// If needed, write the identity into the Repository. In particular, this +// ensure that the Id is properly set. +func (i *Bare) CommitAsNeeded(repo repository.ClockedRepo) error { // Nothing to do, everything is directly embedded return nil } diff --git a/identity/identity.go b/identity/identity.go index 193a3013..b9d40967 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/pkg/errors" @@ -252,8 +253,8 @@ func (i *Identity) AddVersion(version *Version) { // Write the identity into the Repository. In particular, this ensure that // the Id is properly set. -func (i *Identity) Commit(repo repository.Repo) error { - // Todo: check for mismatch between memory and commited data +func (i *Identity) Commit(repo repository.ClockedRepo) error { + // Todo: check for mismatch between memory and commit data if !i.NeedCommit() { return fmt.Errorf("can't commit an identity with no pending version") @@ -266,10 +267,14 @@ func (i *Identity) Commit(repo repository.Repo) error { for _, v := range i.versions { if v.commitHash != "" { i.lastCommit = v.commitHash - // ignore already commited versions + // ignore already commit versions continue } + // get the times where new versions starts to be valid + v.time = repo.EditTime() + v.unixTime = time.Now().Unix() + blobHash, err := v.Write(repo) if err != nil { return err @@ -319,7 +324,7 @@ func (i *Identity) Commit(repo repository.Repo) error { return nil } -func (i *Identity) CommitAsNeeded(repo repository.Repo) error { +func (i *Identity) CommitAsNeeded(repo repository.ClockedRepo) error { if !i.NeedCommit() { return nil } diff --git a/identity/identity_stub.go b/identity/identity_stub.go index e91600b0..830cfb99 100644 --- a/identity/identity_stub.go +++ b/identity/identity_stub.go @@ -76,11 +76,11 @@ func (IdentityStub) Validate() error { panic("identities needs to be properly loaded with identity.ReadLocal()") } -func (IdentityStub) Commit(repo repository.Repo) error { +func (IdentityStub) Commit(repo repository.ClockedRepo) error { panic("identities needs to be properly loaded with identity.ReadLocal()") } -func (i *IdentityStub) CommitAsNeeded(repo repository.Repo) error { +func (i *IdentityStub) CommitAsNeeded(repo repository.ClockedRepo) error { panic("identities needs to be properly loaded with identity.ReadLocal()") } diff --git a/identity/interface.go b/identity/interface.go index 55877c02..9fe4db4f 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -6,13 +6,17 @@ import ( ) type Interface interface { + // Id return the Identity identifier Id() string + // Name return the last version of the name Name() string + // Email return the last version of the email Email() string + // Login return the last version of the login Login() string + // AvatarUrl return the last version of the Avatar URL AvatarUrl() string - // Keys return the last version of the valid keys Keys() []Key @@ -28,11 +32,11 @@ type Interface interface { // Write the identity into the Repository. In particular, this ensure that // the Id is properly set. - Commit(repo repository.Repo) error + Commit(repo repository.ClockedRepo) error // If needed, write the identity into the Repository. In particular, this // ensure that the Id is properly set. - CommitAsNeeded(repo repository.Repo) error + CommitAsNeeded(repo repository.ClockedRepo) error // IsProtected return true if the chain of git commits started to be signed. // If that's the case, only signed commit with a valid key for this identity can be added. diff --git a/identity/version.go b/identity/version.go index 90bf83f2..6ffffd99 100644 --- a/identity/version.go +++ b/identity/version.go @@ -17,14 +17,11 @@ const formatVersion = 1 // Version is a complete set of information about an Identity at a point in time. type Version struct { - // Not serialized - commitHash git.Hash - - // Todo: add unix timestamp for ordering with identical lamport time ? - // The lamport time at which this version become effective // The reference time is the bug edition lamport clock - time lamport.Time + // It must be the first field in this struct due to https://github.com/golang/go/issues/599 + time lamport.Time + unixTime int64 name string email string @@ -43,6 +40,9 @@ type Version struct { // A set of arbitrary key/value to store metadata about a version or about an Identity in general. metadata map[string]string + + // Not serialized + commitHash git.Hash } type VersionJSON struct { @@ -50,6 +50,7 @@ type VersionJSON struct { FormatVersion uint `json:"version"` Time lamport.Time `json:"time"` + UnixTime int64 `json:"unix_time"` Name string `json:"name"` Email string `json:"email"` Login string `json:"login"` @@ -63,6 +64,7 @@ func (v *Version) MarshalJSON() ([]byte, error) { return json.Marshal(VersionJSON{ FormatVersion: formatVersion, Time: v.time, + UnixTime: v.unixTime, Name: v.name, Email: v.email, Login: v.login, @@ -85,6 +87,7 @@ func (v *Version) UnmarshalJSON(data []byte) error { } v.time = aux.Time + v.unixTime = aux.UnixTime v.name = aux.Name v.email = aux.Email v.login = aux.Login @@ -97,6 +100,10 @@ func (v *Version) UnmarshalJSON(data []byte) error { } func (v *Version) Validate() error { + if v.unixTime == 0 { + return fmt.Errorf("unix time not set") + } + if text.Empty(v.name) && text.Empty(v.login) { return fmt.Errorf("either name or login should be set") } diff --git a/repository/git.go b/repository/git.go index 10fddac3..836de8f0 100644 --- a/repository/git.go +++ b/repository/git.go @@ -20,6 +20,8 @@ const editClockFile = "/.git/git-bug/edit-clock" // ErrNotARepo is the error returned when the git repo root wan't be found var ErrNotARepo = errors.New("not a git repository") +var _ ClockedRepo = &GitRepo{} + // GitRepo represents an instance of a (local) git repository. type GitRepo struct { Path string @@ -440,11 +442,21 @@ func (repo *GitRepo) WriteClocks() error { return nil } +// CreateTime return the current value of the creation clock +func (repo *GitRepo) CreateTime() lamport.Time { + return repo.createClock.Time() +} + // CreateTimeIncrement increment the creation clock and return the new value. func (repo *GitRepo) CreateTimeIncrement() (lamport.Time, error) { return repo.createClock.Increment() } +// EditTime return the current value of the edit clock +func (repo *GitRepo) EditTime() lamport.Time { + return repo.editClock.Time() +} + // EditTimeIncrement increment the edit clock and return the new value. func (repo *GitRepo) EditTimeIncrement() (lamport.Time, error) { return repo.editClock.Increment() diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 74de8f57..97a4504f 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -9,6 +9,8 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) +var _ ClockedRepo = &mockRepoForTest{} + // mockRepoForTest defines an instance of Repo that can be used for testing. type mockRepoForTest struct { config map[string]string @@ -227,10 +229,18 @@ func (r *mockRepoForTest) WriteClocks() error { return nil } +func (r *mockRepoForTest) CreateTime() lamport.Time { + return r.createClock.Time() +} + func (r *mockRepoForTest) CreateTimeIncrement() (lamport.Time, error) { return r.createClock.Increment(), nil } +func (r *mockRepoForTest) EditTime() lamport.Time { + return r.editClock.Time() +} + func (r *mockRepoForTest) EditTimeIncrement() (lamport.Time, error) { return r.editClock.Increment(), nil } diff --git a/repository/repo.go b/repository/repo.go index 100feaed..8a66c320 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -83,6 +83,7 @@ type Repo interface { GetTreeHash(commit git.Hash) (git.Hash, error) } +// ClockedRepo is a Repo that also has Lamport clocks type ClockedRepo interface { Repo @@ -92,9 +93,15 @@ type ClockedRepo interface { // WriteClocks write the clocks values into the repo WriteClocks() error + // CreateTime return the current value of the creation clock + CreateTime() lamport.Time + // CreateTimeIncrement increment the creation clock and return the new value. CreateTimeIncrement() (lamport.Time, error) + // EditTime return the current value of the edit clock + EditTime() lamport.Time + // EditTimeIncrement increment the edit clock and return the new value. EditTimeIncrement() (lamport.Time, error) -- cgit From 719303226096c905e602cb620dfdfbcf8fe106ad Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Tue, 19 Feb 2019 01:44:21 +0100 Subject: identity: fix tests --- identity/identity.go | 2 +- identity/version.go | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/identity/identity.go b/identity/identity.go index b9d40967..809719e6 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -416,7 +416,7 @@ func (i *Identity) Validate() error { return err } - if v.time < lastTime { + if v.commitHash != "" && v.time < lastTime { return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.time) } diff --git a/identity/version.go b/identity/version.go index 6ffffd99..1259ae9c 100644 --- a/identity/version.go +++ b/identity/version.go @@ -100,9 +100,13 @@ func (v *Version) UnmarshalJSON(data []byte) error { } func (v *Version) Validate() error { - if v.unixTime == 0 { + // time must be set after a commit + if v.commitHash != "" && v.unixTime == 0 { return fmt.Errorf("unix time not set") } + if v.commitHash != "" && v.time == 0 { + return fmt.Errorf("lamport time not set") + } if text.Empty(v.name) && text.Empty(v.login) { return fmt.Errorf("either name or login should be set") -- cgit From b8caddddc7aaf34b2da61c590fd1d9a0fae024fb Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 23 Feb 2019 13:01:46 +0100 Subject: identity: some UX cleanup --- cache/bug_excerpt.go | 10 +++++----- cache/filter.go | 2 +- commands/root.go | 23 +++++++++++++++++++++++ commands/termui.go | 2 +- commands/user_create.go | 3 ++- identity/identity.go | 20 ++++++++++++++++---- input/prompt.go | 6 +++--- 7 files changed, 51 insertions(+), 15 deletions(-) diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index e39c8310..55518077 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -8,6 +8,11 @@ import ( "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 { @@ -63,11 +68,6 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { return e } -// Package initialisation used to register the type for (de)serialization -func init() { - gob.Register(BugExcerpt{}) -} - /* * Sorting */ diff --git a/cache/filter.go b/cache/filter.go index 3cbc132a..022a8ff2 100644 --- a/cache/filter.go +++ b/cache/filter.go @@ -6,7 +6,7 @@ import ( "github.com/MichaelMure/git-bug/bug" ) -// Filter is a functor that match a subset of bugs +// 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 diff --git a/commands/root.go b/commands/root.go index 797ae949..04bd6a83 100644 --- a/commands/root.go +++ b/commands/root.go @@ -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" ) @@ -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 +} diff --git a/commands/termui.go b/commands/termui.go index 4a029d6c..abfd165f 100644 --- a/commands/termui.go +++ b/commands/termui.go @@ -21,7 +21,7 @@ func runTermUI(cmd *cobra.Command, args []string) error { var termUICmd = &cobra.Command{ Use: "termui", Short: "Launch the terminal UI", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runTermUI, } diff --git a/commands/user_create.go b/commands/user_create.go index 50acac3e..2d007600 100644 --- a/commands/user_create.go +++ b/commands/user_create.go @@ -2,6 +2,7 @@ package commands import ( "fmt" + "os" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/input" @@ -57,7 +58,7 @@ func runUserCreate(cmd *cobra.Command, args []string) error { return err } - fmt.Println() + _, _ = fmt.Fprintln(os.Stderr) fmt.Println(id.Id()) return nil diff --git a/identity/identity.go b/identity/identity.go index 809719e6..114b954e 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -4,6 +4,7 @@ package identity import ( "encoding/json" "fmt" + "os" "strings" "time" @@ -20,6 +21,8 @@ const versionEntryName = "version" const identityConfigKey = "git-bug.identity" var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge") +var ErrNoIdentitySet = errors.New("user identity first needs to be created using \"git bug user create\"") +var ErrMultipleIdentitiesSet = errors.New("multiple user identities set") var _ Interface = &Identity{} @@ -213,7 +216,7 @@ func IsUserIdentitySet(repo repository.RepoCommon) (bool, error) { } if len(configs) > 1 { - return false, fmt.Errorf("multiple identity config exist") + return false, ErrMultipleIdentitiesSet } return len(configs) == 1, nil @@ -232,11 +235,11 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) { } if len(configs) == 0 { - return nil, fmt.Errorf("no identity set") + return nil, ErrNoIdentitySet } if len(configs) > 1 { - return nil, fmt.Errorf("multiple identity config exist") + return nil, ErrMultipleIdentitiesSet } var id string @@ -244,7 +247,16 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) { id = val } - return ReadLocal(repo, id) + i, err := ReadLocal(repo, id) + if err == ErrIdentityNotExist { + innerErr := repo.RmConfigs(identityConfigKey) + if innerErr != nil { + _, _ = fmt.Fprintln(os.Stderr, errors.Wrap(innerErr, "can't clear user identity").Error()) + } + return nil, err + } + + return i, nil } func (i *Identity) AddVersion(version *Version) { diff --git a/input/prompt.go b/input/prompt.go index 7a059b1a..6036c062 100644 --- a/input/prompt.go +++ b/input/prompt.go @@ -18,9 +18,9 @@ func PromptValueRequired(name string, preValue string) (string, error) { func promptValue(name string, preValue string, required bool) (string, error) { for { if preValue != "" { - fmt.Printf("%s [%s]: ", name, preValue) + _, _ = fmt.Fprintf(os.Stderr, "%s [%s]: ", name, preValue) } else { - fmt.Printf("%s: ", name) + _, _ = fmt.Fprintf(os.Stderr, "%s: ", name) } line, err := bufio.NewReader(os.Stdin).ReadString('\n') @@ -35,7 +35,7 @@ func promptValue(name string, preValue string, required bool) (string, error) { } if required && line == "" { - fmt.Printf("%s is empty\n", name) + _, _ = fmt.Fprintf(os.Stderr, "%s is empty\n", name) continue } -- cgit From dc1edf8e640f4d0f4d4a8bc01a7459ff8123705e Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 23 Feb 2019 13:02:18 +0100 Subject: generator cleanup --- doc/gen_manpage.go | 15 +++++++++++++-- doc/gen_markdown.go | 20 ++++++++++++++++---- misc/gen_bash_completion.go | 7 ++++--- misc/gen_zsh_completion.go | 3 ++- misc/random_bugs/main.go | 2 +- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/doc/gen_manpage.go b/doc/gen_manpage.go index 4147d915..0c7a501a 100644 --- a/doc/gen_manpage.go +++ b/doc/gen_manpage.go @@ -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) } diff --git a/doc/gen_markdown.go b/doc/gen_markdown.go index ee87d544..47194666 100644 --- a/doc/gen_markdown.go +++ b/doc/gen_markdown.go @@ -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) } diff --git a/misc/gen_bash_completion.go b/misc/gen_bash_completion.go index 8793556a..f2506606 100644 --- a/misc/gen_bash_completion.go +++ b/misc/gen_bash_completion.go @@ -4,19 +4,20 @@ package main import ( "fmt" - "github.com/MichaelMure/git-bug/commands" "log" "os" "path" + + "github.com/MichaelMure/git-bug/commands" ) func main() { cwd, _ := os.Getwd() - filepath := path.Join(cwd, "misc", "bash_completion", "git-bug") + dir := path.Join(cwd, "misc", "bash_completion", "git-bug") fmt.Println("Generating bash completion file ...") - err := commands.RootCmd.GenBashCompletionFile(filepath) + err := commands.RootCmd.GenBashCompletionFile(dir) if err != nil { log.Fatal(err) } diff --git a/misc/gen_zsh_completion.go b/misc/gen_zsh_completion.go index 41e08f9a..184cab43 100644 --- a/misc/gen_zsh_completion.go +++ b/misc/gen_zsh_completion.go @@ -4,10 +4,11 @@ package main import ( "fmt" - "github.com/MichaelMure/git-bug/commands" "log" "os" "path" + + "github.com/MichaelMure/git-bug/commands" ) func main() { diff --git a/misc/random_bugs/main.go b/misc/random_bugs/main.go index 31b12e45..274b3dac 100644 --- a/misc/random_bugs/main.go +++ b/misc/random_bugs/main.go @@ -17,7 +17,7 @@ func main() { panic(err) } - repo, err := repository.NewGitRepo(dir, func(repo *repository.GitRepo) error { + repo, err := repository.NewGitRepo(dir, func(repo repository.ClockedRepo) error { return nil }) if err != nil { -- cgit From 839b241f0c1b8ee670be207688228e8ea71602b7 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 23 Feb 2019 13:02:36 +0100 Subject: git: fix RmConfigs --- repository/git.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repository/git.go b/repository/git.go index 836de8f0..c982f820 100644 --- a/repository/git.go +++ b/repository/git.go @@ -204,7 +204,7 @@ func (repo *GitRepo) ReadConfigs(keyPrefix string) (map[string]string, error) { // RmConfigs remove all key/value pair matching the key prefix func (repo *GitRepo) RmConfigs(keyPrefix string) error { - _, err := repo.runGitCommand("config", "--remove-section", keyPrefix) + _, err := repo.runGitCommand("config", "--unset-all", keyPrefix) return err } -- cgit From ecf857a71a16dec6d200150215d33587a6d85a54 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 23 Feb 2019 13:02:53 +0100 Subject: makefile: add the clean-local-identities target for debugging --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3c6207c7..7fb04977 100644 --- a/Makefile +++ b/Makefile @@ -35,9 +35,14 @@ debug-webui: 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 -- cgit From b59623a835f1f922d06ff7212b5bf7825624d134 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 24 Feb 2019 12:56:42 +0100 Subject: bridge: fix typo --- bridge/core/bridge.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go index 96646edb..b849bec6 100644 --- a/bridge/core/bridge.go +++ b/bridge/core/bridge.go @@ -12,8 +12,8 @@ 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" @@ -268,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() @@ -287,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() @@ -306,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() @@ -325,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() -- cgit From e100ee9f10dd7f600b58bf3d24b36f9b286210d6 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 24 Feb 2019 12:58:04 +0100 Subject: github: fix 3 edge-case failures --- bridge/github/import.go | 95 +++++++++++++++++++++++++------------------ bridge/launchpad/import.go | 10 ++--- cache/bug_cache.go | 68 +++++++++++++++---------------- commands/comment_add.go | 2 +- commands/label_add.go | 2 +- commands/label_rm.go | 2 +- commands/status_close.go | 2 +- commands/status_open.go | 2 +- commands/title_edit.go | 2 +- graphql/resolvers/mutation.go | 10 ++--- termui/label_select.go | 2 +- termui/show_bug.go | 6 ++- termui/termui.go | 6 +-- 13 files changed, 114 insertions(+), 95 deletions(-) diff --git a/bridge/github/import.go b/bridge/github/import.go index 38278911..4627145e 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -108,13 +108,13 @@ 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) - b, err := repo.ResolveBugCreateMetadata(keyGithubId, parseId(issue.Id)) - if err != nil && err != bug.ErrBugNotExist { + author, err := gi.ensurePerson(repo, issue.Author) + if err != nil { return nil, err } - author, err := gi.makePerson(repo, issue.Author) - if err != nil { + b, err := repo.ResolveBugCreateMetadata(keyGithubId, parseId(issue.Id)) + if err != nil && err != bug.ErrBugNotExist { return nil, err } @@ -187,7 +187,7 @@ 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 } @@ -269,7 +269,7 @@ 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 } @@ -318,15 +318,15 @@ func (gi *githubImporter) ensureTimelineItem(repo *cache.RepoCache, b *cache.Bug case "LabeledEvent": id := parseId(item.LabeledEvent.Id) - _, err := b.ResolveTargetWithMetadata(keyGithubId, id) + _, err := b.ResolveOperationWithMetadata(keyGithubId, id) if err != cache.ErrNoMatchingOp { return err } - author, err := gi.makePerson(repo, item.LabeledEvent.Actor) + author, err := gi.ensurePerson(repo, item.LabeledEvent.Actor) if err != nil { return err } - _, err = b.ChangeLabelsRaw( + _, _, err = b.ChangeLabelsRaw( author, item.LabeledEvent.CreatedAt.Unix(), []string{ @@ -339,15 +339,15 @@ func (gi *githubImporter) ensureTimelineItem(repo *cache.RepoCache, b *cache.Bug case "UnlabeledEvent": id := parseId(item.UnlabeledEvent.Id) - _, err := b.ResolveTargetWithMetadata(keyGithubId, id) + _, err := b.ResolveOperationWithMetadata(keyGithubId, id) if err != cache.ErrNoMatchingOp { return err } - author, err := gi.makePerson(repo, item.UnlabeledEvent.Actor) + author, err := gi.ensurePerson(repo, item.UnlabeledEvent.Actor) if err != nil { return err } - _, err = b.ChangeLabelsRaw( + _, _, err = b.ChangeLabelsRaw( author, item.UnlabeledEvent.CreatedAt.Unix(), nil, @@ -360,52 +360,55 @@ func (gi *githubImporter) ensureTimelineItem(repo *cache.RepoCache, b *cache.Bug case "ClosedEvent": id := parseId(item.ClosedEvent.Id) - _, err := b.ResolveTargetWithMetadata(keyGithubId, id) + _, err := b.ResolveOperationWithMetadata(keyGithubId, id) if err != cache.ErrNoMatchingOp { return err } - author, err := gi.makePerson(repo, item.ClosedEvent.Actor) + author, err := gi.ensurePerson(repo, item.ClosedEvent.Actor) if err != nil { return err } - return b.CloseRaw( + _, 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 } - author, err := gi.makePerson(repo, item.ReopenedEvent.Actor) + author, err := gi.ensurePerson(repo, item.ReopenedEvent.Actor) if err != nil { return err } - return b.OpenRaw( + _, 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 } - author, err := gi.makePerson(repo, item.RenamedTitleEvent.Actor) + author, err := gi.ensurePerson(repo, item.RenamedTitleEvent.Actor) if err != nil { return err } - return b.SetTitleRaw( + _, 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) @@ -415,14 +418,14 @@ func (gi *githubImporter) ensureTimelineItem(repo *cache.RepoCache, b *cache.Bug } func (gi *githubImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache, cursor githubv4.String, comment issueComment, rootVariables map[string]interface{}) error { - target, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(comment.Id)) - if err != nil && err != cache.ErrNoMatchingOp { - // real error + author, err := gi.ensurePerson(repo, comment.Author) + if err != nil { return err } - author, err := gi.makePerson(repo, comment.Author) - if err != nil { + target, err := b.ResolveOperationWithMetadata(keyGithubId, parseId(comment.Id)) + if err != nil && err != cache.ErrNoMatchingOp { + // real error return err } @@ -439,7 +442,7 @@ func (gi *githubImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache if len(comment.UserContentEdits.Nodes) == 0 { if err == cache.ErrNoMatchingOp { - err = b.AddCommentRaw( + op, err := b.AddCommentRaw( author, comment.CreatedAt.Unix(), cleanupText(string(comment.Body)), @@ -448,7 +451,11 @@ func (gi *githubImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache keyGithubId: parseId(comment.Id), }, ) + if err != nil { + return err + } + target, err = op.Hash() if err != nil { return err } @@ -472,7 +479,7 @@ func (gi *githubImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache continue } - err = b.AddCommentRaw( + op, err := b.AddCommentRaw( author, comment.CreatedAt.Unix(), cleanupText(string(*edit.Diff)), @@ -485,6 +492,11 @@ func (gi *githubImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache if err != nil { return err } + + target, err = op.Hash() + if err != nil { + return err + } } err := gi.ensureCommentEdit(repo, b, target, edit) @@ -554,11 +566,7 @@ func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugC 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 @@ -570,9 +578,18 @@ func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugC fmt.Println("import edition") - editor, err := gi.makePerson(repo, edit.Editor) - if err != nil { - return err + var editor *cache.IdentityCache + if edit.Editor == nil { + // user account has been deleted, replacing it with the ghost + editor, err = gi.getGhost(repo) + if err != nil { + return err + } + } else { + editor, err = gi.ensurePerson(repo, edit.Editor) + if err != nil { + return err + } } switch { @@ -581,7 +598,7 @@ func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugC case edit.DeletedAt == nil: // comment edition - err := b.EditCommentRaw( + _, err := b.EditCommentRaw( editor, edit.CreatedAt.Unix(), target, @@ -598,8 +615,8 @@ func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugC return nil } -// makePerson create a bug.Person from the Github data -func (gi *githubImporter) makePerson(repo *cache.RepoCache, actor *actor) (*cache.IdentityCache, error) { +// 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 { diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go index b70d34f0..30ec5c3f 100644 --- a/bridge/launchpad/import.go +++ b/bridge/launchpad/import.go @@ -23,7 +23,7 @@ func (li *launchpadImporter) Init(conf core.Configuration) error { const keyLaunchpadID = "launchpad-id" const keyLaunchpadLogin = "launchpad-login" -func (li *launchpadImporter) makePerson(repo *cache.RepoCache, owner LPPerson) (*cache.IdentityCache, error) { +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 { @@ -67,7 +67,7 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error { return err } - owner, err := li.makePerson(repo, lpBug.Owner) + owner, err := li.ensurePerson(repo, lpBug.Owner) if err != nil { return err } @@ -100,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) } @@ -113,14 +113,14 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error { continue } - owner, err := li.makePerson(repo, lpMessage.Owner) + 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( + _, err = b.AddCommentRaw( owner, createdAt.Unix(), lpMessage.Content, diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 53a96275..5fc76658 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -57,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) @@ -86,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 { +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 *IdentityCache, unixTime int64, message string, files []git.Hash, metadata map[string]string) error { +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) { +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 *IdentityCache, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, error) { +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 { @@ -133,98 +133,98 @@ func (c *BugCache) ChangeLabelsRaw(author *IdentityCache, unixTime int64, added 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 { +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 *IdentityCache, unixTime int64, metadata map[string]string) error { +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 { +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 *IdentityCache, unixTime int64, metadata map[string]string) error { +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 { +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 *IdentityCache, unixTime int64, title string, metadata map[string]string) error { +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 { +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 *IdentityCache, unixTime int64, target git.Hash, message string, metadata map[string]string) error { +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 { diff --git a/commands/comment_add.go b/commands/comment_add.go index 58408bc5..80955da5 100644 --- a/commands/comment_add.go +++ b/commands/comment_add.go @@ -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 } diff --git a/commands/label_add.go b/commands/label_add.go index f04ed7d6..2b808311 100644 --- a/commands/label_add.go +++ b/commands/label_add.go @@ -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) diff --git a/commands/label_rm.go b/commands/label_rm.go index 36051ba1..6b48fe72 100644 --- a/commands/label_rm.go +++ b/commands/label_rm.go @@ -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) diff --git a/commands/status_close.go b/commands/status_close.go index 2b4f9602..03618cc6 100644 --- a/commands/status_close.go +++ b/commands/status_close.go @@ -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 } diff --git a/commands/status_open.go b/commands/status_open.go index 5e3029e2..f19847db 100644 --- a/commands/status_open.go +++ b/commands/status_open.go @@ -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 } diff --git a/commands/title_edit.go b/commands/title_edit.go index 6bbd1b0a..8848e7a8 100644 --- a/commands/title_edit.go +++ b/commands/title_edit.go @@ -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 } diff --git a/graphql/resolvers/mutation.go b/graphql/resolvers/mutation.go index ee79ce6b..be6956af 100644 --- a/graphql/resolvers/mutation.go +++ b/graphql/resolvers/mutation.go @@ -68,7 +68,7 @@ func (r mutationResolver) AddComment(ctx context.Context, repoRef *string, prefi return bug.Snapshot{}, err } - err = b.AddCommentWithFiles(message, files) + _, err = b.AddCommentWithFiles(message, files) if err != nil { return bug.Snapshot{}, err } @@ -89,7 +89,7 @@ func (r mutationResolver) ChangeLabels(ctx context.Context, repoRef *string, pre return bug.Snapshot{}, err } - _, err = b.ChangeLabels(added, removed) + _, _, err = b.ChangeLabels(added, removed) if err != nil { return bug.Snapshot{}, err } @@ -110,7 +110,7 @@ func (r mutationResolver) Open(ctx context.Context, repoRef *string, prefix stri return bug.Snapshot{}, err } - err = b.Open() + _, err = b.Open() if err != nil { return bug.Snapshot{}, err } @@ -131,7 +131,7 @@ func (r mutationResolver) Close(ctx context.Context, repoRef *string, prefix str return bug.Snapshot{}, err } - err = b.Close() + _, err = b.Close() if err != nil { return bug.Snapshot{}, err } @@ -152,7 +152,7 @@ func (r mutationResolver) SetTitle(ctx context.Context, repoRef *string, prefix return bug.Snapshot{}, err } - err = b.SetTitle(title) + _, err = b.SetTitle(title) if err != nil { return bug.Snapshot{}, err } diff --git a/termui/label_select.go b/termui/label_select.go index 026eba04..131703f9 100644 --- a/termui/label_select.go +++ b/termui/label_select.go @@ -296,7 +296,7 @@ func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error { } } - if _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil { + if _, _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil { ui.msgPopup.Activate(msgPopupErrorTitle, err.Error()) } diff --git a/termui/show_bug.go b/termui/show_bug.go index 55d86018..733c801e 100644 --- a/termui/show_bug.go +++ b/termui/show_bug.go @@ -622,9 +622,11 @@ func (sb *showBug) setTitle(g *gocui.Gui, v *gocui.View) error { func (sb *showBug) toggleOpenClose(g *gocui.Gui, v *gocui.View) error { switch sb.bug.Snapshot().Status { case bug.OpenStatus: - return sb.bug.Close() + _, err := sb.bug.Close() + return err case bug.ClosedStatus: - return sb.bug.Open() + _, err := sb.bug.Open() + return err default: return nil } diff --git a/termui/termui.go b/termui/termui.go index 78900de8..54d9df69 100644 --- a/termui/termui.go +++ b/termui/termui.go @@ -226,7 +226,7 @@ func addCommentWithEditor(bug *cache.BugCache) error { if err == input.ErrEmptyMessage { ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.") } else { - err := bug.AddComment(message) + _, err := bug.AddComment(message) if err != nil { return err } @@ -261,7 +261,7 @@ func editCommentWithEditor(bug *cache.BugCache, target git.Hash, preMessage stri } else if message == preMessage { ui.msgPopup.Activate(msgPopupErrorTitle, "No changes found, aborting.") } else { - err := bug.EditComment(target, message) + _, err := bug.EditComment(target, message) if err != nil { return err } @@ -298,7 +298,7 @@ func setTitleWithEditor(bug *cache.BugCache) error { } else if title == snap.Title { ui.msgPopup.Activate(msgPopupErrorTitle, "No change, aborting.") } else { - err := bug.SetTitle(title) + _, err := bug.SetTitle(title) if err != nil { return err } -- cgit From 268f6175fe7394f057a1b6b38c239c36e2c8619a Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 24 Feb 2019 13:06:03 +0100 Subject: github: simplify some code --- bridge/github/import.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/bridge/github/import.go b/bridge/github/import.go index 4627145e..f6b729b7 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -578,18 +578,9 @@ func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugC fmt.Println("import edition") - var editor *cache.IdentityCache - if edit.Editor == nil { - // user account has been deleted, replacing it with the ghost - editor, err = gi.getGhost(repo) - if err != nil { - return err - } - } else { - editor, err = gi.ensurePerson(repo, edit.Editor) - if err != nil { - return err - } + editor, err := gi.ensurePerson(repo, edit.Editor) + if err != nil { + return err } switch { -- cgit From 8bba6d1493fdf064ac9fede0a5098b1abe969052 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 24 Feb 2019 13:23:01 +0100 Subject: cache: fix ResolveIdentityImmutableMetadata byt storing metadata in IdentityExcerpt --- cache/identity_excerpt.go | 12 +++++++----- cache/repo_cache.go | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cache/identity_excerpt.go b/cache/identity_excerpt.go index 7bc660b6..0539a76b 100644 --- a/cache/identity_excerpt.go +++ b/cache/identity_excerpt.go @@ -12,15 +12,17 @@ import ( type IdentityExcerpt struct { Id string - Name string - Login 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(), + Id: i.Id(), + Name: i.Name(), + Login: i.Login(), + ImmutableMetadata: i.ImmutableMetadata(), } } diff --git a/cache/repo_cache.go b/cache/repo_cache.go index e87119fe..78633a69 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -735,8 +735,8 @@ func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) ( // preallocate but empty matching := make([]string, 0, 5) - for id, i := range c.identities { - if i.ImmutableMetadata()[key] == value { + for id, i := range c.identitiesExcerpts { + if i.ImmutableMetadata[key] == value { matching = append(matching, id) } } -- cgit From 7a80d8f849861a6033cd0765e5d85a52b08a8854 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 24 Feb 2019 14:17:52 +0100 Subject: commands: add a super-fast "user ls" command --- cache/bug_excerpt.go | 4 ++++ cache/identity_excerpt.go | 26 +++++++++++++++++++++--- cache/repo_cache.go | 12 +++++++++++ commands/user_list.go | 47 ++++++++++++++++++++++++++++++++++++++++++++ doc/man/git-bug-user-ls.1 | 33 +++++++++++++++++++++++++++++++ doc/man/git-bug-user.1 | 2 +- doc/md/git-bug_user.md | 1 + doc/md/git-bug_user_ls.md | 23 ++++++++++++++++++++++ identity/bare.go | 5 +++++ identity/identity.go | 17 ++++++++++++++++ identity/identity_stub.go | 4 ++++ identity/interface.go | 3 +++ misc/bash_completion/git-bug | 24 ++++++++++++++++++++++ misc/zsh_completion/git-bug | 2 +- 14 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 commands/user_list.go create mode 100644 doc/man/git-bug-user-ls.1 create mode 100644 doc/md/git-bug_user_ls.md diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index 55518077..fd06e51b 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -68,6 +68,10 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { return e } +func (b *BugExcerpt) HumanId() string { + return bug.FormatHumanID(b.Id) +} + /* * Sorting */ diff --git a/cache/identity_excerpt.go b/cache/identity_excerpt.go index 0539a76b..2a13bc60 100644 --- a/cache/identity_excerpt.go +++ b/cache/identity_excerpt.go @@ -2,10 +2,16 @@ 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. @@ -26,9 +32,23 @@ func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt { } } -// Package initialisation used to register the type for (de)serialization -func init() { - gob.Register(IdentityExcerpt{}) +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") } /* diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 78633a69..a50c745b 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -489,6 +489,12 @@ func (c *RepoCache) AllBugsIds() []string { return result } +// AllBugExcerpt return all known bug excerpt. +// This maps is read-only. +func (c *RepoCache) AllBugExcerpt() map[string]*BugExcerpt { + return c.bugExcerpts +} + // ValidLabels list valid labels // // Note: in the future, a proper label policy could be implemented where valid @@ -765,6 +771,12 @@ func (c *RepoCache) AllIdentityIds() []string { return result } +// AllIdentityExcerpt return all known identities excerpt. +// This maps is read-only. +func (c *RepoCache) AllIdentityExcerpt() map[string]*IdentityExcerpt { + return c.identitiesExcerpts +} + func (c *RepoCache) SetUserIdentity(i *IdentityCache) error { err := identity.SetUserIdentity(c.repo, i.Identity) if err != nil { diff --git a/commands/user_list.go b/commands/user_list.go new file mode 100644 index 00000000..4d6e5e12 --- /dev/null +++ b/commands/user_list.go @@ -0,0 +1,47 @@ +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" +) + +var ( + userLsVerbose bool +) + +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 _, i := range backend.AllIdentityExcerpt() { + 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 + + userLsCmd.Flags().BoolVarP(&userLsVerbose, "verbose", "v", false, + "Print extra information") +} diff --git a/doc/man/git-bug-user-ls.1 b/doc/man/git-bug-user-ls.1 new file mode 100644 index 00000000..6820a28d --- /dev/null +++ b/doc/man/git-bug-user-ls.1 @@ -0,0 +1,33 @@ +.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" +.nh +.ad l + + +.SH NAME +.PP +git\-bug\-user\-ls \- List identities + + +.SH SYNOPSIS +.PP +\fBgit\-bug user ls [flags]\fP + + +.SH DESCRIPTION +.PP +List identities + + +.SH OPTIONS +.PP +\fB\-v\fP, \fB\-\-verbose\fP[=false] + Print extra information + +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for ls + + +.SH SEE ALSO +.PP +\fBgit\-bug\-user(1)\fP diff --git a/doc/man/git-bug-user.1 b/doc/man/git-bug-user.1 index eb074973..acb9259f 100644 --- a/doc/man/git-bug-user.1 +++ b/doc/man/git-bug-user.1 @@ -26,4 +26,4 @@ Display or change the user identity .SH SEE ALSO .PP -\fBgit\-bug(1)\fP, \fBgit\-bug\-user\-create(1)\fP +\fBgit\-bug(1)\fP, \fBgit\-bug\-user\-create(1)\fP, \fBgit\-bug\-user\-ls(1)\fP diff --git a/doc/md/git-bug_user.md b/doc/md/git-bug_user.md index 5692b40a..f6a3d91b 100644 --- a/doc/md/git-bug_user.md +++ b/doc/md/git-bug_user.md @@ -20,4 +20,5 @@ git-bug user [] [flags] * [git-bug](git-bug.md) - A bug tracker embedded in Git * [git-bug user create](git-bug_user_create.md) - Create a new identity +* [git-bug user ls](git-bug_user_ls.md) - List identities diff --git a/doc/md/git-bug_user_ls.md b/doc/md/git-bug_user_ls.md new file mode 100644 index 00000000..9c9651f5 --- /dev/null +++ b/doc/md/git-bug_user_ls.md @@ -0,0 +1,23 @@ +## git-bug user ls + +List identities + +### Synopsis + +List identities + +``` +git-bug user ls [flags] +``` + +### Options + +``` + -v, --verbose Print extra information + -h, --help help for ls +``` + +### SEE ALSO + +* [git-bug user](git-bug_user.md) - Display or change the user identity + diff --git a/identity/bare.go b/identity/bare.go index d3f7655a..b6cbe491 100644 --- a/identity/bare.go +++ b/identity/bare.go @@ -85,6 +85,11 @@ func (i *Bare) Id() string { return i.id } +// HumanId return the Identity identifier truncated for human consumption +func (i *Bare) HumanId() string { + return FormatHumanID(i.Id()) +} + // Name return the last version of the name func (i *Bare) Name() string { return i.name diff --git a/identity/identity.go b/identity/identity.go index 114b954e..d57e8ce0 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -20,6 +20,9 @@ const identityRemoteRefPattern = "refs/remotes/%s/identities/" const versionEntryName = "version" const identityConfigKey = "git-bug.identity" +const idLength = 40 +const humanIdLength = 7 + var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge") var ErrNoIdentitySet = errors.New("user identity first needs to be created using \"git bug user create\"") var ErrMultipleIdentitiesSet = errors.New("multiple user identities set") @@ -93,6 +96,10 @@ func read(repo repository.Repo, ref string) (*Identity, error) { refSplit := strings.Split(ref, "/") id := refSplit[len(refSplit)-1] + if len(id) != idLength { + return nil, fmt.Errorf("invalid ref length") + } + hashes, err := repo.ListCommits(ref) // TODO: this is not perfect, it might be a command invoke error @@ -461,6 +468,16 @@ func (i *Identity) Id() string { return i.id } +// HumanId return the Identity identifier truncated for human consumption +func (i *Identity) HumanId() string { + return FormatHumanID(i.Id()) +} + +func FormatHumanID(id string) string { + format := fmt.Sprintf("%%.%ds", humanIdLength) + return fmt.Sprintf(format, id) +} + // Name return the last version of the name func (i *Identity) Name() string { return i.lastVersion().name diff --git a/identity/identity_stub.go b/identity/identity_stub.go index 830cfb99..1bfc18d0 100644 --- a/identity/identity_stub.go +++ b/identity/identity_stub.go @@ -44,6 +44,10 @@ func (i *IdentityStub) Id() string { return i.id } +func (i *IdentityStub) HumanId() string { + return FormatHumanID(i.Id()) +} + func (IdentityStub) Name() string { panic("identities needs to be properly loaded with identity.ReadLocal()") } diff --git a/identity/interface.go b/identity/interface.go index 9fe4db4f..d5c80543 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -9,6 +9,9 @@ type Interface interface { // Id return the Identity identifier Id() string + // HumanId return the Identity identifier truncated for human consumption + HumanId() string + // Name return the last version of the name Name() string // Email return the last version of the email diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 4ec1e472..237a0df0 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -819,6 +819,29 @@ _git-bug_user_create() noun_aliases=() } +_git-bug_user_ls() +{ + last_command="git-bug_user_ls" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--verbose") + flags+=("-v") + local_nonpersistent_flags+=("--verbose") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _git-bug_user() { last_command="git-bug_user" @@ -827,6 +850,7 @@ _git-bug_user() commands=() commands+=("create") + commands+=("ls") flags=() two_word_flags=() diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index 1a705f7d..6fc3cf8e 100644 --- a/misc/zsh_completion/git-bug +++ b/misc/zsh_completion/git-bug @@ -33,7 +33,7 @@ case $state in _arguments '2: :(edit)' ;; user) - _arguments '2: :(create)' + _arguments '2: :(create ls)' ;; *) _arguments '*: :_files' -- cgit From 304a3349300bba909b1f69e54c0d2d8a338994d1 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 24 Feb 2019 14:45:24 +0100 Subject: commands: add a "user adopt" command to use an existing identity --- commands/user_adopt.go | 48 ++++++++++++++++++++++++++++++++++++++++++++ commands/user_create.go | 4 ++++ doc/man/git-bug-user-adopt.1 | 29 ++++++++++++++++++++++++++ doc/man/git-bug-user.1 | 2 +- doc/md/git-bug_user_adopt.md | 22 ++++++++++++++++++++ misc/bash_completion/git-bug | 21 +++++++++++++++++++ misc/zsh_completion/git-bug | 2 +- 7 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 commands/user_adopt.go create mode 100644 doc/man/git-bug-user-adopt.1 create mode 100644 doc/md/git-bug_user_adopt.md diff --git a/commands/user_adopt.go b/commands/user_adopt.go new file mode 100644 index 00000000..5313e366 --- /dev/null +++ b/commands/user_adopt.go @@ -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 ", + 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 +} diff --git a/commands/user_create.go b/commands/user_create.go index 2d007600..00ec5b9b 100644 --- a/commands/user_create.go +++ b/commands/user_create.go @@ -18,6 +18,10 @@ func runUserCreate(cmd *cobra.Command, args []string) error { 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 diff --git a/doc/man/git-bug-user-adopt.1 b/doc/man/git-bug-user-adopt.1 new file mode 100644 index 00000000..f61bc5d4 --- /dev/null +++ b/doc/man/git-bug-user-adopt.1 @@ -0,0 +1,29 @@ +.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" +.nh +.ad l + + +.SH NAME +.PP +git\-bug\-user\-adopt \- Adopt an existing identity as your own. + + +.SH SYNOPSIS +.PP +\fBgit\-bug user adopt [flags]\fP + + +.SH DESCRIPTION +.PP +Adopt an existing identity as your own. + + +.SH OPTIONS +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for adopt + + +.SH SEE ALSO +.PP +\fBgit\-bug\-user(1)\fP diff --git a/doc/man/git-bug-user.1 b/doc/man/git-bug-user.1 index acb9259f..65988c9b 100644 --- a/doc/man/git-bug-user.1 +++ b/doc/man/git-bug-user.1 @@ -26,4 +26,4 @@ Display or change the user identity .SH SEE ALSO .PP -\fBgit\-bug(1)\fP, \fBgit\-bug\-user\-create(1)\fP, \fBgit\-bug\-user\-ls(1)\fP +\fBgit\-bug(1)\fP, \fBgit\-bug\-user\-adopt(1)\fP, \fBgit\-bug\-user\-create(1)\fP, \fBgit\-bug\-user\-ls(1)\fP diff --git a/doc/md/git-bug_user_adopt.md b/doc/md/git-bug_user_adopt.md new file mode 100644 index 00000000..f89cdf68 --- /dev/null +++ b/doc/md/git-bug_user_adopt.md @@ -0,0 +1,22 @@ +## git-bug user adopt + +Adopt an existing identity as your own. + +### Synopsis + +Adopt an existing identity as your own. + +``` +git-bug user adopt [flags] +``` + +### Options + +``` + -h, --help help for adopt +``` + +### SEE ALSO + +* [git-bug user](git-bug_user.md) - Display or change the user identity. + diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 237a0df0..c83d33ae 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -799,6 +799,26 @@ _git-bug_title() noun_aliases=() } +_git-bug_user_adopt() +{ + last_command="git-bug_user_adopt" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _git-bug_user_create() { last_command="git-bug_user_create" @@ -849,6 +869,7 @@ _git-bug_user() command_aliases=() commands=() + commands+=("adopt") commands+=("create") commands+=("ls") diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index 6fc3cf8e..232cd3c1 100644 --- a/misc/zsh_completion/git-bug +++ b/misc/zsh_completion/git-bug @@ -33,7 +33,7 @@ case $state in _arguments '2: :(edit)' ;; user) - _arguments '2: :(create ls)' + _arguments '2: :(adopt create ls)' ;; *) _arguments '*: :_files' -- cgit From 2fd5f71b592eeebd0c0c4c770a7dac88d7eae80a Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 24 Feb 2019 14:46:08 +0100 Subject: commands: add a "." at the end of Short commands usage --- commands/add.go | 2 +- commands/bridge.go | 2 +- commands/bridge_configure.go | 2 +- commands/bridge_pull.go | 2 +- commands/bridge_rm.go | 2 +- commands/commands.go | 2 +- commands/comment.go | 2 +- commands/comment_add.go | 2 +- commands/deselect.go | 2 +- commands/label.go | 2 +- commands/label_add.go | 2 +- commands/label_rm.go | 2 +- commands/ls-labels.go | 2 +- commands/ls.go | 2 +- commands/pull.go | 2 +- commands/push.go | 2 +- commands/root.go | 2 +- commands/select.go | 2 +- commands/show.go | 2 +- commands/status.go | 2 +- commands/status_close.go | 2 +- commands/status_open.go | 2 +- commands/termui.go | 2 +- commands/title.go | 2 +- commands/title_edit.go | 2 +- commands/user.go | 2 +- commands/user_create.go | 2 +- commands/user_list.go | 2 +- commands/version.go | 2 +- commands/webui.go | 2 +- doc/man/git-bug-ls-id.1 | 29 +++++++++++++++++++++++++++++ doc/md/git-bug.md | 38 +++++++++++++++++++------------------- doc/md/git-bug_add.md | 6 +++--- doc/md/git-bug_bridge.md | 12 ++++++------ doc/md/git-bug_bridge_configure.md | 6 +++--- doc/md/git-bug_bridge_pull.md | 6 +++--- doc/md/git-bug_bridge_rm.md | 6 +++--- doc/md/git-bug_commands.md | 6 +++--- doc/md/git-bug_comment.md | 8 ++++---- doc/md/git-bug_comment_add.md | 6 +++--- doc/md/git-bug_deselect.md | 6 +++--- doc/md/git-bug_label.md | 10 +++++----- doc/md/git-bug_label_add.md | 6 +++--- doc/md/git-bug_label_rm.md | 6 +++--- doc/md/git-bug_ls-id.md | 22 ++++++++++++++++++++++ doc/md/git-bug_ls-label.md | 4 ++-- doc/md/git-bug_ls.md | 4 ++-- doc/md/git-bug_pull.md | 6 +++--- doc/md/git-bug_push.md | 6 +++--- doc/md/git-bug_select.md | 6 +++--- doc/md/git-bug_show.md | 6 +++--- doc/md/git-bug_status.md | 10 +++++----- doc/md/git-bug_status_close.md | 6 +++--- doc/md/git-bug_status_open.md | 6 +++--- doc/md/git-bug_termui.md | 6 +++--- doc/md/git-bug_title.md | 8 ++++---- doc/md/git-bug_title_edit.md | 6 +++--- doc/md/git-bug_user.md | 11 ++++++----- doc/md/git-bug_user_create.md | 6 +++--- doc/md/git-bug_user_ls.md | 6 +++--- doc/md/git-bug_version.md | 6 +++--- doc/md/git-bug_webui.md | 6 +++--- 62 files changed, 197 insertions(+), 145 deletions(-) create mode 100644 doc/man/git-bug-ls-id.1 create mode 100644 doc/md/git-bug_ls-id.md diff --git a/commands/add.go b/commands/add.go index 54ede126..ea40227c 100644 --- a/commands/add.go +++ b/commands/add.go @@ -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, } diff --git a/commands/bridge.go b/commands/bridge.go index a473776d..2566fd06 100644 --- a/commands/bridge.go +++ b/commands/bridge.go @@ -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, diff --git a/commands/bridge_configure.go b/commands/bridge_configure.go index ef499f1f..ce10d9af 100644 --- a/commands/bridge_configure.go +++ b/commands/bridge_configure.go @@ -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, } diff --git a/commands/bridge_pull.go b/commands/bridge_pull.go index 669a6713..9b251479 100644 --- a/commands/bridge_pull.go +++ b/commands/bridge_pull.go @@ -38,7 +38,7 @@ func runBridgePull(cmd *cobra.Command, args []string) error { var bridgePullCmd = &cobra.Command{ Use: "pull []", - Short: "Pull updates", + Short: "Pull updates.", PreRunE: loadRepo, RunE: runBridgePull, } diff --git a/commands/bridge_rm.go b/commands/bridge_rm.go index 172fc0d8..80a831ff 100644 --- a/commands/bridge_rm.go +++ b/commands/bridge_rm.go @@ -25,7 +25,7 @@ func runBridgeRm(cmd *cobra.Command, args []string) error { var bridgeRmCmd = &cobra.Command{ Use: "rm name ", - Short: "Delete a configured bridge", + Short: "Delete a configured bridge.", PreRunE: loadRepo, RunE: runBridgeRm, Args: cobra.ExactArgs(1), diff --git a/commands/commands.go b/commands/commands.go index e48cd542..a30c38a5 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -61,7 +61,7 @@ func runCommands(cmd *cobra.Command, args []string) error { var commandsCmd = &cobra.Command{ Use: "commands [