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