import.go

  1package gitlab
  2
  3import (
  4	"context"
  5	"fmt"
  6	"strconv"
  7	"time"
  8
  9	"github.com/xanzy/go-gitlab"
 10
 11	"github.com/MichaelMure/git-bug/bridge/core"
 12	"github.com/MichaelMure/git-bug/bug"
 13	"github.com/MichaelMure/git-bug/cache"
 14	"github.com/MichaelMure/git-bug/entity"
 15	"github.com/MichaelMure/git-bug/util/text"
 16)
 17
 18// gitlabImporter implement the Importer interface
 19type gitlabImporter struct {
 20	conf core.Configuration
 21
 22	// iterator
 23	iterator *iterator
 24
 25	// send only channel
 26	out chan<- core.ImportResult
 27}
 28
 29func (gi *gitlabImporter) Init(conf core.Configuration) error {
 30	gi.conf = conf
 31	return nil
 32}
 33
 34// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
 35// of the missing issues / comments / label events / title changes ...
 36func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
 37	gi.iterator = NewIterator(ctx, 10, gi.conf[keyProjectID], gi.conf[core.ConfigKeyToken], since)
 38	out := make(chan core.ImportResult)
 39	gi.out = out
 40
 41	go func() {
 42		defer close(gi.out)
 43
 44		// Loop over all matching issues
 45		for gi.iterator.NextIssue() {
 46			issue := gi.iterator.IssueValue()
 47
 48			// create issue
 49			b, err := gi.ensureIssue(repo, issue)
 50			if err != nil {
 51				err := fmt.Errorf("issue creation: %v", err)
 52				out <- core.NewImportError(err, "")
 53				return
 54			}
 55
 56			// Loop over all notes
 57			for gi.iterator.NextNote() {
 58				note := gi.iterator.NoteValue()
 59				if err := gi.ensureNote(repo, b, note); err != nil {
 60					err := fmt.Errorf("note creation: %v", err)
 61					out <- core.NewImportError(err, entity.Id(strconv.Itoa(note.ID)))
 62					return
 63				}
 64			}
 65
 66			// Loop over all label events
 67			for gi.iterator.NextLabelEvent() {
 68				labelEvent := gi.iterator.LabelEventValue()
 69				if err := gi.ensureLabelEvent(repo, b, labelEvent); err != nil {
 70					err := fmt.Errorf("label event creation: %v", err)
 71					out <- core.NewImportError(err, entity.Id(strconv.Itoa(labelEvent.ID)))
 72					return
 73				}
 74			}
 75
 76			if !b.NeedCommit() {
 77				out <- core.NewImportNothing(b.Id(), "no imported operation")
 78			} else if err := b.Commit(); err != nil {
 79				// commit bug state
 80				err := fmt.Errorf("bug commit: %v", err)
 81				out <- core.NewImportError(err, "")
 82				return
 83			}
 84		}
 85
 86		if err := gi.iterator.Error(); err != nil {
 87			out <- core.NewImportError(err, "")
 88		}
 89	}()
 90
 91	return out, nil
 92}
 93
 94func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
 95	// ensure issue author
 96	author, err := gi.ensurePerson(repo, issue.Author.ID)
 97	if err != nil {
 98		return nil, err
 99	}
100
101	// resolve bug
102	b, err := repo.ResolveBugCreateMetadata(metaKeyGitlabUrl, issue.WebURL)
103	if err == nil {
104		return b, nil
105	}
106	if err != bug.ErrBugNotExist {
107		return nil, err
108	}
109
110	// if bug was never imported
111	cleanText, err := text.Cleanup(issue.Description)
112	if err != nil {
113		return nil, err
114	}
115
116	// create bug
117	b, _, err = repo.NewBugRaw(
118		author,
119		issue.CreatedAt.Unix(),
120		issue.Title,
121		cleanText,
122		nil,
123		map[string]string{
124			core.MetaKeyOrigin:   target,
125			metaKeyGitlabId:      parseID(issue.IID),
126			metaKeyGitlabUrl:     issue.WebURL,
127			metaKeyGitlabProject: gi.conf[keyProjectID],
128		},
129	)
130
131	if err != nil {
132		return nil, err
133	}
134
135	// importing a new bug
136	gi.out <- core.NewImportBug(b.Id())
137
138	return b, nil
139}
140
141func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
142	gitlabID := parseID(note.ID)
143
144	id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, gitlabID)
145	if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
146		return errResolve
147	}
148
149	// ensure issue author
150	author, err := gi.ensurePerson(repo, note.Author.ID)
151	if err != nil {
152		return err
153	}
154
155	noteType, body := GetNoteType(note)
156	switch noteType {
157	case NOTE_CLOSED:
158		if errResolve == nil {
159			return nil
160		}
161
162		op, err := b.CloseRaw(
163			author,
164			note.CreatedAt.Unix(),
165			map[string]string{
166				metaKeyGitlabId: gitlabID,
167			},
168		)
169		if err != nil {
170			return err
171		}
172
173		gi.out <- core.NewImportStatusChange(op.Id())
174
175	case NOTE_REOPENED:
176		if errResolve == nil {
177			return nil
178		}
179
180		op, err := b.OpenRaw(
181			author,
182			note.CreatedAt.Unix(),
183			map[string]string{
184				metaKeyGitlabId: gitlabID,
185			},
186		)
187		if err != nil {
188			return err
189		}
190
191		gi.out <- core.NewImportStatusChange(op.Id())
192
193	case NOTE_DESCRIPTION_CHANGED:
194		issue := gi.iterator.IssueValue()
195
196		firstComment := b.Snapshot().Comments[0]
197		// since gitlab doesn't provide the issue history
198		// we should check for "changed the description" notes and compare issue texts
199		// TODO: Check only one time and ignore next 'description change' within one issue
200		if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
201			// comment edition
202			op, err := b.EditCommentRaw(
203				author,
204				note.UpdatedAt.Unix(),
205				firstComment.Id(),
206				issue.Description,
207				map[string]string{
208					metaKeyGitlabId: gitlabID,
209				},
210			)
211			if err != nil {
212				return err
213			}
214
215			gi.out <- core.NewImportTitleEdition(op.Id())
216		}
217
218	case NOTE_COMMENT:
219		cleanText, err := text.Cleanup(body)
220		if err != nil {
221			return err
222		}
223
224		// if we didn't import the comment
225		if errResolve == cache.ErrNoMatchingOp {
226
227			// add comment operation
228			op, err := b.AddCommentRaw(
229				author,
230				note.CreatedAt.Unix(),
231				cleanText,
232				nil,
233				map[string]string{
234					metaKeyGitlabId: gitlabID,
235				},
236			)
237			if err != nil {
238				return err
239			}
240			gi.out <- core.NewImportComment(op.Id())
241			return nil
242		}
243
244		// if comment was already exported
245
246		// search for last comment update
247		comment, err := b.Snapshot().SearchComment(id)
248		if err != nil {
249			return err
250		}
251
252		// compare local bug comment with the new note body
253		if comment.Message != cleanText {
254			// comment edition
255			op, err := b.EditCommentRaw(
256				author,
257				note.UpdatedAt.Unix(),
258				comment.Id(),
259				cleanText,
260				nil,
261			)
262
263			if err != nil {
264				return err
265			}
266			gi.out <- core.NewImportCommentEdition(op.Id())
267		}
268
269		return nil
270
271	case NOTE_TITLE_CHANGED:
272		// title change events are given new notes
273		if errResolve == nil {
274			return nil
275		}
276
277		op, err := b.SetTitleRaw(
278			author,
279			note.CreatedAt.Unix(),
280			body,
281			map[string]string{
282				metaKeyGitlabId: gitlabID,
283			},
284		)
285		if err != nil {
286			return err
287		}
288
289		gi.out <- core.NewImportTitleEdition(op.Id())
290
291	case NOTE_UNKNOWN,
292		NOTE_ASSIGNED,
293		NOTE_UNASSIGNED,
294		NOTE_CHANGED_MILESTONE,
295		NOTE_REMOVED_MILESTONE,
296		NOTE_CHANGED_DUEDATE,
297		NOTE_REMOVED_DUEDATE,
298		NOTE_LOCKED,
299		NOTE_UNLOCKED,
300		NOTE_MENTIONED_IN_ISSUE,
301		NOTE_MENTIONED_IN_MERGE_REQUEST:
302
303		return nil
304
305	default:
306		panic("unhandled note type")
307	}
308
309	return nil
310}
311
312func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
313	_, err := b.ResolveOperationWithMetadata(metaKeyGitlabId, parseID(labelEvent.ID))
314	if err != cache.ErrNoMatchingOp {
315		return err
316	}
317
318	// ensure issue author
319	author, err := gi.ensurePerson(repo, labelEvent.User.ID)
320	if err != nil {
321		return err
322	}
323
324	switch labelEvent.Action {
325	case "add":
326		_, err = b.ForceChangeLabelsRaw(
327			author,
328			labelEvent.CreatedAt.Unix(),
329			[]string{labelEvent.Label.Name},
330			nil,
331			map[string]string{
332				metaKeyGitlabId: parseID(labelEvent.ID),
333			},
334		)
335
336	case "remove":
337		_, err = b.ForceChangeLabelsRaw(
338			author,
339			labelEvent.CreatedAt.Unix(),
340			nil,
341			[]string{labelEvent.Label.Name},
342			map[string]string{
343				metaKeyGitlabId: parseID(labelEvent.ID),
344			},
345		)
346
347	default:
348		err = fmt.Errorf("unexpected label event action")
349	}
350
351	return err
352}
353
354func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
355	// Look first in the cache
356	i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id))
357	if err == nil {
358		return i, nil
359	}
360	if _, ok := err.(entity.ErrMultipleMatch); ok {
361		return nil, err
362	}
363
364	client := buildClient(gi.conf["token"])
365
366	user, _, err := client.Users.GetUser(id)
367	if err != nil {
368		return nil, err
369	}
370
371	i, err = repo.NewIdentityRaw(
372		user.Name,
373		user.PublicEmail,
374		user.Username,
375		user.AvatarURL,
376		map[string]string{
377			// because Gitlab
378			metaKeyGitlabId:    strconv.Itoa(id),
379			metaKeyGitlabLogin: user.Username,
380		},
381	)
382	if err != nil {
383		return nil, err
384	}
385
386	gi.out <- core.NewImportIdentity(i.Id())
387	return i, nil
388}
389
390func parseID(id int) string {
391	return fmt.Sprintf("%d", id)
392}