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