diff options
47 files changed, 1366 insertions, 636 deletions
@@ -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/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/tests/graphql_test.go b/graphql/graphql_test.go index 77008628..90381987 100644 --- a/tests/graphql_test.go +++ b/graphql/graphql_test.go @@ -1,18 +1,18 @@ -package tests +package graphql import ( "net/http/httptest" "testing" - "github.com/MichaelMure/git-bug/graphql" "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 := createFilledRepo(10) + repo := test.CreateFilledRepo(10) - handler, err := graphql.NewHandler(repo) + handler, err := NewHandler(repo) if err != nil { t.Fatal(err) } 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/bug.graphql b/graphql/schema/bug.graphql index 27bbba99..9530c576 100644 --- a/graphql/bug.graphql +++ b/graphql/schema/bug.graphql @@ -1,21 +1,7 @@ -"""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! + author: Identity! """The message of this comment.""" message: String! @@ -47,7 +33,7 @@ type Bug { status: Status! title: String! labels: [Label!]! - author: Person! + author: Identity! createdAt: Time! lastEdit: Time! 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/operations.graphql b/graphql/schema/operations.graphql index 420a9e12..2b206418 100644 --- a/graphql/operations.graphql +++ b/graphql/schema/operations.graphql @@ -3,7 +3,7 @@ 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! } @@ -30,7 +30,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! @@ -43,7 +43,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! @@ -55,7 +55,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! @@ -67,7 +67,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! @@ -80,7 +80,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! @@ -91,7 +91,7 @@ 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! diff --git a/graphql/root.graphql b/graphql/schema/root.graphql index fd8419fa..56558f7c 100644 --- a/graphql/root.graphql +++ b/graphql/schema/root.graphql @@ -17,7 +17,7 @@ type PageInfo { """An object that has an author.""" interface Authored { """The author of this object.""" - author: Person! + author: Identity! } type Query { diff --git a/graphql/timeline.graphql b/graphql/schema/timeline.graphql index 75f72305..29ed6e60 100644 --- a/graphql/timeline.graphql +++ b/graphql/schema/timeline.graphql @@ -32,7 +32,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!]! @@ -46,7 +46,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!]! @@ -60,7 +60,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!]! @@ -70,7 +70,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! } @@ -79,7 +79,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/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/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 +} |