git-bug/bridge/github/import.go

532 lines
12 KiB
Go
Raw Normal View History

2018-09-24 20:22:32 +03:00
package github
import (
"context"
"fmt"
2019-04-27 02:15:02 +03:00
"time"
2018-09-24 20:22:32 +03:00
"github.com/shurcooL/githubv4"
2018-09-24 20:22:32 +03:00
"github.com/MichaelMure/git-bug/bridge/core"
2018-09-25 20:10:38 +03:00
"github.com/MichaelMure/git-bug/bug"
2018-09-24 20:22:32 +03:00
"github.com/MichaelMure/git-bug/cache"
2019-01-17 05:09:08 +03:00
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/text"
2018-09-24 20:22:32 +03:00
)
2019-04-27 02:15:02 +03:00
const (
keyOrigin = "origin"
2019-04-27 02:15:02 +03:00
keyGithubId = "github-id"
keyGithubUrl = "github-url"
keyGithubLogin = "github-login"
)
2018-09-25 20:10:38 +03:00
// githubImporter implement the Importer interface
type githubImporter struct {
conf core.Configuration
2019-05-10 03:05:00 +03:00
// iterator
iterator *iterator
// number of imported issues
importedIssues int
// number of imported identities
importedIdentities int
}
func (gi *githubImporter) Init(conf core.Configuration) error {
gi.conf = conf
2018-09-24 20:22:32 +03:00
return nil
}
// ImportAll iterate over all the configured repository issues and ensure the creation of the
// missing issues / timeline items / edits / label events ...
func (gi *githubImporter) ImportAll(repo *cache.RepoCache, since time.Time) error {
gi.iterator = NewIterator(gi.conf[keyOwner], gi.conf[keyProject], gi.conf[keyToken], since)
// Loop over all matching issues
2019-05-10 03:05:00 +03:00
for gi.iterator.NextIssue() {
issue := gi.iterator.IssueValue()
2019-05-17 17:33:16 +03:00
fmt.Printf("importing issue: %v\n", issue.Title)
// create issue
2019-05-10 13:02:44 +03:00
b, err := gi.ensureIssue(repo, issue)
if err != nil {
return fmt.Errorf("issue creation: %v", err)
2019-04-27 02:15:02 +03:00
}
// loop over timeline items
for gi.iterator.NextTimelineItem() {
if err := gi.ensureTimelineItem(repo, b, gi.iterator.TimelineItemValue()); err != nil {
return fmt.Errorf("timeline item creation: %v", err)
}
}
2019-04-27 02:15:02 +03:00
// commit bug state
if err := b.CommitAsNeeded(); err != nil {
return fmt.Errorf("bug commit: %v", err)
2019-04-27 02:15:02 +03:00
}
}
2019-05-10 03:05:00 +03:00
if err := gi.iterator.Error(); err != nil {
2019-04-27 02:15:02 +03:00
fmt.Printf("import error: %v\n", err)
return err
}
fmt.Printf("Successfully imported %d issues and %d identities from Github\n", gi.importedIssues, gi.importedIdentities)
2019-04-27 02:15:02 +03:00
return nil
2018-09-25 20:10:38 +03:00
}
2019-05-10 13:02:44 +03:00
func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline) (*cache.BugCache, error) {
// ensure issue author
author, err := gi.ensurePerson(repo, issue.Author)
if err != nil {
return nil, err
}
// resolve bug
b, err := repo.ResolveBugCreateMetadata(keyGithubUrl, issue.Url.String())
if err != nil && err != bug.ErrBugNotExist {
return nil, err
}
2019-05-10 13:02:44 +03:00
// get issue edits
var issueEdits []userContentEdit
2019-05-10 13:02:44 +03:00
for gi.iterator.NextIssueEdit() {
issueEdits = append(issueEdits, gi.iterator.IssueEditValue())
}
// if issueEdits is empty
if len(issueEdits) == 0 {
if err == bug.ErrBugNotExist {
cleanText, err := text.Cleanup(string(issue.Body))
if err != nil {
return nil, err
}
// create bug
b, _, err = repo.NewBugRaw(
author,
issue.CreatedAt.Unix(),
issue.Title,
cleanText,
nil,
map[string]string{
keyOrigin: target,
keyGithubId: parseId(issue.Id),
keyGithubUrl: issue.Url.String(),
})
if err != nil {
return nil, err
}
2019-05-17 17:33:16 +03:00
// importing a new bug
gi.importedIssues++
}
} else {
// create bug from given issueEdits
for i, edit := range issueEdits {
if i == 0 && b != nil {
// The first edit in the github result is the issue creation itself, we already have that
continue
}
cleanText, err := text.Cleanup(string(*edit.Diff))
if err != nil {
return nil, err
}
// if the bug doesn't exist
if b == nil {
// we create the bug as soon as we have a legit first edition
b, _, err = repo.NewBugRaw(
author,
issue.CreatedAt.Unix(),
issue.Title,
cleanText,
nil,
map[string]string{
keyOrigin: target,
keyGithubId: parseId(issue.Id),
keyGithubUrl: issue.Url.String(),
},
)
if err != nil {
return nil, err
}
// importing a new bug
gi.importedIssues++
continue
}
// other edits will be added as CommentEdit operations
target, err := b.ResolveOperationWithMetadata(keyGithubUrl, issue.Url.String())
if err != nil {
return nil, err
}
err = gi.ensureCommentEdit(repo, b, target, edit)
if err != nil {
return nil, err
}
}
}
return b, nil
2019-04-27 02:15:02 +03:00
}
func (gi *githubImporter) ensureTimelineItem(repo *cache.RepoCache, b *cache.BugCache, item timelineItem) error {
fmt.Printf("import event item: %s\n", item.Typename)
2018-09-25 20:10:38 +03:00
switch item.Typename {
case "IssueComment":
2019-05-10 03:05:00 +03:00
// collect all comment edits
commentEdits := []userContentEdit{}
for gi.iterator.NextCommentEdit() {
commentEdits = append(commentEdits, gi.iterator.CommentEditValue())
}
err := gi.ensureTimelineComment(repo, b, item.IssueComment, commentEdits)
if err != nil {
return fmt.Errorf("timeline comment creation: %v", err)
}
2018-09-25 20:10:38 +03:00
case "LabeledEvent":
id := parseId(item.LabeledEvent.Id)
2019-02-24 14:58:04 +03:00
_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
if err != cache.ErrNoMatchingOp {
return err
}
2019-02-24 14:58:04 +03:00
author, err := gi.ensurePerson(repo, item.LabeledEvent.Actor)
2019-01-19 18:01:06 +03:00
if err != nil {
return err
}
2019-05-04 14:19:56 +03:00
_, err = b.ForceChangeLabelsRaw(
2019-01-19 18:01:06 +03:00
author,
2018-09-25 20:10:38 +03:00
item.LabeledEvent.CreatedAt.Unix(),
[]string{
string(item.LabeledEvent.Label.Name),
},
nil,
map[string]string{keyGithubId: id},
2018-09-25 20:10:38 +03:00
)
2019-05-04 14:19:56 +03:00
2018-09-25 20:10:38 +03:00
return err
case "UnlabeledEvent":
id := parseId(item.UnlabeledEvent.Id)
2019-02-24 14:58:04 +03:00
_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
if err != cache.ErrNoMatchingOp {
return err
}
2019-02-24 14:58:04 +03:00
author, err := gi.ensurePerson(repo, item.UnlabeledEvent.Actor)
2019-01-19 18:01:06 +03:00
if err != nil {
return err
}
2019-05-04 14:19:56 +03:00
_, err = b.ForceChangeLabelsRaw(
2019-01-19 18:01:06 +03:00
author,
2018-09-25 20:10:38 +03:00
item.UnlabeledEvent.CreatedAt.Unix(),
nil,
[]string{
string(item.UnlabeledEvent.Label.Name),
},
map[string]string{keyGithubId: id},
2018-09-25 20:10:38 +03:00
)
return err
case "ClosedEvent":
id := parseId(item.ClosedEvent.Id)
2019-02-24 14:58:04 +03:00
_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
if err != cache.ErrNoMatchingOp {
return err
}
2019-02-24 14:58:04 +03:00
author, err := gi.ensurePerson(repo, item.ClosedEvent.Actor)
2019-01-19 18:01:06 +03:00
if err != nil {
return err
}
2019-02-24 14:58:04 +03:00
_, err = b.CloseRaw(
2019-01-19 18:01:06 +03:00
author,
2018-09-25 20:10:38 +03:00
item.ClosedEvent.CreatedAt.Unix(),
map[string]string{keyGithubId: id},
2018-09-25 20:10:38 +03:00
)
2019-02-24 14:58:04 +03:00
return err
2018-09-25 20:10:38 +03:00
case "ReopenedEvent":
id := parseId(item.ReopenedEvent.Id)
2019-02-24 14:58:04 +03:00
_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
if err != cache.ErrNoMatchingOp {
return err
}
2019-02-24 14:58:04 +03:00
author, err := gi.ensurePerson(repo, item.ReopenedEvent.Actor)
2019-01-19 18:01:06 +03:00
if err != nil {
return err
}
2019-02-24 14:58:04 +03:00
_, err = b.OpenRaw(
2019-01-19 18:01:06 +03:00
author,
2018-09-25 20:10:38 +03:00
item.ReopenedEvent.CreatedAt.Unix(),
map[string]string{keyGithubId: id},
2018-09-25 20:10:38 +03:00
)
2019-02-24 14:58:04 +03:00
return err
2018-09-25 20:10:38 +03:00
case "RenamedTitleEvent":
id := parseId(item.RenamedTitleEvent.Id)
2019-02-24 14:58:04 +03:00
_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
if err != cache.ErrNoMatchingOp {
return err
}
2019-02-24 14:58:04 +03:00
author, err := gi.ensurePerson(repo, item.RenamedTitleEvent.Actor)
2019-01-19 18:01:06 +03:00
if err != nil {
return err
}
2019-02-24 14:58:04 +03:00
_, err = b.SetTitleRaw(
2019-01-19 18:01:06 +03:00
author,
2018-09-25 20:10:38 +03:00
item.RenamedTitleEvent.CreatedAt.Unix(),
string(item.RenamedTitleEvent.CurrentTitle),
map[string]string{keyGithubId: id},
2018-09-25 20:10:38 +03:00
)
2019-02-24 14:58:04 +03:00
return err
2018-09-25 20:10:38 +03:00
default:
2019-04-27 02:15:02 +03:00
fmt.Printf("ignore event: %v\n", item.Typename)
2018-09-25 20:10:38 +03:00
}
return nil
}
func (gi *githubImporter) ensureTimelineComment(repo *cache.RepoCache, b *cache.BugCache, item issueComment, edits []userContentEdit) error {
// ensure person
author, err := gi.ensurePerson(repo, item.Author)
if err != nil {
return err
}
targetOpID, err := b.ResolveOperationWithMetadata(keyGithubId, parseId(item.Id))
if err != nil && err != cache.ErrNoMatchingOp {
// real error
return err
}
// if no edits are given we create the comment
if len(edits) == 0 {
// if comment doesn't exist
if err == cache.ErrNoMatchingOp {
cleanText, err := text.Cleanup(string(item.Body))
if err != nil {
return err
}
// add comment operation
_, err = b.AddCommentRaw(
author,
item.CreatedAt.Unix(),
cleanText,
nil,
map[string]string{
keyGithubId: parseId(item.Id),
keyGithubUrl: parseId(item.Url.String()),
},
)
return err
}
} else {
2019-05-05 15:08:48 +03:00
for i, edit := range edits {
if i == 0 && targetOpID != "" {
// The first edit in the github result is the comment creation itself, we already have that
continue
}
// ensure editor identity
editor, err := gi.ensurePerson(repo, edit.Editor)
if err != nil {
return err
}
// create comment when target is empty
if targetOpID == "" {
cleanText, err := text.Cleanup(string(*edit.Diff))
if err != nil {
return err
}
op, err := b.AddCommentRaw(
editor,
edit.CreatedAt.Unix(),
cleanText,
nil,
map[string]string{
keyGithubId: parseId(item.Id),
keyGithubUrl: item.Url.String(),
},
)
if err != nil {
return err
}
// set target for the nexr edit now that the comment is created
targetOpID = op.ID()
continue
}
err = gi.ensureCommentEdit(repo, b, targetOpID, edit)
if err != nil {
return err
}
}
}
return nil
}
func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugCache, target string, edit userContentEdit) error {
2019-02-24 14:58:04 +03:00
_, err := b.ResolveOperationWithMetadata(keyGithubId, parseId(edit.Id))
if err == nil {
// already imported
return nil
}
if err != cache.ErrNoMatchingOp {
// real error
return err
}
fmt.Println("import edition")
2019-02-24 15:06:03 +03:00
editor, err := gi.ensurePerson(repo, edit.Editor)
if err != nil {
return err
2019-01-19 18:01:06 +03:00
}
switch {
case edit.DeletedAt != nil:
// comment deletion, not supported yet
fmt.Println("comment deletion is not supported yet")
case edit.DeletedAt == nil:
cleanText, err := text.Cleanup(string(*edit.Diff))
if err != nil {
return err
}
// comment edition
_, err = b.EditCommentRaw(
2019-01-19 18:01:06 +03:00
editor,
edit.CreatedAt.Unix(),
target,
cleanText,
map[string]string{
keyGithubId: parseId(edit.Id),
},
)
if err != nil {
return err
}
}
return nil
}
2019-02-24 14:58:04 +03:00
// ensurePerson create a bug.Person from the Github data
func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*cache.IdentityCache, error) {
2019-01-19 18:01:06 +03:00
// When a user has been deleted, Github return a null actor, while displaying a profile named "ghost"
// in it's UI. So we need a special case to get it.
if actor == nil {
2019-01-19 18:01:06 +03:00
return gi.getGhost(repo)
}
// Look first in the cache
i, err := repo.ResolveIdentityImmutableMetadata(keyGithubLogin, string(actor.Login))
if err == nil {
return i, nil
}
if _, ok := err.(identity.ErrMultipleMatch); ok {
return nil, err
}
2019-01-19 18:01:06 +03:00
2019-05-10 03:05:00 +03:00
// importing a new identity
gi.importedIdentities++
2018-10-07 19:27:23 +03:00
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":
}
2019-01-19 18:01:06 +03:00
return repo.NewIdentityRaw(
name,
email,
string(actor.Login),
string(actor.AvatarUrl),
map[string]string{
keyGithubLogin: string(actor.Login),
},
)
2018-09-25 20:10:38 +03:00
}
func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, error) {
2019-01-19 18:01:06 +03:00
// Look first in the cache
i, err := repo.ResolveIdentityImmutableMetadata(keyGithubLogin, "ghost")
if err == nil {
return i, nil
}
if _, ok := err.(identity.ErrMultipleMatch); ok {
return nil, err
}
var q ghostQuery
variables := map[string]interface{}{
"login": githubv4.String("ghost"),
}
gc := buildClient(gi.conf[keyToken])
parentCtx := context.Background()
ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout)
defer cancel()
err = gc.Query(ctx, &q, variables)
if err != nil {
2019-01-19 18:01:06 +03:00
return nil, err
}
2018-10-07 19:27:23 +03:00
var name string
if q.User.Name != nil {
name = string(*q.User.Name)
}
2019-01-19 18:01:06 +03:00
return repo.NewIdentityRaw(
2019-01-17 05:09:08 +03:00
name,
"",
2019-01-17 05:09:08 +03:00
string(q.User.Login),
string(q.User.AvatarUrl),
2019-01-19 18:01:06 +03:00
map[string]string{
keyGithubLogin: string(q.User.Login),
},
2019-01-17 05:09:08 +03:00
)
}
2018-09-25 20:10:38 +03:00
// parseId convert the unusable githubv4.ID (an interface{}) into a string
func parseId(id githubv4.ID) string {
return fmt.Sprintf("%v", id)
}