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