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