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[confKeyGitlabBaseUrl] &&
131			excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyProjectID]
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, create bug
141	b, _, err = repo.NewBugRaw(
142		author,
143		issue.CreatedAt.Unix(),
144		text.CleanupOneLine(issue.Title),
145		text.Cleanup(issue.Description),
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				text.Cleanup(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 := text.Cleanup(body)
245
246		// if we didn't import the comment
247		if errResolve == cache.ErrNoMatchingOp {
248
249			// add comment operation
250			op, err := b.AddCommentRaw(
251				author,
252				note.CreatedAt.Unix(),
253				cleanText,
254				nil,
255				map[string]string{
256					metaKeyGitlabId: gitlabID,
257				},
258			)
259			if err != nil {
260				return err
261			}
262			gi.out <- core.NewImportComment(op.Id())
263			return nil
264		}
265
266		// if comment was already exported
267
268		// search for last comment update
269		comment, err := b.Snapshot().SearchComment(id)
270		if err != nil {
271			return err
272		}
273
274		// compare local bug comment with the new note body
275		if comment.Message != cleanText {
276			// comment edition
277			op, err := b.EditCommentRaw(
278				author,
279				note.UpdatedAt.Unix(),
280				comment.Id(),
281				cleanText,
282				nil,
283			)
284
285			if err != nil {
286				return err
287			}
288			gi.out <- core.NewImportCommentEdition(op.Id())
289		}
290
291		return nil
292
293	case NOTE_TITLE_CHANGED:
294		// title change events are given new notes
295		if errResolve == nil {
296			return nil
297		}
298
299		op, err := b.SetTitleRaw(
300			author,
301			note.CreatedAt.Unix(),
302			text.CleanupOneLine(body),
303			map[string]string{
304				metaKeyGitlabId: gitlabID,
305			},
306		)
307		if err != nil {
308			return err
309		}
310
311		gi.out <- core.NewImportTitleEdition(op.Id())
312
313	case NOTE_UNKNOWN,
314		NOTE_ASSIGNED,
315		NOTE_UNASSIGNED,
316		NOTE_CHANGED_MILESTONE,
317		NOTE_REMOVED_MILESTONE,
318		NOTE_CHANGED_DUEDATE,
319		NOTE_REMOVED_DUEDATE,
320		NOTE_LOCKED,
321		NOTE_UNLOCKED,
322		NOTE_MENTIONED_IN_ISSUE,
323		NOTE_MENTIONED_IN_MERGE_REQUEST:
324
325		return nil
326
327	default:
328		panic("unhandled note type")
329	}
330
331	return nil
332}
333
334func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
335	_, err := b.ResolveOperationWithMetadata(metaKeyGitlabId, parseID(labelEvent.ID))
336	if err != cache.ErrNoMatchingOp {
337		return err
338	}
339
340	// ensure issue author
341	author, err := gi.ensurePerson(repo, labelEvent.User.ID)
342	if err != nil {
343		return err
344	}
345
346	switch labelEvent.Action {
347	case "add":
348		_, err = b.ForceChangeLabelsRaw(
349			author,
350			labelEvent.CreatedAt.Unix(),
351			[]string{text.CleanupOneLine(labelEvent.Label.Name)},
352			nil,
353			map[string]string{
354				metaKeyGitlabId: parseID(labelEvent.ID),
355			},
356		)
357
358	case "remove":
359		_, err = b.ForceChangeLabelsRaw(
360			author,
361			labelEvent.CreatedAt.Unix(),
362			nil,
363			[]string{text.CleanupOneLine(labelEvent.Label.Name)},
364			map[string]string{
365				metaKeyGitlabId: parseID(labelEvent.ID),
366			},
367		)
368
369	default:
370		err = fmt.Errorf("unexpected label event action")
371	}
372
373	return err
374}
375
376func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
377	// Look first in the cache
378	i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id))
379	if err == nil {
380		return i, nil
381	}
382	if entity.IsErrMultipleMatch(err) {
383		return nil, err
384	}
385
386	user, _, err := gi.client.Users.GetUser(id, gitlab.GetUsersOptions{})
387	if err != nil {
388		return nil, err
389	}
390
391	i, err = repo.NewIdentityRaw(
392		user.Name,
393		user.PublicEmail,
394		user.Username,
395		user.AvatarURL,
396		nil,
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}