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