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