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