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