2019-07-09 23:56:38 +03:00
|
|
|
package gitlab
|
|
|
|
|
|
|
|
import (
|
2019-07-17 01:06:42 +03:00
|
|
|
"fmt"
|
2019-07-17 01:09:02 +03:00
|
|
|
"strconv"
|
2019-07-09 23:56:38 +03:00
|
|
|
"time"
|
|
|
|
|
2019-07-17 01:06:42 +03:00
|
|
|
"github.com/xanzy/go-gitlab"
|
|
|
|
|
2019-07-09 23:56:38 +03:00
|
|
|
"github.com/MichaelMure/git-bug/bridge/core"
|
2019-07-17 01:06:42 +03:00
|
|
|
"github.com/MichaelMure/git-bug/bug"
|
2019-07-09 23:56:38 +03:00
|
|
|
"github.com/MichaelMure/git-bug/cache"
|
2019-07-17 01:09:02 +03:00
|
|
|
"github.com/MichaelMure/git-bug/identity"
|
2019-07-17 01:06:42 +03:00
|
|
|
"github.com/MichaelMure/git-bug/util/text"
|
2019-07-09 23:56:38 +03:00
|
|
|
)
|
|
|
|
|
2019-07-22 01:13:47 +03:00
|
|
|
// gitlabImporter implement the Importer interface
|
2019-07-09 23:56:38 +03:00
|
|
|
type gitlabImporter struct {
|
|
|
|
conf core.Configuration
|
|
|
|
|
2019-07-17 01:06:42 +03:00
|
|
|
// iterator
|
|
|
|
iterator *iterator
|
|
|
|
|
2019-07-09 23:56:38 +03:00
|
|
|
// number of imported issues
|
|
|
|
importedIssues int
|
|
|
|
|
|
|
|
// number of imported identities
|
|
|
|
importedIdentities int
|
|
|
|
}
|
|
|
|
|
2019-07-17 01:06:42 +03:00
|
|
|
func (gi *gitlabImporter) Init(conf core.Configuration) error {
|
|
|
|
gi.conf = conf
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-22 01:13:47 +03:00
|
|
|
// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
|
|
|
|
// of the missing issues / comments / label events / title changes ...
|
2019-07-17 01:06:42 +03:00
|
|
|
func (gi *gitlabImporter) ImportAll(repo *cache.RepoCache, since time.Time) error {
|
|
|
|
gi.iterator = NewIterator(gi.conf[keyProjectID], gi.conf[keyToken], since)
|
|
|
|
|
|
|
|
// Loop over all matching issues
|
|
|
|
for gi.iterator.NextIssue() {
|
|
|
|
issue := gi.iterator.IssueValue()
|
|
|
|
fmt.Printf("importing issue: %v\n", issue.Title)
|
|
|
|
|
|
|
|
// create issue
|
|
|
|
b, err := gi.ensureIssue(repo, issue)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("issue creation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop over all notes
|
|
|
|
for gi.iterator.NextNote() {
|
|
|
|
note := gi.iterator.NoteValue()
|
|
|
|
if err := gi.ensureNote(repo, b, note); err != nil {
|
|
|
|
return fmt.Errorf("note creation: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop over all label events
|
|
|
|
for gi.iterator.NextLabelEvent() {
|
|
|
|
labelEvent := gi.iterator.LabelEventValue()
|
|
|
|
if err := gi.ensureLabelEvent(repo, b, labelEvent); err != nil {
|
|
|
|
return fmt.Errorf("label event creation: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// commit bug state
|
|
|
|
if err := b.CommitAsNeeded(); err != nil {
|
|
|
|
return fmt.Errorf("bug commit: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := gi.iterator.Error(); err != nil {
|
|
|
|
fmt.Printf("import error: %v\n", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("Successfully imported %d issues and %d identities from Gitlab\n", gi.importedIssues, gi.importedIdentities)
|
2019-07-09 23:56:38 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-17 01:06:42 +03:00
|
|
|
func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
|
|
|
|
// ensure issue author
|
|
|
|
author, err := gi.ensurePerson(repo, issue.Author.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// resolve bug
|
|
|
|
b, err := repo.ResolveBugCreateMetadata(keyGitlabUrl, issue.WebURL)
|
|
|
|
if err != nil && err != bug.ErrBugNotExist {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-07-22 01:13:47 +03:00
|
|
|
// if bug was never imported
|
2019-07-17 01:06:42 +03:00
|
|
|
if err == bug.ErrBugNotExist {
|
|
|
|
cleanText, err := text.Cleanup(string(issue.Description))
|
|
|
|
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,
|
|
|
|
keyGitlabId: parseID(issue.ID),
|
|
|
|
keyGitlabUrl: issue.WebURL,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// importing a new bug
|
|
|
|
gi.importedIssues++
|
|
|
|
|
|
|
|
return b, nil
|
|
|
|
}
|
|
|
|
|
2019-07-19 20:39:15 +03:00
|
|
|
return b, nil
|
2019-07-17 01:06:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
|
|
|
|
id := parseID(note.ID)
|
|
|
|
|
|
|
|
// ensure issue author
|
|
|
|
author, err := gi.ensurePerson(repo, note.Author.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-07-17 19:54:19 +03:00
|
|
|
hash, errResolve := b.ResolveOperationWithMetadata(keyGitlabId, id)
|
2019-07-17 23:41:42 +03:00
|
|
|
if errResolve != cache.ErrNoMatchingOp {
|
2019-07-17 19:54:19 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-07-17 01:06:42 +03:00
|
|
|
noteType, body := GetNoteType(note)
|
|
|
|
switch noteType {
|
|
|
|
case NOTE_CLOSED:
|
|
|
|
_, err = b.CloseRaw(
|
|
|
|
author,
|
|
|
|
note.CreatedAt.Unix(),
|
|
|
|
map[string]string{
|
2019-07-17 19:54:19 +03:00
|
|
|
keyGitlabId: id,
|
|
|
|
keyGitlabUrl: "",
|
2019-07-17 01:06:42 +03:00
|
|
|
},
|
|
|
|
)
|
|
|
|
return err
|
|
|
|
|
|
|
|
case NOTE_REOPENED:
|
|
|
|
_, err = b.OpenRaw(
|
|
|
|
author,
|
|
|
|
note.CreatedAt.Unix(),
|
|
|
|
map[string]string{
|
2019-07-17 19:54:19 +03:00
|
|
|
keyGitlabId: id,
|
|
|
|
keyGitlabUrl: "",
|
2019-07-17 01:06:42 +03:00
|
|
|
},
|
|
|
|
)
|
|
|
|
return err
|
|
|
|
|
|
|
|
case NOTE_DESCRIPTION_CHANGED:
|
|
|
|
issue := gi.iterator.IssueValue()
|
|
|
|
|
|
|
|
// since gitlab doesn't provide the issue history
|
|
|
|
// we should check for "changed the description" notes and compare issue texts
|
2019-07-19 19:56:58 +03:00
|
|
|
// TODO: Check only one time and ignore next 'description change' within one issue
|
2019-07-17 01:06:42 +03:00
|
|
|
if issue.Description != b.Snapshot().Comments[0].Message {
|
2019-07-17 19:54:19 +03:00
|
|
|
|
2019-07-17 01:06:42 +03:00
|
|
|
// comment edition
|
|
|
|
_, err = b.EditCommentRaw(
|
|
|
|
author,
|
|
|
|
note.UpdatedAt.Unix(),
|
|
|
|
target,
|
|
|
|
issue.Description,
|
|
|
|
map[string]string{
|
|
|
|
keyGitlabId: id,
|
|
|
|
keyGitlabUrl: "",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
case NOTE_COMMENT:
|
|
|
|
|
|
|
|
cleanText, err := text.Cleanup(body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we didn't import the comment
|
2019-07-17 19:54:19 +03:00
|
|
|
if errResolve == cache.ErrNoMatchingOp {
|
2019-07-17 01:06:42 +03:00
|
|
|
|
|
|
|
// add comment operation
|
|
|
|
_, err = b.AddCommentRaw(
|
|
|
|
author,
|
|
|
|
note.CreatedAt.Unix(),
|
|
|
|
cleanText,
|
|
|
|
nil,
|
|
|
|
map[string]string{
|
|
|
|
keyGitlabId: id,
|
|
|
|
keyGitlabUrl: "",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// if comment was already exported
|
|
|
|
|
|
|
|
// if note wasn't updated
|
|
|
|
if note.UpdatedAt.Equal(*note.CreatedAt) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// search for last comment update
|
2019-07-17 19:54:19 +03:00
|
|
|
comment, err := b.Snapshot().SearchComment(hash)
|
2019-07-17 01:06:42 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// compare local bug comment with the new note body
|
2019-07-17 19:54:19 +03:00
|
|
|
if comment.Message != cleanText {
|
2019-07-17 01:06:42 +03:00
|
|
|
// comment edition
|
|
|
|
_, err = b.EditCommentRaw(
|
|
|
|
author,
|
|
|
|
note.UpdatedAt.Unix(),
|
|
|
|
target,
|
|
|
|
cleanText,
|
|
|
|
map[string]string{
|
|
|
|
// no metadata unique metadata to store
|
|
|
|
keyGitlabId: "",
|
|
|
|
keyGitlabUrl: "",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
case NOTE_TITLE_CHANGED:
|
2019-07-17 19:54:19 +03:00
|
|
|
// title change events are given new notes
|
2019-07-17 01:06:42 +03:00
|
|
|
_, err = b.SetTitleRaw(
|
|
|
|
author,
|
|
|
|
note.CreatedAt.Unix(),
|
|
|
|
body,
|
|
|
|
map[string]string{
|
|
|
|
keyGitlabId: id,
|
|
|
|
keyGitlabUrl: "",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
default:
|
2019-07-17 19:54:19 +03:00
|
|
|
// non handled note types, this is not an error
|
|
|
|
//TODO: send warning via channel
|
2019-07-17 01:06:42 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-09 23:56:38 +03:00
|
|
|
return nil
|
|
|
|
}
|
2019-07-17 01:06:42 +03:00
|
|
|
|
|
|
|
func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
|
|
|
|
_, err := b.ResolveOperationWithMetadata(keyGitlabId, parseID(labelEvent.ID))
|
|
|
|
if err != cache.ErrNoMatchingOp {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// ensure issue author
|
|
|
|
author, err := gi.ensurePerson(repo, labelEvent.User.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch labelEvent.Action {
|
|
|
|
case "add":
|
|
|
|
_, err = b.ForceChangeLabelsRaw(
|
|
|
|
author,
|
|
|
|
labelEvent.CreatedAt.Unix(),
|
|
|
|
[]string{labelEvent.Label.Name},
|
|
|
|
nil,
|
|
|
|
map[string]string{
|
|
|
|
keyGitlabId: parseID(labelEvent.ID),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
case "remove":
|
|
|
|
_, err = b.ForceChangeLabelsRaw(
|
|
|
|
author,
|
|
|
|
labelEvent.CreatedAt.Unix(),
|
|
|
|
nil,
|
|
|
|
[]string{labelEvent.Label.Name},
|
|
|
|
map[string]string{
|
|
|
|
keyGitlabId: parseID(labelEvent.ID),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
default:
|
|
|
|
panic("unexpected label event action")
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
|
2019-07-17 01:09:02 +03:00
|
|
|
// Look first in the cache
|
|
|
|
i, err := repo.ResolveIdentityImmutableMetadata(keyGitlabId, strconv.Itoa(id))
|
|
|
|
if err == nil {
|
|
|
|
return i, nil
|
|
|
|
}
|
|
|
|
if _, ok := err.(identity.ErrMultipleMatch); ok {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// importing a new identity
|
|
|
|
gi.importedIdentities++
|
|
|
|
|
2019-07-17 01:06:42 +03:00
|
|
|
client := buildClient(gi.conf["token"])
|
|
|
|
|
|
|
|
user, _, err := client.Users.GetUser(id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return repo.NewIdentityRaw(
|
|
|
|
user.Name,
|
|
|
|
user.PublicEmail,
|
|
|
|
user.Username,
|
|
|
|
user.AvatarURL,
|
|
|
|
map[string]string{
|
2019-07-17 01:09:02 +03:00
|
|
|
keyGitlabId: strconv.Itoa(id),
|
2019-07-17 01:06:42 +03:00
|
|
|
keyGitlabLogin: user.Username,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseID(id int) string {
|
|
|
|
return fmt.Sprintf("%d", id)
|
|
|
|
}
|