package github
import (
"context"
"fmt"
"strings"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/shurcooL/githubv4"
)
const keyGithubId = "github-id"
const keyGithubUrl = "github-url"
// githubImporter implement the Importer interface
type githubImporter struct{}
type Actor struct {
Login githubv4.String
AvatarUrl githubv4.String
}
type ActorEvent struct {
Id githubv4.ID
CreatedAt githubv4.DateTime
Actor Actor
}
type AuthorEvent struct {
Id githubv4.ID
CreatedAt githubv4.DateTime
Author Actor
}
type TimelineItem struct {
Typename githubv4.String `graphql:"__typename"`
// Issue
IssueComment struct {
AuthorEvent
Body githubv4.String
Url githubv4.URI
// TODO: edition
} `graphql:"... on IssueComment"`
// Label
LabeledEvent struct {
ActorEvent
Label struct {
// Color githubv4.String
Name githubv4.String
}
} `graphql:"... on LabeledEvent"`
UnlabeledEvent struct {
ActorEvent
Label struct {
// Color githubv4.String
Name githubv4.String
}
} `graphql:"... on UnlabeledEvent"`
// Status
ClosedEvent struct {
ActorEvent
// Url githubv4.URI
} `graphql:"... on ClosedEvent"`
ReopenedEvent struct {
ActorEvent
} `graphql:"... on ReopenedEvent"`
// Title
RenamedTitleEvent struct {
ActorEvent
CurrentTitle githubv4.String
PreviousTitle githubv4.String
} `graphql:"... on RenamedTitleEvent"`
}
type Issue struct {
AuthorEvent
Title string
Body githubv4.String
Url githubv4.URI
Timeline struct {
Nodes []TimelineItem
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
} `graphql:"timeline(first: $timelineFirst, after: $timelineAfter)"`
}
var q struct {
Repository struct {
Issues struct {
Nodes []Issue
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
} `graphql:"issues(first: $issueFirst, after: $issueAfter, orderBy: {field: CREATED_AT, direction: ASC})"`
} `graphql:"repository(owner: $owner, name: $name)"`
}
func (*githubImporter) ImportAll(repo *cache.RepoCache, conf core.Configuration) error {
client := buildClient(conf)
variables := map[string]interface{}{
"owner": githubv4.String(conf[keyUser]),
"name": githubv4.String(conf[keyProject]),
"issueFirst": githubv4.Int(1),
"issueAfter": (*githubv4.String)(nil),
"timelineFirst": githubv4.Int(10),
"timelineAfter": (*githubv4.String)(nil),
}
var b *cache.BugCache
for {
err := client.Query(context.TODO(), &q, variables)
if err != nil {
return err
}
if len(q.Repository.Issues.Nodes) != 1 {
return fmt.Errorf("Something went wrong when iterating issues, len is %d", len(q.Repository.Issues.Nodes))
}
issue := q.Repository.Issues.Nodes[0]
if b == nil {
b, err = importIssue(repo, issue)
if err != nil {
return err
}
}
for _, item := range q.Repository.Issues.Nodes[0].Timeline.Nodes {
importTimelineItem(b, item)
}
if !issue.Timeline.PageInfo.HasNextPage {
err = b.CommitAsNeeded()
if err != nil {
return err
}
b = nil
if !q.Repository.Issues.PageInfo.HasNextPage {
break
}
variables["issueAfter"] = githubv4.NewString(q.Repository.Issues.PageInfo.EndCursor)
variables["timelineAfter"] = (*githubv4.String)(nil)
continue
}
variables["timelineAfter"] = githubv4.NewString(issue.Timeline.PageInfo.EndCursor)
}
return nil
}
func (*githubImporter) Import(repo *cache.RepoCache, conf core.Configuration, id string) error {
fmt.Println(conf)
fmt.Println("IMPORT")
return nil
}
func importIssue(repo *cache.RepoCache, issue Issue) (*cache.BugCache, error) {
fmt.Printf("import issue: %s\n", issue.Title)
// TODO: check + import files
return repo.NewBugRaw(
makePerson(issue.Author),
issue.CreatedAt.Unix(),
issue.Title,
cleanupText(string(issue.Body)),
nil,
map[string]string{
keyGithubId: parseId(issue.Id),
keyGithubUrl: issue.Url.String(),
},
)
}
func importTimelineItem(b *cache.BugCache, item TimelineItem) error {
switch item.Typename {
case "IssueComment":
// fmt.Printf("import %s: %s\n", item.Typename, item.IssueComment)
return b.AddCommentRaw(
makePerson(item.IssueComment.Author),
item.IssueComment.CreatedAt.Unix(),
cleanupText(string(item.IssueComment.Body)),
nil,
map[string]string{
keyGithubId: parseId(item.IssueComment.Id),
keyGithubUrl: item.IssueComment.Url.String(),
},
)
case "LabeledEvent":
// fmt.Printf("import %s: %s\n", item.Typename, item.LabeledEvent)
_, err := b.ChangeLabelsRaw(
makePerson(item.LabeledEvent.Actor),
item.LabeledEvent.CreatedAt.Unix(),
[]string{
string(item.LabeledEvent.Label.Name),
},
nil,
)
return err
case "UnlabeledEvent":
// fmt.Printf("import %s: %s\n", item.Typename, item.UnlabeledEvent)
_, err := b.ChangeLabelsRaw(
makePerson(item.UnlabeledEvent.Actor),
item.UnlabeledEvent.CreatedAt.Unix(),
nil,
[]string{
string(item.UnlabeledEvent.Label.Name),
},
)
return err
case "ClosedEvent":
// fmt.Printf("import %s: %s\n", item.Typename, item.ClosedEvent)
return b.CloseRaw(
makePerson(item.ClosedEvent.Actor),
item.ClosedEvent.CreatedAt.Unix(),
)
case "ReopenedEvent":
// fmt.Printf("import %s: %s\n", item.Typename, item.ReopenedEvent)
return b.OpenRaw(
makePerson(item.ReopenedEvent.Actor),
item.ReopenedEvent.CreatedAt.Unix(),
)
case "RenamedTitleEvent":
// fmt.Printf("import %s: %s\n", item.Typename, item.RenamedTitleEvent)
return b.SetTitleRaw(
makePerson(item.RenamedTitleEvent.Actor),
item.RenamedTitleEvent.CreatedAt.Unix(),
string(item.RenamedTitleEvent.CurrentTitle),
)
default:
fmt.Println("ignore event ", item.Typename)
}
return nil
}
// makePerson create a bug.Person from the Github data
func makePerson(actor Actor) bug.Person {
return bug.Person{
Name: string(actor.Login),
AvatarUrl: string(actor.AvatarUrl),
}
}
// parseId convert the unusable githubv4.ID (an interface{}) into a string
func parseId(id githubv4.ID) string {
return fmt.Sprintf("%v", id)
}
func cleanupText(text string) string {
// windows new line, Github, really ?
return strings.Replace(text, "\r\n", "\n", -1)
}