diff --git a/bridge/github/config.go b/bridge/github/config.go index 8aa416389..b881c585e 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -29,7 +29,10 @@ func (*Github) Configure(repo repository.RepoCommon) (core.Configuration, error) conf := make(core.Configuration) fmt.Println() - fmt.Println("git-bug will generate an access token in your Github profile. Your credential are not stored and are only used to generate the token. The token is stored in the repository git config.") + fmt.Println("git-bug will now generate an access token in your Github profile. Your credential are not stored and are only used to generate the token. The token is stored in the repository git config.") + fmt.Println() + fmt.Println("The token will have the following scopes:") + fmt.Println(" - user:email: to be able to read public-only users email") // fmt.Println("The token will have the \"repo\" permission, giving it read/write access to your repositories and issues. There is no narrower scope available, sorry :-|") fmt.Println() @@ -120,7 +123,9 @@ func requestTokenWith2FA(note, username, password, otpCode string) (*http.Respon Note string `json:"note"` Fingerprint string `json:"fingerprint"` }{ - // Scopes: []string{"repo"}, + // user:email is requested to be able to read public emails + // - a private email will stay private, even with this token + Scopes: []string{"user:email"}, Note: note, Fingerprint: randomFingerprint(), } diff --git a/bridge/github/import.go b/bridge/github/import.go index ed0135e76..93390408d 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -565,9 +565,29 @@ func (gi *githubImporter) makePerson(actor *actor) bug.Person { if actor == nil { return gi.ghost } + var name string + var email string + + switch actor.Typename { + case "User": + if actor.User.Name != nil { + name = string(*(actor.User.Name)) + } + email = string(actor.User.Email) + case "Organization": + if actor.Organization.Name != nil { + name = string(*(actor.Organization.Name)) + } + if actor.Organization.Email != nil { + email = string(*(actor.Organization.Email)) + } + case "Bot": + } return bug.Person{ - Name: string(actor.Login), + Name: name, + Email: email, + Login: string(actor.Login), AvatarUrl: string(actor.AvatarUrl), } } @@ -584,9 +604,16 @@ func (gi *githubImporter) fetchGhost() error { return err } + var name string + if q.User.Name != nil { + name = string(*q.User.Name) + } + gi.ghost = bug.Person{ - Name: string(q.User.Login), + Name: name, + Login: string(q.User.Login), AvatarUrl: string(q.User.AvatarUrl), + Email: string(q.User.Email), } return nil diff --git a/bridge/github/import_query.go b/bridge/github/import_query.go index e1dcff644..59799f6a7 100644 --- a/bridge/github/import_query.go +++ b/bridge/github/import_query.go @@ -10,8 +10,17 @@ type pageInfo struct { } type actor struct { + Typename githubv4.String `graphql:"__typename"` Login githubv4.String AvatarUrl githubv4.String + User struct { + Name *githubv4.String + Email githubv4.String + } `graphql:"... on User"` + Organization struct { + Name *githubv4.String + Email *githubv4.String + } `graphql:"... on Organization"` } type actorEvent struct { @@ -152,5 +161,10 @@ type commentEditQuery struct { } type userQuery struct { - User actor `graphql:"user(login: $login)"` + User struct { + Login githubv4.String + AvatarUrl githubv4.String + Name *githubv4.String + Email githubv4.String + } `graphql:"user(login: $login)"` } diff --git a/bug/person.go b/bug/person.go index c79e99357..449e22623 100644 --- a/bug/person.go +++ b/bug/person.go @@ -12,6 +12,7 @@ import ( type Person struct { Name string `json:"name"` Email string `json:"email"` + Login string `json:"login"` AvatarUrl string `json:"avatar_url"` } @@ -38,12 +39,15 @@ func GetUser(repo repository.Repo) (Person, error) { // Match tell is the Person match the given query string func (p Person) Match(query string) bool { - return strings.Contains(strings.ToLower(p.Name), strings.ToLower(query)) + 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) { - return fmt.Errorf("name is not set") + 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") { @@ -54,6 +58,14 @@ func (p Person) Validate() error { 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") } @@ -69,6 +81,15 @@ func (p Person) Validate() error { return nil } -func (p Person) String() string { - return fmt.Sprintf("%s <%s>", p.Name, p.Email) +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/commands/ls.go b/commands/ls.go index ad45eefad..1fababaad 100644 --- a/commands/ls.go +++ b/commands/ls.go @@ -59,7 +59,7 @@ func runLsBug(cmd *cobra.Command, args []string) error { // truncate + pad if needed titleFmt := fmt.Sprintf("%-50.50s", snapshot.Title) - authorFmt := fmt.Sprintf("%-15.15s", author.Name) + authorFmt := fmt.Sprintf("%-15.15s", author.DisplayName()) fmt.Printf("%s %s\t%s\t%s\t%s\n", colors.Cyan(b.HumanId()), diff --git a/commands/show.go b/commands/show.go index b1b7b432c..c061b69bc 100644 --- a/commands/show.go +++ b/commands/show.go @@ -39,7 +39,7 @@ func runShowBug(cmd *cobra.Command, args []string) error { ) fmt.Printf("%s opened this issue %s\n\n", - colors.Magenta(firstComment.Author.Name), + colors.Magenta(firstComment.Author.DisplayName()), firstComment.FormatTimeRel(), ) @@ -59,7 +59,7 @@ func runShowBug(cmd *cobra.Command, args []string) error { fmt.Printf("%s#%d %s <%s>\n\n", indent, i, - comment.Author.Name, + comment.Author.DisplayName(), comment.Author.Email, ) diff --git a/graphql/gqlgen.yml b/graphql/gqlgen.yml index 3932eafea..19bf686e4 100644 --- a/graphql/gqlgen.yml +++ b/graphql/gqlgen.yml @@ -15,6 +15,15 @@ models: 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 Label: model: github.com/MichaelMure/git-bug/bug.Label Hash: @@ -27,6 +36,8 @@ models: model: github.com/MichaelMure/git-bug/bug.SetTitleOperation AddCommentOperation: model: github.com/MichaelMure/git-bug/bug.AddCommentOperation + EditCommentOperation: + model: github.com/MichaelMure/git-bug/bug.EditCommentOperation SetStatusOperation: model: github.com/MichaelMure/git-bug/bug.SetStatusOperation LabelChangeOperation: diff --git a/graphql/graph/gen_graph.go b/graphql/graph/gen_graph.go index 7478383ec..82862d697 100644 --- a/graphql/graph/gen_graph.go +++ b/graphql/graph/gen_graph.go @@ -45,6 +45,7 @@ type ResolverRoot interface { LabelChangeOperation() LabelChangeOperationResolver LabelChangeTimelineItem() LabelChangeTimelineItemResolver Mutation() MutationResolver + Person() PersonResolver Query() QueryResolver Repository() RepositoryResolver SetStatusOperation() SetStatusOperationResolver @@ -200,9 +201,11 @@ type ComplexityRoot struct { } Person struct { - Email func(childComplexity int) int - Name func(childComplexity int) int - AvatarUrl func(childComplexity int) int + Name func(childComplexity int) int + Email func(childComplexity int) int + Login func(childComplexity int) int + DisplayName func(childComplexity int) int + AvatarUrl func(childComplexity int) int } Query struct { @@ -301,6 +304,13 @@ 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) @@ -1650,6 +1660,13 @@ 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 @@ -1657,12 +1674,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Person.Email(childComplexity), true - case "Person.name": - if e.complexity.Person.Name == nil { + case "Person.login": + if e.complexity.Person.Login == nil { break } - return e.complexity.Person.Name(childComplexity), true + 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 { @@ -5280,6 +5304,7 @@ var personImplementors = []string{"Person"} 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 { @@ -5288,26 +5313,69 @@ func (ec *executionContext) _Person(ctx context.Context, sel ast.SelectionSet, o switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Person") - case "email": - out.Values[i] = ec._Person_email(ctx, field, obj) case "name": - out.Values[i] = ec._Person_name(ctx, field, obj) + 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": - out.Values[i] = ec._Person_avatarUrl(ctx, field, obj) + 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 { + rctx := &graphql.ResolverContext{ + Object: "Person", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) { + return ec.resolvers.Person().Name(ctx, obj) + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + rctx.Result = res + + 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 { rctx := &graphql.ResolverContext{ @@ -5317,18 +5385,22 @@ func (ec *executionContext) _Person_email(ctx context.Context, field graphql.Col } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) { - return obj.Email, nil + return ec.resolvers.Person().Email(ctx, obj) }) if resTmp == nil { return graphql.Null } - res := resTmp.(string) + res := resTmp.(*string) rctx.Result = res - return graphql.MarshalString(res) + + if res == nil { + return graphql.Null + } + return graphql.MarshalString(*res) } // nolint: vetshadow -func (ec *executionContext) _Person_name(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler { +func (ec *executionContext) _Person_login(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler { rctx := &graphql.ResolverContext{ Object: "Person", Args: nil, @@ -5336,7 +5408,30 @@ func (ec *executionContext) _Person_name(ctx context.Context, field graphql.Coll } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) { - return obj.Name, nil + return ec.resolvers.Person().Login(ctx, obj) + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + rctx.Result = res + + 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 { + rctx := &graphql.ResolverContext{ + Object: "Person", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) { + return obj.DisplayName(), nil }) if resTmp == nil { if !ec.HasError(rctx) { @@ -5358,14 +5453,18 @@ func (ec *executionContext) _Person_avatarUrl(ctx context.Context, field graphql } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) { - return obj.AvatarUrl, nil + return ec.resolvers.Person().AvatarURL(ctx, obj) }) if resTmp == nil { return graphql.Null } - res := resTmp.(string) + res := resTmp.(*string) rctx.Result = res - return graphql.MarshalString(res) + + if res == nil { + return graphql.Null + } + return graphql.MarshalString(*res) } var queryImplementors = []string{"Query"} @@ -7922,11 +8021,17 @@ type PageInfo { """Represents an person in a git object.""" type Person { - """The email of the person.""" + """The name of the person, if known.""" + name: String + + """The email of the person, if known.""" email: String - """The name of the person.""" - name: 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 diff --git a/graphql/resolvers/root.go b/graphql/resolvers/root.go index 9b3a730bb..d7bd60218 100644 --- a/graphql/resolvers/root.go +++ b/graphql/resolvers/root.go @@ -32,6 +32,10 @@ func (RootResolver) Bug() graph.BugResolver { return &bugResolver{} } +func (r RootResolver) Person() graph.PersonResolver { + return &personResolver{} +} + func (RootResolver) CommentHistoryStep() graph.CommentHistoryStepResolver { return &commentHistoryStepResolver{} } diff --git a/graphql/schema.graphql b/graphql/schema.graphql index c187ce497..fefe895b7 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -16,11 +16,17 @@ type PageInfo { """Represents an person in a git object.""" type Person { - """The email of the person.""" + """The name of the person, if known.""" + name: String + + """The email of the person, if known.""" email: String - """The name of the person.""" - name: 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 diff --git a/termui/bug_table.go b/termui/bug_table.go index a60e226eb..8ad77b41c 100644 --- a/termui/bug_table.go +++ b/termui/bug_table.go @@ -299,7 +299,7 @@ func (bt *bugTable) render(v *gocui.View, maxX int) { id := text.LeftPadMaxLine(snap.HumanId(), columnWidths["id"], 2) status := text.LeftPadMaxLine(snap.Status.String(), columnWidths["status"], 2) title := text.LeftPadMaxLine(snap.Title, columnWidths["title"], 2) - author := text.LeftPadMaxLine(person.Name, columnWidths["author"], 2) + author := text.LeftPadMaxLine(person.DisplayName(), columnWidths["author"], 2) summary := text.LeftPadMaxLine(snap.Summary(), columnWidths["summary"], 2) lastEdit := text.LeftPadMaxLine(humanize.Time(snap.LastEditTime()), columnWidths["lastEdit"], 2) diff --git a/termui/show_bug.go b/termui/show_bug.go index 72bcfe2f4..395d0cd2d 100644 --- a/termui/show_bug.go +++ b/termui/show_bug.go @@ -8,8 +8,8 @@ import ( "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/util/colors" - "github.com/MichaelMure/git-bug/util/text" "github.com/MichaelMure/git-bug/util/git" + "github.com/MichaelMure/git-bug/util/text" "github.com/jroimartin/gocui" ) @@ -233,7 +233,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error { colors.Cyan(snap.HumanId()), colors.Bold(snap.Title), colors.Yellow(snap.Status), - colors.Magenta(snap.Author.Name), + colors.Magenta(snap.Author.DisplayName()), snap.CreatedAt.Format(timeLayout), edited, ) @@ -276,7 +276,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error { message, _ := text.WrapLeftPadded(comment.Message, maxX, 4) content := fmt.Sprintf("%s commented on %s%s\n\n%s", - colors.Magenta(comment.Author.Name), + colors.Magenta(comment.Author.DisplayName()), comment.CreatedAt.Time().Format(timeLayout), edited, message, @@ -294,7 +294,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error { setTitle := op.(*bug.SetTitleTimelineItem) content := fmt.Sprintf("%s changed the title to %s on %s", - colors.Magenta(setTitle.Author.Name), + colors.Magenta(setTitle.Author.DisplayName()), colors.Bold(setTitle.Title), setTitle.UnixTime.Time().Format(timeLayout), ) @@ -311,7 +311,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error { setStatus := op.(*bug.SetStatusTimelineItem) content := fmt.Sprintf("%s %s the bug on %s", - colors.Magenta(setStatus.Author.Name), + colors.Magenta(setStatus.Author.DisplayName()), colors.Bold(setStatus.Status.Action()), setStatus.UnixTime.Time().Format(timeLayout), ) @@ -360,7 +360,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error { } content := fmt.Sprintf("%s %s on %s", - colors.Magenta(labelChange.Author.Name), + colors.Magenta(labelChange.Author.DisplayName()), action.String(), labelChange.UnixTime.Time().Format(timeLayout), ) diff --git a/webui/public/index.html b/webui/public/index.html index 8287df671..3293ce2e3 100644 --- a/webui/public/index.html +++ b/webui/public/index.html @@ -6,7 +6,7 @@ - git-bug-webui(1) + git-bug webui diff --git a/webui/src/Author.js b/webui/src/Author.js index 0ad7e257f..e8fe9f94b 100644 --- a/webui/src/Author.js +++ b/webui/src/Author.js @@ -14,9 +14,13 @@ const styles = theme => ({ const Author = ({ author, bold, classes }) => { const klass = bold ? [classes.author, classes.bold] : [classes.author]; + if(!author.email) { + return {author.displayName} + } + return ( - {author.name} + {author.displayName} ); }; diff --git a/webui/src/bug/Bug.js b/webui/src/bug/Bug.js index 329fdd72d..496d0c4f8 100644 --- a/webui/src/bug/Bug.js +++ b/webui/src/bug/Bug.js @@ -88,6 +88,7 @@ Bug.fragment = gql` author { email name + displayName } } `; diff --git a/webui/src/bug/LabelChange.js b/webui/src/bug/LabelChange.js index bb5466783..b1bed4a57 100644 --- a/webui/src/bug/LabelChange.js +++ b/webui/src/bug/LabelChange.js @@ -42,6 +42,7 @@ LabelChange.fragment = gql` author { name email + displayName } added removed diff --git a/webui/src/bug/Message.js b/webui/src/bug/Message.js index 2d03e7806..a4e3eeb0e 100644 --- a/webui/src/bug/Message.js +++ b/webui/src/bug/Message.js @@ -47,6 +47,7 @@ Message.createFragment = gql` author { name email + displayName } message } @@ -60,6 +61,7 @@ Message.commentFragment = gql` author { name email + displayName } message } diff --git a/webui/src/bug/SetStatus.js b/webui/src/bug/SetStatus.js index 7d6bccf33..332c8352e 100644 --- a/webui/src/bug/SetStatus.js +++ b/webui/src/bug/SetStatus.js @@ -27,6 +27,7 @@ SetStatus.fragment = gql` author { name email + displayName } status } diff --git a/webui/src/bug/SetTitle.js b/webui/src/bug/SetTitle.js index 838219e29..5b17c431e 100644 --- a/webui/src/bug/SetTitle.js +++ b/webui/src/bug/SetTitle.js @@ -33,6 +33,7 @@ SetTitle.fragment = gql` author { name email + displayName } title was diff --git a/webui/src/list/BugRow.js b/webui/src/list/BugRow.js index 9253cc88a..a045770b9 100644 --- a/webui/src/list/BugRow.js +++ b/webui/src/list/BugRow.js @@ -77,7 +77,7 @@ const BugRow = ({ bug, classes }) => ( {bug.humanId} opened - by {bug.author.name} + by {bug.author.displayName} @@ -94,6 +94,7 @@ BugRow.fragment = gql` labels author { name + displayName } } `;