import.go

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