Detailed changes
@@ -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(),
}
@@ -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
@@ -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)"`
}
@@ -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")
}
@@ -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()),
@@ -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,
)
@@ -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:
@@ -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
@@ -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{}
}
@@ -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
@@ -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)
@@ -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),
)
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
- <title>git-bug-webui(1)</title>
+ <title>git-bug webui</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
@@ -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 <span className={klass.join(' ')}>{author.displayName}</span>
+ }
+
return (
<Tooltip title={author.email}>
- <span className={klass.join(' ')}>{author.name}</span>
+ <span className={klass.join(' ')}>{author.displayName}</span>
</Tooltip>
);
};
@@ -88,6 +88,7 @@ Bug.fragment = gql`
author {
email
name
+ displayName
}
}
`;
@@ -42,6 +42,7 @@ LabelChange.fragment = gql`
author {
name
email
+ displayName
}
added
removed
@@ -47,6 +47,7 @@ Message.createFragment = gql`
author {
name
email
+ displayName
}
message
}
@@ -60,6 +61,7 @@ Message.commentFragment = gql`
author {
name
email
+ displayName
}
message
}
@@ -27,6 +27,7 @@ SetStatus.fragment = gql`
author {
name
email
+ displayName
}
status
}
@@ -33,6 +33,7 @@ SetTitle.fragment = gql`
author {
name
email
+ displayName
}
title
was
@@ -77,7 +77,7 @@ const BugRow = ({ bug, classes }) => (
<Typography color={'textSecondary'}>
{bug.humanId} opened
<Date date={bug.createdAt} />
- by {bug.author.name}
+ by {bug.author.displayName}
</Typography>
</div>
</TableCell>
@@ -94,6 +94,7 @@ BugRow.fragment = gql`
labels
author {
name
+ displayName
}
}
`;