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