aboutsummaryrefslogtreecommitdiffstats
path: root/bridge
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2022-02-01 09:24:58 +0100
committerGitHub <noreply@github.com>2022-02-01 09:24:58 +0100
commit05d73e1b5321c97cd05133b5ae49d1798bc2fe5d (patch)
tree66eeb4ada52d282ad3ed9e231b3e74b42cbcfa48 /bridge
parent843da8e1f56504efb979f8e5eb43be5c176e7338 (diff)
parente888391b36307d0d4a1e12ba3a57b602c8b1528a (diff)
downloadgit-bug-05d73e1b5321c97cd05133b5ae49d1798bc2fe5d.tar.gz
Merge pull request #571 from 5nord/add-new-gitlab-iterator
[gitlab] Add new iterator with state change events
Diffstat (limited to 'bridge')
-rw-r--r--bridge/gitlab/event.go215
-rw-r--r--bridge/gitlab/event_test.go (renamed from bridge/gitlab/import_notes_test.go)39
-rw-r--r--bridge/gitlab/export_test.go5
-rw-r--r--bridge/gitlab/gitlab_api.go140
-rw-r--r--bridge/gitlab/import.go174
-rw-r--r--bridge/gitlab/import_notes.go147
-rw-r--r--bridge/gitlab/iterator/issue.go89
-rw-r--r--bridge/gitlab/iterator/iterator.go138
-rw-r--r--bridge/gitlab/iterator/labelEvent.go105
-rw-r--r--bridge/gitlab/iterator/note.go90
10 files changed, 461 insertions, 681 deletions
diff --git a/bridge/gitlab/event.go b/bridge/gitlab/event.go
new file mode 100644
index 00000000..80663edd
--- /dev/null
+++ b/bridge/gitlab/event.go
@@ -0,0 +1,215 @@
+package gitlab
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/MichaelMure/git-bug/util/text"
+ "github.com/xanzy/go-gitlab"
+)
+
+// Event represents a unified GitLab event (note, label or state event).
+type Event interface {
+ ID() string
+ UserID() int
+ Kind() EventKind
+ CreatedAt() time.Time
+}
+
+type EventKind int
+
+const (
+ EventUnknown EventKind = iota
+ EventError
+ EventComment
+ EventTitleChanged
+ EventDescriptionChanged
+ EventClosed
+ EventReopened
+ EventLocked
+ EventUnlocked
+ EventChangedDuedate
+ EventRemovedDuedate
+ EventAssigned
+ EventUnassigned
+ EventChangedMilestone
+ EventRemovedMilestone
+ EventAddLabel
+ EventRemoveLabel
+ EventMentionedInIssue
+ EventMentionedInMergeRequest
+)
+
+var _ Event = &NoteEvent{}
+
+type NoteEvent struct{ gitlab.Note }
+
+func (n NoteEvent) ID() string { return fmt.Sprintf("%d", n.Note.ID) }
+func (n NoteEvent) UserID() int { return n.Author.ID }
+func (n NoteEvent) CreatedAt() time.Time { return *n.Note.CreatedAt }
+func (n NoteEvent) Kind() EventKind {
+
+ switch {
+ case !n.System:
+ return EventComment
+
+ case n.Body == "closed":
+ return EventClosed
+
+ case n.Body == "reopened":
+ return EventReopened
+
+ case n.Body == "changed the description":
+ return EventDescriptionChanged
+
+ case n.Body == "locked this issue":
+ return EventLocked
+
+ case n.Body == "unlocked this issue":
+ return EventUnlocked
+
+ case strings.HasPrefix(n.Body, "changed title from"):
+ return EventTitleChanged
+
+ case strings.HasPrefix(n.Body, "changed due date to"):
+ return EventChangedDuedate
+
+ case n.Body == "removed due date":
+ return EventRemovedDuedate
+
+ case strings.HasPrefix(n.Body, "assigned to @"):
+ return EventAssigned
+
+ case strings.HasPrefix(n.Body, "unassigned @"):
+ return EventUnassigned
+
+ case strings.HasPrefix(n.Body, "changed milestone to %"):
+ return EventChangedMilestone
+
+ case strings.HasPrefix(n.Body, "removed milestone"):
+ return EventRemovedMilestone
+
+ case strings.HasPrefix(n.Body, "mentioned in issue"):
+ return EventMentionedInIssue
+
+ case strings.HasPrefix(n.Body, "mentioned in merge request"):
+ return EventMentionedInMergeRequest
+
+ default:
+ return EventUnknown
+ }
+
+}
+
+func (n NoteEvent) Title() string {
+ if n.Kind() == EventTitleChanged {
+ return getNewTitle(n.Body)
+ }
+ return text.CleanupOneLine(n.Body)
+}
+
+var _ Event = &LabelEvent{}
+
+type LabelEvent struct{ gitlab.LabelEvent }
+
+func (l LabelEvent) ID() string { return fmt.Sprintf("%d", l.LabelEvent.ID) }
+func (l LabelEvent) UserID() int { return l.User.ID }
+func (l LabelEvent) CreatedAt() time.Time { return *l.LabelEvent.CreatedAt }
+func (l LabelEvent) Kind() EventKind {
+ switch l.Action {
+ case "add":
+ return EventAddLabel
+ case "remove":
+ return EventRemoveLabel
+ default:
+ return EventUnknown
+ }
+}
+
+var _ Event = &StateEvent{}
+
+type StateEvent struct{ gitlab.StateEvent }
+
+func (s StateEvent) ID() string { return fmt.Sprintf("%d", s.StateEvent.ID) }
+func (s StateEvent) UserID() int { return s.User.ID }
+func (s StateEvent) CreatedAt() time.Time { return *s.StateEvent.CreatedAt }
+func (s StateEvent) Kind() EventKind {
+ switch s.State {
+ case "closed":
+ return EventClosed
+ case "opened", "reopened":
+ return EventReopened
+ default:
+ return EventUnknown
+ }
+}
+
+var _ Event = &ErrorEvent{}
+
+type ErrorEvent struct {
+ Err error
+ Time time.Time
+}
+
+func (e ErrorEvent) ID() string { return "" }
+func (e ErrorEvent) UserID() int { return -1 }
+func (e ErrorEvent) CreatedAt() time.Time { return e.Time }
+func (e ErrorEvent) Kind() EventKind { return EventError }
+
+// SortedEvents fan-in some Event-channels into one, sorted by creation date, using CreatedAt-method.
+// This function assume that each channel is pre-ordered.
+func SortedEvents(inputs ...<-chan Event) chan Event {
+ out := make(chan Event)
+
+ go func() {
+ defer close(out)
+
+ heads := make([]Event, len(inputs))
+
+ // pre-fill the head view
+ for i, input := range inputs {
+ if event, ok := <-input; ok {
+ heads[i] = event
+ }
+ }
+
+ for {
+ var earliestEvent Event
+ var originChannel int
+
+ // pick the earliest event of the heads
+ for i, head := range heads {
+ if head != nil && (earliestEvent == nil || head.CreatedAt().Before(earliestEvent.CreatedAt())) {
+ earliestEvent = head
+ originChannel = i
+ }
+ }
+
+ if earliestEvent == nil {
+ // no event anymore, we are done
+ return
+ }
+
+ // we have an event: consume it and replace it if possible
+ heads[originChannel] = nil
+ if event, ok := <-inputs[originChannel]; ok {
+ heads[originChannel] = event
+ }
+ out <- earliestEvent
+ }
+ }()
+
+ return out
+}
+
+// getNewTitle parses body diff given by gitlab api and return it final form
+// examples: "changed title from **fourth issue** to **fourth issue{+ changed+}**"
+// "changed title from **fourth issue{- changed-}** to **fourth issue**"
+// because Gitlab
+func getNewTitle(diff string) string {
+ newTitle := strings.Split(diff, "** to **")[1]
+ newTitle = strings.Replace(newTitle, "{+", "", -1)
+ newTitle = strings.Replace(newTitle, "+}", "", -1)
+ return strings.TrimSuffix(newTitle, "**")
+}
diff --git a/bridge/gitlab/import_notes_test.go b/bridge/gitlab/event_test.go
index c7b5ab56..860570d1 100644
--- a/bridge/gitlab/import_notes_test.go
+++ b/bridge/gitlab/event_test.go
@@ -2,8 +2,10 @@ package gitlab
import (
"testing"
+ "time"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestGetNewTitle(t *testing.T) {
@@ -54,3 +56,40 @@ func TestGetNewTitle(t *testing.T) {
})
}
}
+
+var _ Event = mockEvent(0)
+
+type mockEvent int64
+
+func (m mockEvent) ID() string { panic("implement me") }
+func (m mockEvent) UserID() int { panic("implement me") }
+func (m mockEvent) Kind() EventKind { panic("implement me") }
+func (m mockEvent) CreatedAt() time.Time { return time.Unix(int64(m), 0) }
+
+func TestSortedEvents(t *testing.T) {
+ makeInput := func(times ...int64) chan Event {
+ out := make(chan Event)
+ go func() {
+ for _, t := range times {
+ out <- mockEvent(t)
+ }
+ close(out)
+ }()
+ return out
+ }
+
+ sorted := SortedEvents(
+ makeInput(),
+ makeInput(1, 7, 9, 19),
+ makeInput(2, 8, 23),
+ makeInput(35, 48, 59, 64, 721),
+ )
+
+ var previous Event
+ for event := range sorted {
+ if previous != nil {
+ require.True(t, previous.CreatedAt().Before(event.CreatedAt()))
+ }
+ previous = event
+ }
+}
diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go
index 88b0d44e..422366c1 100644
--- a/bridge/gitlab/export_test.go
+++ b/bridge/gitlab/export_test.go
@@ -242,11 +242,6 @@ func TestGitlabPushPull(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if tt.name == "bug changed status" {
- t.Skip("test known as broken, see https://github.com/MichaelMure/git-bug/issues/435 and complain to gitlab")
- // TODO: fix, somehow, someday, or drop support.
- }
-
// for each operation a SetMetadataOperation will be added
// so number of operations should double
require.Len(t, tt.bug.Snapshot().Operations, tt.numOpExp)
diff --git a/bridge/gitlab/gitlab_api.go b/bridge/gitlab/gitlab_api.go
new file mode 100644
index 00000000..c00baf9d
--- /dev/null
+++ b/bridge/gitlab/gitlab_api.go
@@ -0,0 +1,140 @@
+package gitlab
+
+import (
+ "context"
+ "time"
+
+ "github.com/MichaelMure/git-bug/util/text"
+ "github.com/xanzy/go-gitlab"
+)
+
+// Issues returns a channel with gitlab project issues, ascending order.
+func Issues(ctx context.Context, client *gitlab.Client, pid string, since time.Time) <-chan *gitlab.Issue {
+ out := make(chan *gitlab.Issue)
+
+ go func() {
+ defer close(out)
+
+ opts := gitlab.ListProjectIssuesOptions{
+ UpdatedAfter: &since,
+ Scope: gitlab.String("all"),
+ Sort: gitlab.String("asc"),
+ }
+
+ for {
+ issues, resp, err := client.Issues.ListProjectIssues(pid, &opts, gitlab.WithContext(ctx))
+ if err != nil {
+ return
+ }
+
+ for _, issue := range issues {
+ out <- issue
+ }
+
+ if resp.CurrentPage >= resp.TotalPages {
+ break
+ }
+
+ opts.Page = resp.NextPage
+ }
+ }()
+
+ return out
+}
+
+// Notes returns a channel with note events
+func Notes(ctx context.Context, client *gitlab.Client, issue *gitlab.Issue) <-chan Event {
+ out := make(chan Event)
+
+ go func() {
+ defer close(out)
+
+ opts := gitlab.ListIssueNotesOptions{
+ OrderBy: gitlab.String("created_at"),
+ Sort: gitlab.String("asc"),
+ }
+
+ for {
+ notes, resp, err := client.Notes.ListIssueNotes(issue.ProjectID, issue.IID, &opts, gitlab.WithContext(ctx))
+
+ if err != nil {
+ out <- ErrorEvent{Err: err, Time: time.Now()}
+ }
+
+ for _, note := range notes {
+ out <- NoteEvent{*note}
+ }
+
+ if resp.CurrentPage >= resp.TotalPages {
+ break
+ }
+
+ opts.Page = resp.NextPage
+ }
+ }()
+
+ return out
+}
+
+// LabelEvents returns a channel with label events.
+func LabelEvents(ctx context.Context, client *gitlab.Client, issue *gitlab.Issue) <-chan Event {
+ out := make(chan Event)
+
+ go func() {
+ defer close(out)
+
+ opts := gitlab.ListLabelEventsOptions{}
+
+ for {
+ events, resp, err := client.ResourceLabelEvents.ListIssueLabelEvents(issue.ProjectID, issue.IID, &opts, gitlab.WithContext(ctx))
+
+ if err != nil {
+ out <- ErrorEvent{Err: err, Time: time.Now()}
+ }
+
+ for _, e := range events {
+ le := LabelEvent{*e}
+ le.Label.Name = text.CleanupOneLine(le.Label.Name)
+ out <- le
+ }
+
+ if resp.CurrentPage >= resp.TotalPages {
+ break
+ }
+
+ opts.Page = resp.NextPage
+ }
+ }()
+
+ return out
+}
+
+// StateEvents returns a channel with state change events.
+func StateEvents(ctx context.Context, client *gitlab.Client, issue *gitlab.Issue) <-chan Event {
+ out := make(chan Event)
+
+ go func() {
+ defer close(out)
+
+ opts := gitlab.ListStateEventsOptions{}
+
+ for {
+ events, resp, err := client.ResourceStateEvents.ListIssueStateEvents(issue.ProjectID, issue.IID, &opts, gitlab.WithContext(ctx))
+ if err != nil {
+ out <- ErrorEvent{Err: err, Time: time.Now()}
+ }
+
+ for _, e := range events {
+ out <- StateEvent{*e}
+ }
+
+ if resp.CurrentPage >= resp.TotalPages {
+ break
+ }
+
+ opts.Page = resp.NextPage
+ }
+ }()
+
+ return out
+}
diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go
index 79a92dac..879ef102 100644
--- a/bridge/gitlab/import.go
+++ b/bridge/gitlab/import.go
@@ -10,7 +10,6 @@ import (
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bridge/core/auth"
- "github.com/MichaelMure/git-bug/bridge/gitlab/iterator"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
@@ -24,9 +23,6 @@ type gitlabImporter struct {
// default client
client *gitlab.Client
- // iterator
- iterator *iterator.Iterator
-
// send only channel
out chan<- core.ImportResult
}
@@ -59,18 +55,15 @@ func (gi *gitlabImporter) Init(_ context.Context, repo *cache.RepoCache, conf co
// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
// of the missing issues / comments / label events / title changes ...
func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
- gi.iterator = iterator.NewIterator(ctx, gi.client, 10, gi.conf[confKeyProjectID], since)
+
out := make(chan core.ImportResult)
gi.out = out
go func() {
- defer close(gi.out)
+ defer close(out)
- // Loop over all matching issues
- for gi.iterator.NextIssue() {
- issue := gi.iterator.IssueValue()
+ for issue := range Issues(ctx, gi.client, gi.conf[confKeyProjectID], since) {
- // create issue
b, err := gi.ensureIssue(repo, issue)
if err != nil {
err := fmt.Errorf("issue creation: %v", err)
@@ -78,23 +71,20 @@ func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache,
return
}
- // Loop over all notes
- for gi.iterator.NextNote() {
- note := gi.iterator.NoteValue()
- if err := gi.ensureNote(repo, b, note); err != nil {
- err := fmt.Errorf("note creation: %v", err)
- out <- core.NewImportError(err, entity.Id(strconv.Itoa(note.ID)))
- return
- }
- }
+ issueEvents := SortedEvents(
+ Notes(ctx, gi.client, issue),
+ LabelEvents(ctx, gi.client, issue),
+ StateEvents(ctx, gi.client, issue),
+ )
- // Loop over all label events
- for gi.iterator.NextLabelEvent() {
- labelEvent := gi.iterator.LabelEventValue()
- if err := gi.ensureLabelEvent(repo, b, labelEvent); err != nil {
- err := fmt.Errorf("label event creation: %v", err)
- out <- core.NewImportError(err, entity.Id(strconv.Itoa(labelEvent.ID)))
- return
+ for e := range issueEvents {
+ if e, ok := e.(ErrorEvent); ok {
+ out <- core.NewImportError(e.Err, "")
+ continue
+ }
+ if err := gi.ensureIssueEvent(repo, b, issue, e); err != nil {
+ err := fmt.Errorf("issue event creation: %v", err)
+ out <- core.NewImportError(err, entity.Id(e.ID()))
}
}
@@ -107,10 +97,6 @@ func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache,
return
}
}
-
- if err := gi.iterator.Error(); err != nil {
- out <- core.NewImportError(err, "")
- }
}()
return out, nil
@@ -126,7 +112,7 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue
// resolve bug
b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
- excerpt.CreateMetadata[metaKeyGitlabId] == parseID(issue.IID) &&
+ excerpt.CreateMetadata[metaKeyGitlabId] == fmt.Sprintf("%d", issue.IID) &&
excerpt.CreateMetadata[metaKeyGitlabBaseUrl] == gi.conf[confKeyGitlabBaseUrl] &&
excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyProjectID]
})
@@ -146,7 +132,7 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue
nil,
map[string]string{
core.MetaKeyOrigin: target,
- metaKeyGitlabId: parseID(issue.IID),
+ metaKeyGitlabId: fmt.Sprintf("%d", issue.IID),
metaKeyGitlabUrl: issue.WebURL,
metaKeyGitlabProject: gi.conf[confKeyProjectID],
metaKeyGitlabBaseUrl: gi.conf[confKeyGitlabBaseUrl],
@@ -163,50 +149,49 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue
return b, nil
}
-func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
- gitlabID := parseID(note.ID)
+func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCache, issue *gitlab.Issue, event Event) error {
- id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, gitlabID)
+ id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, event.ID())
if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
return errResolve
}
// ensure issue author
- author, err := gi.ensurePerson(repo, note.Author.ID)
+ author, err := gi.ensurePerson(repo, event.UserID())
if err != nil {
return err
}
- noteType, body := GetNoteType(note)
- switch noteType {
- case NOTE_CLOSED:
+ switch event.Kind() {
+ case EventClosed:
if errResolve == nil {
return nil
}
op, err := b.CloseRaw(
author,
- note.CreatedAt.Unix(),
+ event.CreatedAt().Unix(),
map[string]string{
- metaKeyGitlabId: gitlabID,
+ metaKeyGitlabId: event.ID(),
},
)
+
if err != nil {
return err
}
gi.out <- core.NewImportStatusChange(op.Id())
- case NOTE_REOPENED:
+ case EventReopened:
if errResolve == nil {
return nil
}
op, err := b.OpenRaw(
author,
- note.CreatedAt.Unix(),
+ event.CreatedAt().Unix(),
map[string]string{
- metaKeyGitlabId: gitlabID,
+ metaKeyGitlabId: event.ID(),
},
)
if err != nil {
@@ -215,9 +200,7 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
gi.out <- core.NewImportStatusChange(op.Id())
- case NOTE_DESCRIPTION_CHANGED:
- issue := gi.iterator.IssueValue()
-
+ case EventDescriptionChanged:
firstComment := b.Snapshot().Comments[0]
// since gitlab doesn't provide the issue history
// we should check for "changed the description" notes and compare issue texts
@@ -226,11 +209,11 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
// comment edition
op, err := b.EditCommentRaw(
author,
- note.UpdatedAt.Unix(),
+ event.(NoteEvent).UpdatedAt.Unix(),
firstComment.Id(),
text.Cleanup(issue.Description),
map[string]string{
- metaKeyGitlabId: gitlabID,
+ metaKeyGitlabId: event.ID(),
},
)
if err != nil {
@@ -240,8 +223,8 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
gi.out <- core.NewImportTitleEdition(op.Id())
}
- case NOTE_COMMENT:
- cleanText := text.Cleanup(body)
+ case EventComment:
+ cleanText := text.Cleanup(event.(NoteEvent).Body)
// if we didn't import the comment
if errResolve == cache.ErrNoMatchingOp {
@@ -249,11 +232,11 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
// add comment operation
op, err := b.AddCommentRaw(
author,
- note.CreatedAt.Unix(),
+ event.CreatedAt().Unix(),
cleanText,
nil,
map[string]string{
- metaKeyGitlabId: gitlabID,
+ metaKeyGitlabId: event.ID(),
},
)
if err != nil {
@@ -271,12 +254,12 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
return err
}
- // compare local bug comment with the new note body
+ // compare local bug comment with the new event body
if comment.Message != cleanText {
// comment edition
op, err := b.EditCommentRaw(
author,
- note.UpdatedAt.Unix(),
+ event.(NoteEvent).UpdatedAt.Unix(),
comment.Id(),
cleanText,
nil,
@@ -290,7 +273,7 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
return nil
- case NOTE_TITLE_CHANGED:
+ case EventTitleChanged:
// title change events are given new notes
if errResolve == nil {
return nil
@@ -298,10 +281,10 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
op, err := b.SetTitleRaw(
author,
- note.CreatedAt.Unix(),
- text.CleanupOneLine(body),
+ event.CreatedAt().Unix(),
+ event.(NoteEvent).Title(),
map[string]string{
- metaKeyGitlabId: gitlabID,
+ metaKeyGitlabId: event.ID(),
},
)
if err != nil {
@@ -310,67 +293,48 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
gi.out <- core.NewImportTitleEdition(op.Id())
- case NOTE_UNKNOWN,
- NOTE_ASSIGNED,
- NOTE_UNASSIGNED,
- NOTE_CHANGED_MILESTONE,
- NOTE_REMOVED_MILESTONE,
- NOTE_CHANGED_DUEDATE,
- NOTE_REMOVED_DUEDATE,
- NOTE_LOCKED,
- NOTE_UNLOCKED,
- NOTE_MENTIONED_IN_ISSUE,
- NOTE_MENTIONED_IN_MERGE_REQUEST:
-
- return nil
-
- default:
- panic("unhandled note type")
- }
-
- return nil
-}
-
-func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
- _, err := b.ResolveOperationWithMetadata(metaKeyGitlabId, 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":
+ case EventAddLabel:
_, err = b.ForceChangeLabelsRaw(
author,
- labelEvent.CreatedAt.Unix(),
- []string{text.CleanupOneLine(labelEvent.Label.Name)},
+ event.CreatedAt().Unix(),
+ []string{event.(LabelEvent).Label.Name},
nil,
map[string]string{
- metaKeyGitlabId: parseID(labelEvent.ID),
+ metaKeyGitlabId: event.ID(),
},
)
+ return err
- case "remove":
+ case EventRemoveLabel:
_, err = b.ForceChangeLabelsRaw(
author,
- labelEvent.CreatedAt.Unix(),
+ event.CreatedAt().Unix(),
nil,
- []string{text.CleanupOneLine(labelEvent.Label.Name)},
+ []string{event.(LabelEvent).Label.Name},
map[string]string{
- metaKeyGitlabId: parseID(labelEvent.ID),
+ metaKeyGitlabId: event.ID(),
},
)
+ return err
+
+ case EventAssigned,
+ EventUnassigned,
+ EventChangedMilestone,
+ EventRemovedMilestone,
+ EventChangedDuedate,
+ EventRemovedDuedate,
+ EventLocked,
+ EventUnlocked,
+ EventMentionedInIssue,
+ EventMentionedInMergeRequest:
+
+ return nil
default:
- err = fmt.Errorf("unexpected label event action")
+ return fmt.Errorf("unexpected event")
}
- return err
+ return nil
}
func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
@@ -407,7 +371,3 @@ func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.Id
gi.out <- core.NewImportIdentity(i.Id())
return i, nil
}
-
-func parseID(id int) string {
- return fmt.Sprintf("%d", id)
-}
diff --git a/bridge/gitlab/import_notes.go b/bridge/gitlab/import_notes.go
deleted file mode 100644
index b38cb371..00000000
--- a/bridge/gitlab/import_notes.go
+++ /dev/null
@@ -1,147 +0,0 @@
-package gitlab
-
-import (
- "strings"
-
- "github.com/xanzy/go-gitlab"
-)
-
-type NoteType int
-
-const (
- _ NoteType = iota
- NOTE_COMMENT
- NOTE_TITLE_CHANGED
- NOTE_DESCRIPTION_CHANGED
- NOTE_CLOSED
- NOTE_REOPENED
- NOTE_LOCKED
- NOTE_UNLOCKED
- NOTE_CHANGED_DUEDATE
- NOTE_REMOVED_DUEDATE
- NOTE_ASSIGNED
- NOTE_UNASSIGNED
- NOTE_CHANGED_MILESTONE
- NOTE_REMOVED_MILESTONE
- NOTE_MENTIONED_IN_ISSUE
- NOTE_MENTIONED_IN_MERGE_REQUEST
- NOTE_UNKNOWN
-)
-
-func (nt NoteType) String() string {
- switch nt {
- case NOTE_COMMENT:
- return "note comment"
- case NOTE_TITLE_CHANGED:
- return "note title changed"
- case NOTE_DESCRIPTION_CHANGED:
- return "note description changed"
- case NOTE_CLOSED:
- return "note closed"
- case NOTE_REOPENED:
- return "note reopened"
- case NOTE_LOCKED:
- return "note locked"
- case NOTE_UNLOCKED:
- return "note unlocked"
- case NOTE_CHANGED_DUEDATE:
- return "note changed duedate"
- case NOTE_REMOVED_DUEDATE:
- return "note remove duedate"
- case NOTE_ASSIGNED:
- return "note assigned"
- case NOTE_UNASSIGNED:
- return "note unassigned"
- case NOTE_CHANGED_MILESTONE:
- return "note changed milestone"
- case NOTE_REMOVED_MILESTONE:
- return "note removed in milestone"
- case NOTE_MENTIONED_IN_ISSUE:
- return "note mentioned in issue"
- case NOTE_MENTIONED_IN_MERGE_REQUEST:
- return "note mentioned in merge request"
- case NOTE_UNKNOWN:
- return "note unknown"
- default:
- panic("unknown note type")
- }
-}
-
-// GetNoteType parse a note system and body and return the note type and it content
-func GetNoteType(n *gitlab.Note) (NoteType, string) {
- // when a note is a comment system is set to false
- // when a note is a different event system is set to true
- // because Gitlab
- if !n.System {
- return NOTE_COMMENT, n.Body
- }
-
- if n.Body == "closed" {
- return NOTE_CLOSED, ""
- }
-
- if n.Body == "reopened" {
- return NOTE_REOPENED, ""
- }
-
- if n.Body == "changed the description" {
- return NOTE_DESCRIPTION_CHANGED, ""
- }
-
- if n.Body == "locked this issue" {
- return NOTE_LOCKED, ""
- }
-
- if n.Body == "unlocked this issue" {
- return NOTE_UNLOCKED, ""
- }
-
- if strings.HasPrefix(n.Body, "changed title from") {
- return NOTE_TITLE_CHANGED, getNewTitle(n.Body)
- }
-
- if strings.HasPrefix(n.Body, "changed due date to") {
- return NOTE_CHANGED_DUEDATE, ""
- }
-
- if n.Body == "removed due date" {
- return NOTE_REMOVED_DUEDATE, ""
- }
-
- if strings.HasPrefix(n.Body, "assigned to @") {
- return NOTE_ASSIGNED, ""
- }
-
- if strings.HasPrefix(n.Body, "unassigned @") {
- return NOTE_UNASSIGNED, ""
- }
-
- if strings.HasPrefix(n.Body, "changed milestone to %") {
- return NOTE_CHANGED_MILESTONE, ""
- }
-
- if strings.HasPrefix(n.Body, "removed milestone") {
- return NOTE_REMOVED_MILESTONE, ""
- }
-
- if strings.HasPrefix(n.Body, "mentioned in issue") {
- return NOTE_MENTIONED_IN_ISSUE, ""
- }
-
- if strings.HasPrefix(n.Body, "mentioned in merge request") {
- return NOTE_MENTIONED_IN_MERGE_REQUEST, ""
- }
-
- return NOTE_UNKNOWN, ""
-}
-
-// getNewTitle parses body diff given by gitlab api and return it final form
-// examples: "changed title from **fourth issue** to **fourth issue{+ changed+}**"
-// "changed title from **fourth issue{- changed-}** to **fourth issue**"
-// because Gitlab
-func getNewTitle(diff string) string {
- newTitle := strings.Split(diff, "** to **")[1]
- newTitle = strings.Replace(newTitle, "{+", "", -1)
- newTitle = strings.Replace(newTitle, "+}", "", -1)
- return strings.TrimSuffix(newTitle, "**")
-}
diff --git a/bridge/gitlab/iterator/issue.go b/bridge/gitlab/iterator/issue.go
deleted file mode 100644
index 9361b496..00000000
--- a/bridge/gitlab/iterator/issue.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package iterator
-
-import (
- "context"
-
- "github.com/xanzy/go-gitlab"
-)
-
-type issueIterator struct {
- page int
- lastPage bool
- index int
- cache []*gitlab.Issue
-}
-
-func newIssueIterator() *issueIterator {
- ii := &issueIterator{}
- ii.Reset()
- return ii
-}
-
-func (ii *issueIterator) Next(ctx context.Context, conf config) (bool, error) {
- // first query
- if ii.cache == nil {
- return ii.getNext(ctx, conf)
- }
-
- // move cursor index
- if ii.index < len(ii.cache)-1 {
- ii.index++
- return true, nil
- }
-
- return ii.getNext(ctx, conf)
-}
-
-func (ii *issueIterator) Value() *gitlab.Issue {
- return ii.cache[ii.index]
-}
-
-func (ii *issueIterator) getNext(ctx context.Context, conf config) (bool, error) {
- if ii.lastPage {
- return false, nil
- }
-
- ctx, cancel := context.WithTimeout(ctx, conf.timeout)
- defer cancel()
-
- issues, resp, err := conf.gc.Issues.ListProjectIssues(
- conf.project,
- &gitlab.ListProjectIssuesOptions{
- ListOptions: gitlab.ListOptions{
- Page: ii.page,
- PerPage: conf.capacity,
- },
- Scope: gitlab.String("all"),
- UpdatedAfter: &conf.since,
- Sort: gitlab.String("asc"),
- },
- gitlab.WithContext(ctx),
- )
-
- if err != nil {
- ii.Reset()
- return false, err
- }
-
- if resp.TotalPages == ii.page {
- ii.lastPage = true
- }
-
- // if repository doesn't have any issues
- if len(issues) == 0 {
- return false, nil
- }
-
- ii.cache = issues
- ii.index = 0
- ii.page++
-
- return true, nil
-}
-
-func (ii *issueIterator) Reset() {
- ii.index = -1
- ii.page = 1
- ii.lastPage = false
- ii.cache = nil
-}
diff --git a/bridge/gitlab/iterator/iterator.go b/bridge/gitlab/iterator/iterator.go
deleted file mode 100644
index ee2090b0..00000000
--- a/bridge/gitlab/iterator/iterator.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package iterator
-
-import (
- "context"
- "time"
-
- "github.com/xanzy/go-gitlab"
-)
-
-type Iterator struct {
- // shared context
- ctx context.Context
-
- // to pass to sub-iterators
- conf config
-
- // sticky error
- err error
-
- // issues iterator
- issue *issueIterator
-
- // notes iterator
- note *noteIterator
-
- // labelEvent iterator
- labelEvent *labelEventIterator
-}
-
-type config struct {
- // gitlab api v4 client
- gc *gitlab.Client
-
- timeout time.Duration
-
- // if since is given the iterator will query only the issues
- // updated after this date
- since time.Time
-
- // project id
- project string
-
- // number of issues and notes to query at once
- capacity int
-}
-
-// NewIterator create a new iterator
-func NewIterator(ctx context.Context, client *gitlab.Client, capacity int, projectID string, since time.Time) *Iterator {
- return &Iterator{
- ctx: ctx,
- conf: config{
- gc: client,
- timeout: 60 * time.Second,
- since: since,
- project: projectID,
- capacity: capacity,
- },
- issue: newIssueIterator(),
- note: newNoteIterator(),
- labelEvent: newLabelEventIterator(),
- }
-}
-
-// Error return last encountered error
-func (i *Iterator) Error() error {
- return i.err
-}
-
-func (i *Iterator) NextIssue() bool {
- if i.err != nil {
- return false
- }
-
- if i.ctx.Err() != nil {
- return false
- }
-
- more, err := i.issue.Next(i.ctx, i.conf)
- if err != nil {
- i.err = err
- return false
- }
-
- // Also reset the other sub iterators as they would
- // no longer be valid
- i.note.Reset(i.issue.Value().IID)
- i.labelEvent.Reset(i.issue.Value().IID)
-
- return more
-}
-
-func (i *Iterator) IssueValue() *gitlab.Issue {
- return i.issue.Value()
-}
-
-func (i *Iterator) NextNote() bool {
- if i.err != nil {
- return false
- }
-
- if i.ctx.Err() != nil {
- return false
- }
-
- more, err := i.note.Next(i.ctx, i.conf)
- if err != nil {
- i.err = err
- return false
- }
-
- return more
-}
-
-func (i *Iterator) NoteValue() *gitlab.Note {
- return i.note.Value()
-}
-
-func (i *Iterator) NextLabelEvent() bool {
- if i.err != nil {
- return false
- }
-
- if i.ctx.Err() != nil {
- return false
- }
-
- more, err := i.labelEvent.Next(i.ctx, i.conf)
- if err != nil {
- i.err = err
- return false
- }
-
- return more
-}
-
-func (i *Iterator) LabelEventValue() *gitlab.LabelEvent {
- return i.labelEvent.Value()
-}
diff --git a/bridge/gitlab/iterator/labelEvent.go b/bridge/gitlab/iterator/labelEvent.go
deleted file mode 100644
index 812e6646..00000000
--- a/bridge/gitlab/iterator/labelEvent.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package iterator
-
-import (
- "context"
- "sort"
-
- "github.com/xanzy/go-gitlab"
-)
-
-// Since Gitlab does not return the label events items in the correct order
-// we need to sort the list ourselves and stop relying on the pagination model
-// #BecauseGitlab
-type labelEventIterator struct {
- issue int
- index int
- cache []*gitlab.LabelEvent
-}
-
-func newLabelEventIterator() *labelEventIterator {
- lei := &labelEventIterator{}
- lei.Reset(-1)
- return lei
-}
-
-func (lei *labelEventIterator) Next(ctx context.Context, conf config) (bool, error) {
- // first query
- if lei.cache == nil {
- return lei.getNext(ctx, conf)
- }
-
- // move cursor index
- if lei.index < len(lei.cache)-1 {
- lei.index++
- return true, nil
- }
-
- return false, nil
-}
-
-func (lei *labelEventIterator) Value() *gitlab.LabelEvent {
- return lei.cache[lei.index]
-}
-
-func (lei *labelEventIterator) getNext(ctx context.Context, conf config) (bool, error) {
- ctx, cancel := context.WithTimeout(ctx, conf.timeout)
- defer cancel()
-
- // since order is not guaranteed we should query all label events
- // and sort them by ID
- page := 1
- for {
- labelEvents, resp, err := conf.gc.ResourceLabelEvents.ListIssueLabelEvents(
- conf.project,
- lei.issue,
- &gitlab.ListLabelEventsOptions{
- ListOptions: gitlab.ListOptions{
- Page: page,
- PerPage: conf.capacity,
- },
- },
- gitlab.WithContext(ctx),
- )
- if err != nil {
- lei.Reset(-1)
- return false, err
- }
-
- if len(labelEvents) == 0 {
- break
- }
-
- lei.cache = append(lei.cache, labelEvents...)
-
- if resp.TotalPages == page {
- break
- }
-
- page++
- }
-
- sort.Sort(lei)
- lei.index = 0
-
- return len(lei.cache) > 0, nil
-}
-
-func (lei *labelEventIterator) Reset(issue int) {
- lei.issue = issue
- lei.index = -1
- lei.cache = nil
-}
-
-// ORDERING
-
-func (lei *labelEventIterator) Len() int {
- return len(lei.cache)
-}
-
-func (lei *labelEventIterator) Swap(i, j int) {
- lei.cache[i], lei.cache[j] = lei.cache[j], lei.cache[i]
-}
-
-func (lei *labelEventIterator) Less(i, j int) bool {
- return lei.cache[i].ID < lei.cache[j].ID
-}
diff --git a/bridge/gitlab/iterator/note.go b/bridge/gitlab/iterator/note.go
deleted file mode 100644
index a1e0544c..00000000
--- a/bridge/gitlab/iterator/note.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package iterator
-
-import (
- "context"
-
- "github.com/xanzy/go-gitlab"
-)
-
-type noteIterator struct {
- issue int
- page int
- lastPage bool
- index int
- cache []*gitlab.Note
-}
-
-func newNoteIterator() *noteIterator {
- in := &noteIterator{}
- in.Reset(-1)
- return in
-}
-
-func (in *noteIterator) Next(ctx context.Context, conf config) (bool, error) {
- // first query
- if in.cache == nil {
- return in.getNext(ctx, conf)
- }
-
- // move cursor index
- if in.index < len(in.cache)-1 {
- in.index++
- return true, nil
- }
-
- return in.getNext(ctx, conf)
-}
-
-func (in *noteIterator) Value() *gitlab.Note {
- return in.cache[in.index]
-}
-
-func (in *noteIterator) getNext(ctx context.Context, conf config) (bool, error) {
- if in.lastPage {
- return false, nil
- }
-
- ctx, cancel := context.WithTimeout(ctx, conf.timeout)
- defer cancel()
-
- notes, resp, err := conf.gc.Notes.ListIssueNotes(
- conf.project,
- in.issue,
- &gitlab.ListIssueNotesOptions{
- ListOptions: gitlab.ListOptions{
- Page: in.page,
- PerPage: conf.capacity,
- },
- Sort: gitlab.String("asc"),
- OrderBy: gitlab.String("created_at"),
- },
- gitlab.WithContext(ctx),
- )
-
- if err != nil {
- in.Reset(-1)
- return false, err
- }
-
- if resp.TotalPages == in.page {
- in.lastPage = true
- }
-
- if len(notes) == 0 {
- return false, nil
- }
-
- in.cache = notes
- in.index = 0
- in.page++
-
- return true, nil
-}
-
-func (in *noteIterator) Reset(issue int) {
- in.issue = issue
- in.index = -1
- in.page = 1
- in.lastPage = false
- in.cache = nil
-}