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[confKeyProjectID] &&
131			excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyGitlabBaseUrl]
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	cleanText, err := text.Cleanup(issue.Description)
142	if err != nil {
143		return nil, err
144	}
145
146	// create bug
147	b, _, err = repo.NewBugRaw(
148		author,
149		issue.CreatedAt.Unix(),
150		issue.Title,
151		cleanText,
152		nil,
153		map[string]string{
154			core.MetaKeyOrigin:   target,
155			metaKeyGitlabId:      parseID(issue.IID),
156			metaKeyGitlabUrl:     issue.WebURL,
157			metaKeyGitlabProject: gi.conf[confKeyProjectID],
158			metaKeyGitlabBaseUrl: gi.conf[confKeyGitlabBaseUrl],
159		},
160	)
161
162	if err != nil {
163		return nil, err
164	}
165
166	// importing a new bug
167	gi.out <- core.NewImportBug(b.Id())
168
169	return b, nil
170}
171
172func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
173	gitlabID := parseID(note.ID)
174
175	id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, gitlabID)
176	if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
177		return errResolve
178	}
179
180	// ensure issue author
181	author, err := gi.ensurePerson(repo, note.Author.ID)
182	if err != nil {
183		return err
184	}
185
186	noteType, body := GetNoteType(note)
187	switch noteType {
188	case NOTE_CLOSED:
189		if errResolve == nil {
190			return nil
191		}
192
193		op, err := b.CloseRaw(
194			author,
195			note.CreatedAt.Unix(),
196			map[string]string{
197				metaKeyGitlabId: gitlabID,
198			},
199		)
200		if err != nil {
201			return err
202		}
203
204		gi.out <- core.NewImportStatusChange(op.Id())
205
206	case NOTE_REOPENED:
207		if errResolve == nil {
208			return nil
209		}
210
211		op, err := b.OpenRaw(
212			author,
213			note.CreatedAt.Unix(),
214			map[string]string{
215				metaKeyGitlabId: gitlabID,
216			},
217		)
218		if err != nil {
219			return err
220		}
221
222		gi.out <- core.NewImportStatusChange(op.Id())
223
224	case NOTE_DESCRIPTION_CHANGED:
225		issue := gi.iterator.IssueValue()
226
227		firstComment := b.Snapshot().Comments[0]
228		// since gitlab doesn't provide the issue history
229		// we should check for "changed the description" notes and compare issue texts
230		// TODO: Check only one time and ignore next 'description change' within one issue
231		if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
232			// comment edition
233			op, err := b.EditCommentRaw(
234				author,
235				note.UpdatedAt.Unix(),
236				firstComment.Id(),
237				issue.Description,
238				map[string]string{
239					metaKeyGitlabId: gitlabID,
240				},
241			)
242			if err != nil {
243				return err
244			}
245
246			gi.out <- core.NewImportTitleEdition(op.Id())
247		}
248
249	case NOTE_COMMENT:
250		cleanText, err := text.Cleanup(body)
251		if err != nil {
252			return err
253		}
254
255		// if we didn't import the comment
256		if errResolve == cache.ErrNoMatchingOp {
257
258			// add comment operation
259			op, err := b.AddCommentRaw(
260				author,
261				note.CreatedAt.Unix(),
262				cleanText,
263				nil,
264				map[string]string{
265					metaKeyGitlabId: gitlabID,
266				},
267			)
268			if err != nil {
269				return err
270			}
271			gi.out <- core.NewImportComment(op.Id())
272			return nil
273		}
274
275		// if comment was already exported
276
277		// search for last comment update
278		comment, err := b.Snapshot().SearchComment(id)
279		if err != nil {
280			return err
281		}
282
283		// compare local bug comment with the new note body
284		if comment.Message != cleanText {
285			// comment edition
286			op, err := b.EditCommentRaw(
287				author,
288				note.UpdatedAt.Unix(),
289				comment.Id(),
290				cleanText,
291				nil,
292			)
293
294			if err != nil {
295				return err
296			}
297			gi.out <- core.NewImportCommentEdition(op.Id())
298		}
299
300		return nil
301
302	case NOTE_TITLE_CHANGED:
303		// title change events are given new notes
304		if errResolve == nil {
305			return nil
306		}
307
308		op, err := b.SetTitleRaw(
309			author,
310			note.CreatedAt.Unix(),
311			body,
312			map[string]string{
313				metaKeyGitlabId: gitlabID,
314			},
315		)
316		if err != nil {
317			return err
318		}
319
320		gi.out <- core.NewImportTitleEdition(op.Id())
321
322	case NOTE_UNKNOWN,
323		NOTE_ASSIGNED,
324		NOTE_UNASSIGNED,
325		NOTE_CHANGED_MILESTONE,
326		NOTE_REMOVED_MILESTONE,
327		NOTE_CHANGED_DUEDATE,
328		NOTE_REMOVED_DUEDATE,
329		NOTE_LOCKED,
330		NOTE_UNLOCKED,
331		NOTE_MENTIONED_IN_ISSUE,
332		NOTE_MENTIONED_IN_MERGE_REQUEST:
333
334		return nil
335
336	default:
337		panic("unhandled note type")
338	}
339
340	return nil
341}
342
343func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
344	_, err := b.ResolveOperationWithMetadata(metaKeyGitlabId, parseID(labelEvent.ID))
345	if err != cache.ErrNoMatchingOp {
346		return err
347	}
348
349	// ensure issue author
350	author, err := gi.ensurePerson(repo, labelEvent.User.ID)
351	if err != nil {
352		return err
353	}
354
355	switch labelEvent.Action {
356	case "add":
357		_, err = b.ForceChangeLabelsRaw(
358			author,
359			labelEvent.CreatedAt.Unix(),
360			[]string{labelEvent.Label.Name},
361			nil,
362			map[string]string{
363				metaKeyGitlabId: parseID(labelEvent.ID),
364			},
365		)
366
367	case "remove":
368		_, err = b.ForceChangeLabelsRaw(
369			author,
370			labelEvent.CreatedAt.Unix(),
371			nil,
372			[]string{labelEvent.Label.Name},
373			map[string]string{
374				metaKeyGitlabId: parseID(labelEvent.ID),
375			},
376		)
377
378	default:
379		err = fmt.Errorf("unexpected label event action")
380	}
381
382	return err
383}
384
385func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
386	// Look first in the cache
387	i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id))
388	if err == nil {
389		return i, nil
390	}
391	if entity.IsErrMultipleMatch(err) {
392		return nil, err
393	}
394
395	user, _, err := gi.client.Users.GetUser(id)
396	if err != nil {
397		return nil, err
398	}
399
400	i, err = repo.NewIdentityRaw(
401		user.Name,
402		user.PublicEmail,
403		user.Username,
404		user.AvatarURL,
405		map[string]string{
406			// because Gitlab
407			metaKeyGitlabId:    strconv.Itoa(id),
408			metaKeyGitlabLogin: user.Username,
409		},
410	)
411	if err != nil {
412		return nil, err
413	}
414
415	gi.out <- core.NewImportIdentity(i.Id())
416	return i, nil
417}
418
419func parseID(id int) string {
420	return fmt.Sprintf("%d", id)
421}