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