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			for _, e := range SortedEvents(IssueEvents(ctx, gi.client, issue)) {
 75				if e, ok := e.(ErrorEvent); ok {
 76					out <- core.NewImportError(e.Err, "")
 77					continue
 78				}
 79				if err := gi.ensureIssueEvent(repo, b, issue, e); err != nil {
 80					err := fmt.Errorf("issue event creation: %v", err)
 81					out <- core.NewImportError(err, entity.Id(e.ID()))
 82				}
 83			}
 84
 85			if !b.NeedCommit() {
 86				out <- core.NewImportNothing(b.Id(), "no imported operation")
 87			} else if err := b.Commit(); err != nil {
 88				// commit bug state
 89				err := fmt.Errorf("bug commit: %v", err)
 90				out <- core.NewImportError(err, "")
 91				return
 92			}
 93		}
 94	}()
 95
 96	return out, nil
 97}
 98
 99func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
100	// ensure issue author
101	author, err := gi.ensurePerson(repo, issue.Author.ID)
102	if err != nil {
103		return nil, err
104	}
105
106	// resolve bug
107	b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
108		return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
109			excerpt.CreateMetadata[metaKeyGitlabId] == fmt.Sprintf("%d", issue.IID) &&
110			excerpt.CreateMetadata[metaKeyGitlabBaseUrl] == gi.conf[confKeyGitlabBaseUrl] &&
111			excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyProjectID]
112	})
113	if err == nil {
114		return b, nil
115	}
116	if err != bug.ErrBugNotExist {
117		return nil, err
118	}
119
120	// if bug was never imported, create bug
121	b, _, err = repo.NewBugRaw(
122		author,
123		issue.CreatedAt.Unix(),
124		text.CleanupOneLine(issue.Title),
125		text.Cleanup(issue.Description),
126		nil,
127		map[string]string{
128			core.MetaKeyOrigin:   target,
129			metaKeyGitlabId:      fmt.Sprintf("%d", issue.IID),
130			metaKeyGitlabUrl:     issue.WebURL,
131			metaKeyGitlabProject: gi.conf[confKeyProjectID],
132			metaKeyGitlabBaseUrl: gi.conf[confKeyGitlabBaseUrl],
133		},
134	)
135
136	if err != nil {
137		return nil, err
138	}
139
140	// importing a new bug
141	gi.out <- core.NewImportBug(b.Id())
142
143	return b, nil
144}
145
146func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCache, issue *gitlab.Issue, event Event) error {
147
148	id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, event.ID())
149	if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
150		return errResolve
151	}
152
153	// ensure issue author
154	author, err := gi.ensurePerson(repo, event.UserID())
155	if err != nil {
156		return err
157	}
158
159	switch event.Kind() {
160	case EventClosed:
161		if errResolve == nil {
162			return nil
163		}
164
165		op, err := b.CloseRaw(
166			author,
167			event.CreatedAt().Unix(),
168			map[string]string{
169				metaKeyGitlabId: event.ID(),
170			},
171		)
172
173		if err != nil {
174			return err
175		}
176
177		gi.out <- core.NewImportStatusChange(op.Id())
178
179	case EventReopened:
180		if errResolve == nil {
181			return nil
182		}
183
184		op, err := b.OpenRaw(
185			author,
186			event.CreatedAt().Unix(),
187			map[string]string{
188				metaKeyGitlabId: event.ID(),
189			},
190		)
191		if err != nil {
192			return err
193		}
194
195		gi.out <- core.NewImportStatusChange(op.Id())
196
197	case EventDescriptionChanged:
198		firstComment := b.Snapshot().Comments[0]
199		// since gitlab doesn't provide the issue history
200		// we should check for "changed the description" notes and compare issue texts
201		// TODO: Check only one time and ignore next 'description change' within one issue
202		if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
203			// comment edition
204			op, err := b.EditCommentRaw(
205				author,
206				event.(NoteEvent).UpdatedAt.Unix(),
207				firstComment.Id(),
208				text.Cleanup(issue.Description),
209				map[string]string{
210					metaKeyGitlabId: event.ID(),
211				},
212			)
213			if err != nil {
214				return err
215			}
216
217			gi.out <- core.NewImportTitleEdition(op.Id())
218		}
219
220	case EventComment:
221		cleanText := text.Cleanup(event.(NoteEvent).Body)
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				event.CreatedAt().Unix(),
230				cleanText,
231				nil,
232				map[string]string{
233					metaKeyGitlabId: event.ID(),
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 event body
252		if comment.Message != cleanText {
253			// comment edition
254			op, err := b.EditCommentRaw(
255				author,
256				event.(NoteEvent).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 EventTitleChanged:
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			event.CreatedAt().Unix(),
279			event.(NoteEvent).Title(),
280			map[string]string{
281				metaKeyGitlabId: event.ID(),
282			},
283		)
284		if err != nil {
285			return err
286		}
287
288		gi.out <- core.NewImportTitleEdition(op.Id())
289
290	case EventAddLabel:
291		_, err = b.ForceChangeLabelsRaw(
292			author,
293			event.CreatedAt().Unix(),
294			[]string{event.(LabelEvent).Label.Name},
295			nil,
296			map[string]string{
297				metaKeyGitlabId: event.ID(),
298			},
299		)
300		return err
301
302	case EventRemoveLabel:
303		_, err = b.ForceChangeLabelsRaw(
304			author,
305			event.CreatedAt().Unix(),
306			nil,
307			[]string{event.(LabelEvent).Label.Name},
308			map[string]string{
309				metaKeyGitlabId: event.ID(),
310			},
311		)
312		return err
313
314	case EventAssigned,
315		EventUnassigned,
316		EventChangedMilestone,
317		EventRemovedMilestone,
318		EventChangedDuedate,
319		EventRemovedDuedate,
320		EventLocked,
321		EventUnlocked,
322		EventMentionedInIssue,
323		EventMentionedInMergeRequest:
324
325		return nil
326
327	default:
328		return fmt.Errorf("unexpected event")
329	}
330
331	return nil
332}
333
334func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
335	// Look first in the cache
336	i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id))
337	if err == nil {
338		return i, nil
339	}
340	if entity.IsErrMultipleMatch(err) {
341		return nil, err
342	}
343
344	user, _, err := gi.client.Users.GetUser(id)
345	if err != nil {
346		return nil, err
347	}
348
349	i, err = repo.NewIdentityRaw(
350		user.Name,
351		user.PublicEmail,
352		user.Username,
353		user.AvatarURL,
354		nil,
355		map[string]string{
356			// because Gitlab
357			metaKeyGitlabId:    strconv.Itoa(id),
358			metaKeyGitlabLogin: user.Username,
359		},
360	)
361	if err != nil {
362		return nil, err
363	}
364
365	gi.out <- core.NewImportIdentity(i.Id())
366	return i, nil
367}