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