import.go

  1package gitlab
  2
  3import (
  4	"fmt"
  5	"strconv"
  6	"time"
  7
  8	"github.com/xanzy/go-gitlab"
  9
 10	"github.com/MichaelMure/git-bug/bridge/core"
 11	"github.com/MichaelMure/git-bug/bug"
 12	"github.com/MichaelMure/git-bug/cache"
 13	"github.com/MichaelMure/git-bug/identity"
 14	"github.com/MichaelMure/git-bug/util/text"
 15)
 16
 17const (
 18	keyGitlabLogin = "gitlab-login"
 19)
 20
 21type gitlabImporter struct {
 22	conf core.Configuration
 23
 24	// iterator
 25	iterator *iterator
 26
 27	// number of imported issues
 28	importedIssues int
 29
 30	// number of imported identities
 31	importedIdentities int
 32}
 33
 34func (gi *gitlabImporter) Init(conf core.Configuration) error {
 35	gi.conf = conf
 36	return nil
 37}
 38
 39func (gi *gitlabImporter) ImportAll(repo *cache.RepoCache, since time.Time) error {
 40	gi.iterator = NewIterator(gi.conf[keyProjectID], gi.conf[keyToken], since)
 41
 42	// Loop over all matching issues
 43	for gi.iterator.NextIssue() {
 44		issue := gi.iterator.IssueValue()
 45		fmt.Printf("importing issue: %v\n", issue.Title)
 46
 47		// create issue
 48		b, err := gi.ensureIssue(repo, issue)
 49		if err != nil {
 50			return fmt.Errorf("issue creation: %v", err)
 51		}
 52
 53		// Loop over all notes
 54		for gi.iterator.NextNote() {
 55			note := gi.iterator.NoteValue()
 56			if err := gi.ensureNote(repo, b, note); err != nil {
 57				return fmt.Errorf("note creation: %v", err)
 58			}
 59		}
 60
 61		// Loop over all label events
 62		for gi.iterator.NextLabelEvent() {
 63			labelEvent := gi.iterator.LabelEventValue()
 64			if err := gi.ensureLabelEvent(repo, b, labelEvent); err != nil {
 65				return fmt.Errorf("label event creation: %v", err)
 66			}
 67
 68		}
 69
 70		// commit bug state
 71		if err := b.CommitAsNeeded(); err != nil {
 72			return fmt.Errorf("bug commit: %v", err)
 73		}
 74	}
 75
 76	if err := gi.iterator.Error(); err != nil {
 77		fmt.Printf("import error: %v\n", err)
 78		return err
 79	}
 80
 81	fmt.Printf("Successfully imported %d issues and %d identities from Gitlab\n", gi.importedIssues, gi.importedIdentities)
 82	return nil
 83}
 84
 85func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
 86	// ensure issue author
 87	author, err := gi.ensurePerson(repo, issue.Author.ID)
 88	if err != nil {
 89		return nil, err
 90	}
 91
 92	// resolve bug
 93	b, err := repo.ResolveBugCreateMetadata(keyGitlabUrl, issue.WebURL)
 94	if err != nil && err != bug.ErrBugNotExist {
 95		return nil, err
 96	}
 97
 98	if err == bug.ErrBugNotExist {
 99		cleanText, err := text.Cleanup(string(issue.Description))
100		if err != nil {
101			return nil, err
102		}
103
104		// create bug
105		b, _, err = repo.NewBugRaw(
106			author,
107			issue.CreatedAt.Unix(),
108			issue.Title,
109			cleanText,
110			nil,
111			map[string]string{
112				keyOrigin:    target,
113				keyGitlabId:  parseID(issue.ID),
114				keyGitlabUrl: issue.WebURL,
115			},
116		)
117
118		if err != nil {
119			return nil, err
120		}
121
122		// importing a new bug
123		gi.importedIssues++
124
125		return b, nil
126	}
127
128	return nil, nil
129}
130
131func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
132	id := parseID(note.ID)
133
134	hash, err := b.ResolveOperationWithMetadata(keyGitlabId, id)
135	if err != cache.ErrNoMatchingOp {
136		return err
137	}
138
139	// ensure issue author
140	author, err := gi.ensurePerson(repo, note.Author.ID)
141	if err != nil {
142		return err
143	}
144
145	noteType, body := GetNoteType(note)
146	switch noteType {
147	case NOTE_CLOSED:
148		_, err = b.CloseRaw(
149			author,
150			note.CreatedAt.Unix(),
151			map[string]string{
152				keyGitlabId: id,
153			},
154		)
155		return err
156
157	case NOTE_REOPENED:
158		_, err = b.OpenRaw(
159			author,
160			note.CreatedAt.Unix(),
161			map[string]string{
162				keyGitlabId: id,
163			},
164		)
165		return err
166
167	case NOTE_DESCRIPTION_CHANGED:
168		issue := gi.iterator.IssueValue()
169
170		// since gitlab doesn't provide the issue history
171		// we should check for "changed the description" notes and compare issue texts
172
173		if issue.Description != b.Snapshot().Comments[0].Message {
174			// comment edition
175			_, err = b.EditCommentRaw(
176				author,
177				note.UpdatedAt.Unix(),
178				target,
179				issue.Description,
180				map[string]string{
181					keyGitlabId:  id,
182					keyGitlabUrl: "",
183				},
184			)
185
186			return err
187
188		}
189
190	case NOTE_COMMENT:
191
192		cleanText, err := text.Cleanup(body)
193		if err != nil {
194			return err
195		}
196
197		// if we didn't import the comment
198		if err == cache.ErrNoMatchingOp {
199
200			// add comment operation
201			_, err = b.AddCommentRaw(
202				author,
203				note.CreatedAt.Unix(),
204				cleanText,
205				nil,
206				map[string]string{
207					keyGitlabId:  id,
208					keyGitlabUrl: "",
209				},
210			)
211
212			return err
213		}
214
215		// if comment was already exported
216
217		// if note wasn't updated
218		if note.UpdatedAt.Equal(*note.CreatedAt) {
219			return nil
220		}
221
222		// search for last comment update
223		timeline, err := b.Snapshot().SearchTimelineItem(hash)
224		if err != nil {
225			return err
226		}
227
228		item, ok := timeline.(*bug.AddCommentTimelineItem)
229		if !ok {
230			return fmt.Errorf("expected add comment time line")
231		}
232
233		// compare local bug comment with the new note body
234		if item.Message != cleanText {
235			// comment edition
236			_, err = b.EditCommentRaw(
237				author,
238				note.UpdatedAt.Unix(),
239				target,
240				cleanText,
241				map[string]string{
242					// no metadata unique metadata to store
243					keyGitlabId:  "",
244					keyGitlabUrl: "",
245				},
246			)
247
248			return err
249		}
250
251		return nil
252
253	case NOTE_TITLE_CHANGED:
254
255		_, err = b.SetTitleRaw(
256			author,
257			note.CreatedAt.Unix(),
258			body,
259			map[string]string{
260				keyGitlabId:  id,
261				keyGitlabUrl: "",
262			},
263		)
264
265		return err
266
267	default:
268		// non handled note types
269
270		return nil
271	}
272
273	return nil
274}
275
276func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
277	_, err := b.ResolveOperationWithMetadata(keyGitlabId, parseID(labelEvent.ID))
278	if err != cache.ErrNoMatchingOp {
279		return err
280	}
281
282	// ensure issue author
283	author, err := gi.ensurePerson(repo, labelEvent.User.ID)
284	if err != nil {
285		return err
286	}
287
288	switch labelEvent.Action {
289	case "add":
290		_, err = b.ForceChangeLabelsRaw(
291			author,
292			labelEvent.CreatedAt.Unix(),
293			[]string{labelEvent.Label.Name},
294			nil,
295			map[string]string{
296				keyGitlabId: parseID(labelEvent.ID),
297			},
298		)
299
300	case "remove":
301		_, err = b.ForceChangeLabelsRaw(
302			author,
303			labelEvent.CreatedAt.Unix(),
304			nil,
305			[]string{labelEvent.Label.Name},
306			map[string]string{
307				keyGitlabId: parseID(labelEvent.ID),
308			},
309		)
310
311	default:
312		panic("unexpected label event action")
313	}
314
315	return err
316}
317
318func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
319	// Look first in the cache
320	i, err := repo.ResolveIdentityImmutableMetadata(keyGitlabId, strconv.Itoa(id))
321	if err == nil {
322		return i, nil
323	}
324	if _, ok := err.(identity.ErrMultipleMatch); ok {
325		return nil, err
326	}
327
328	// importing a new identity
329	gi.importedIdentities++
330
331	client := buildClient(gi.conf["token"])
332
333	user, _, err := client.Users.GetUser(id)
334	if err != nil {
335		return nil, err
336	}
337
338	return repo.NewIdentityRaw(
339		user.Name,
340		user.PublicEmail,
341		user.Username,
342		user.AvatarURL,
343		map[string]string{
344			keyGitlabId:    strconv.Itoa(id),
345			keyGitlabLogin: user.Username,
346		},
347	)
348}
349
350func parseID(id int) string {
351	return fmt.Sprintf("%d", id)
352}