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	// send only channel
 27	out chan<- core.ImportResult
 28}
 29
 30func (gi *gitlabImporter) Init(_ context.Context, repo *cache.RepoCache, conf core.Configuration) error {
 31	gi.conf = conf
 32
 33	creds, err := auth.List(repo,
 34		auth.WithTarget(target),
 35		auth.WithKind(auth.KindToken),
 36		auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyGitlabBaseUrl]),
 37		auth.WithMeta(auth.MetaKeyLogin, conf[confKeyDefaultLogin]),
 38	)
 39	if err != nil {
 40		return err
 41	}
 42
 43	if len(creds) == 0 {
 44		return ErrMissingIdentityToken
 45	}
 46
 47	gi.client, err = buildClient(conf[confKeyGitlabBaseUrl], creds[0].(*auth.Token))
 48	if err != nil {
 49		return err
 50	}
 51
 52	return nil
 53}
 54
 55// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
 56// of the missing issues / comments / label events / title changes ...
 57func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
 58
 59	out := make(chan core.ImportResult)
 60	gi.out = out
 61
 62	go func() {
 63		defer close(out)
 64
 65		for issue := range Issues(ctx, gi.client, gi.conf[confKeyProjectID], since) {
 66
 67			b, err := gi.ensureIssue(repo, issue)
 68			if err != nil {
 69				err := fmt.Errorf("issue creation: %v", err)
 70				out <- core.NewImportError(err, "")
 71				return
 72			}
 73
 74			issueEvents := SortedEvents(
 75				Notes(ctx, gi.client, issue),
 76				LabelEvents(ctx, gi.client, issue),
 77				StateEvents(ctx, gi.client, issue),
 78			)
 79
 80			for e := range issueEvents {
 81				if e, ok := e.(ErrorEvent); ok {
 82					out <- core.NewImportError(e.Err, "")
 83					continue
 84				}
 85				if err := gi.ensureIssueEvent(repo, b, issue, e); err != nil {
 86					err := fmt.Errorf("issue event creation: %v", err)
 87					out <- core.NewImportError(err, entity.Id(e.ID()))
 88				}
 89			}
 90
 91			if !b.NeedCommit() {
 92				out <- core.NewImportNothing(b.Id(), "no imported operation")
 93			} else if err := b.Commit(); err != nil {
 94				// commit bug state
 95				err := fmt.Errorf("bug commit: %v", err)
 96				out <- core.NewImportError(err, "")
 97				return
 98			}
 99		}
100	}()
101
102	return out, nil
103}
104
105func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
106	// ensure issue author
107	author, err := gi.ensurePerson(repo, issue.Author.ID)
108	if err != nil {
109		return nil, err
110	}
111
112	// resolve bug
113	b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
114		return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
115			excerpt.CreateMetadata[metaKeyGitlabId] == fmt.Sprintf("%d", issue.IID) &&
116			excerpt.CreateMetadata[metaKeyGitlabBaseUrl] == gi.conf[confKeyGitlabBaseUrl] &&
117			excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyProjectID]
118	})
119	if err == nil {
120		return b, nil
121	}
122	if err != bug.ErrBugNotExist {
123		return nil, err
124	}
125
126	// if bug was never imported, create bug
127	b, _, err = repo.NewBugRaw(
128		author,
129		issue.CreatedAt.Unix(),
130		text.CleanupOneLine(issue.Title),
131		text.Cleanup(issue.Description),
132		nil,
133		map[string]string{
134			core.MetaKeyOrigin:   target,
135			metaKeyGitlabId:      fmt.Sprintf("%d", issue.IID),
136			metaKeyGitlabUrl:     issue.WebURL,
137			metaKeyGitlabProject: gi.conf[confKeyProjectID],
138			metaKeyGitlabBaseUrl: gi.conf[confKeyGitlabBaseUrl],
139		},
140	)
141
142	if err != nil {
143		return nil, err
144	}
145
146	// importing a new bug
147	gi.out <- core.NewImportBug(b.Id())
148
149	return b, nil
150}
151
152func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCache, issue *gitlab.Issue, event Event) error {
153
154	id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, event.ID())
155	if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
156		return errResolve
157	}
158
159	// ensure issue author
160	author, err := gi.ensurePerson(repo, event.UserID())
161	if err != nil {
162		return err
163	}
164
165	switch event.Kind() {
166	case EventClosed:
167		if errResolve == nil {
168			return nil
169		}
170
171		op, err := b.CloseRaw(
172			author,
173			event.CreatedAt().Unix(),
174			map[string]string{
175				metaKeyGitlabId: event.ID(),
176			},
177		)
178
179		if err != nil {
180			return err
181		}
182
183		gi.out <- core.NewImportStatusChange(op.Id())
184
185	case EventReopened:
186		if errResolve == nil {
187			return nil
188		}
189
190		op, err := b.OpenRaw(
191			author,
192			event.CreatedAt().Unix(),
193			map[string]string{
194				metaKeyGitlabId: event.ID(),
195			},
196		)
197		if err != nil {
198			return err
199		}
200
201		gi.out <- core.NewImportStatusChange(op.Id())
202
203	case EventDescriptionChanged:
204		firstComment := b.Snapshot().Comments[0]
205		// since gitlab doesn't provide the issue history
206		// we should check for "changed the description" notes and compare issue texts
207		// TODO: Check only one time and ignore next 'description change' within one issue
208		if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
209			// comment edition
210			op, err := b.EditCommentRaw(
211				author,
212				event.(NoteEvent).UpdatedAt.Unix(),
213				firstComment.Id(),
214				text.Cleanup(issue.Description),
215				map[string]string{
216					metaKeyGitlabId: event.ID(),
217				},
218			)
219			if err != nil {
220				return err
221			}
222
223			gi.out <- core.NewImportTitleEdition(op.Id())
224		}
225
226	case EventComment:
227		cleanText := text.Cleanup(event.(NoteEvent).Body)
228
229		// if we didn't import the comment
230		if errResolve == cache.ErrNoMatchingOp {
231
232			// add comment operation
233			op, err := b.AddCommentRaw(
234				author,
235				event.CreatedAt().Unix(),
236				cleanText,
237				nil,
238				map[string]string{
239					metaKeyGitlabId: event.ID(),
240				},
241			)
242			if err != nil {
243				return err
244			}
245			gi.out <- core.NewImportComment(op.Id())
246			return nil
247		}
248
249		// if comment was already exported
250
251		// search for last comment update
252		comment, err := b.Snapshot().SearchComment(id)
253		if err != nil {
254			return err
255		}
256
257		// compare local bug comment with the new event body
258		if comment.Message != cleanText {
259			// comment edition
260			op, err := b.EditCommentRaw(
261				author,
262				event.(NoteEvent).UpdatedAt.Unix(),
263				comment.Id(),
264				cleanText,
265				nil,
266			)
267
268			if err != nil {
269				return err
270			}
271			gi.out <- core.NewImportCommentEdition(op.Id())
272		}
273
274		return nil
275
276	case EventTitleChanged:
277		// title change events are given new notes
278		if errResolve == nil {
279			return nil
280		}
281
282		op, err := b.SetTitleRaw(
283			author,
284			event.CreatedAt().Unix(),
285			event.(NoteEvent).Title(),
286			map[string]string{
287				metaKeyGitlabId: event.ID(),
288			},
289		)
290		if err != nil {
291			return err
292		}
293
294		gi.out <- core.NewImportTitleEdition(op.Id())
295
296	case EventAddLabel:
297		_, err = b.ForceChangeLabelsRaw(
298			author,
299			event.CreatedAt().Unix(),
300			[]string{event.(LabelEvent).Label.Name},
301			nil,
302			map[string]string{
303				metaKeyGitlabId: event.ID(),
304			},
305		)
306		return err
307
308	case EventRemoveLabel:
309		_, err = b.ForceChangeLabelsRaw(
310			author,
311			event.CreatedAt().Unix(),
312			nil,
313			[]string{event.(LabelEvent).Label.Name},
314			map[string]string{
315				metaKeyGitlabId: event.ID(),
316			},
317		)
318		return err
319
320	case EventAssigned,
321		EventUnassigned,
322		EventChangedMilestone,
323		EventRemovedMilestone,
324		EventChangedDuedate,
325		EventRemovedDuedate,
326		EventLocked,
327		EventUnlocked,
328		EventMentionedInIssue,
329		EventMentionedInMergeRequest:
330
331		return nil
332
333	default:
334		return fmt.Errorf("unexpected event")
335	}
336
337	return nil
338}
339
340func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
341	// Look first in the cache
342	i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id))
343	if err == nil {
344		return i, nil
345	}
346	if entity.IsErrMultipleMatch(err) {
347		return nil, err
348	}
349
350	user, _, err := gi.client.Users.GetUser(id)
351	if err != nil {
352		return nil, err
353	}
354
355	i, err = repo.NewIdentityRaw(
356		user.Name,
357		user.PublicEmail,
358		user.Username,
359		user.AvatarURL,
360		nil,
361		map[string]string{
362			// because Gitlab
363			metaKeyGitlabId:    strconv.Itoa(id),
364			metaKeyGitlabLogin: user.Username,
365		},
366	)
367	if err != nil {
368		return nil, err
369	}
370
371	gi.out <- core.NewImportIdentity(i.Id())
372	return i, nil
373}