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/git"
 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	// 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
 37// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
 38// of the missing issues / comments / label events / title changes ...
 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		if err := gi.iterator.Error(); err != nil {
 70			fmt.Printf("import error: %v\n", err)
 71			return err
 72		}
 73
 74		// commit bug state
 75		if err := b.CommitAsNeeded(); err != nil {
 76			return fmt.Errorf("bug commit: %v", err)
 77		}
 78	}
 79
 80	fmt.Printf("Successfully imported %d issues and %d identities from Gitlab\n", gi.importedIssues, gi.importedIdentities)
 81	return nil
 82}
 83
 84func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
 85	// ensure issue author
 86	author, err := gi.ensurePerson(repo, issue.Author.ID)
 87	if err != nil {
 88		return nil, err
 89	}
 90
 91	// resolve bug
 92	b, err := repo.ResolveBugCreateMetadata(keyGitlabUrl, issue.WebURL)
 93	if err != nil && err != bug.ErrBugNotExist {
 94		return nil, err
 95	}
 96
 97	if err == nil {
 98		return b, nil
 99	}
100
101	// if bug was never imported
102	cleanText, err := text.Cleanup(issue.Description)
103	if err != nil {
104		return nil, err
105	}
106
107	// create bug
108	b, _, err = repo.NewBugRaw(
109		author,
110		issue.CreatedAt.Unix(),
111		issue.Title,
112		cleanText,
113		nil,
114		map[string]string{
115			core.KeyOrigin:   target,
116			keyGitlabId:      parseID(issue.ID),
117			keyGitlabUrl:     issue.WebURL,
118			keyGitlabProject: gi.conf[keyProjectID],
119		},
120	)
121
122	if err != nil {
123		return nil, err
124	}
125
126	// importing a new bug
127	gi.importedIssues++
128
129	return b, nil
130}
131
132func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
133	id := parseID(note.ID)
134
135	hash, errResolve := b.ResolveOperationWithMetadata(keyGitlabId, id)
136	if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
137		return errResolve
138	}
139
140	// ensure issue author
141	author, err := gi.ensurePerson(repo, note.Author.ID)
142	if err != nil {
143		return err
144	}
145
146	noteType, body := GetNoteType(note)
147	switch noteType {
148	case NOTE_CLOSED:
149		if errResolve == nil {
150			return nil
151		}
152
153		_, err = b.CloseRaw(
154			author,
155			note.CreatedAt.Unix(),
156			map[string]string{
157				keyGitlabId: id,
158			},
159		)
160		return err
161
162	case NOTE_REOPENED:
163		if errResolve == nil {
164			return nil
165		}
166
167		_, err = b.OpenRaw(
168			author,
169			note.CreatedAt.Unix(),
170			map[string]string{
171				keyGitlabId: id,
172			},
173		)
174		return err
175
176	case NOTE_DESCRIPTION_CHANGED:
177		issue := gi.iterator.IssueValue()
178
179		firstComment := b.Snapshot().Comments[0]
180		// since gitlab doesn't provide the issue history
181		// we should check for "changed the description" notes and compare issue texts
182		// TODO: Check only one time and ignore next 'description change' within one issue
183		if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
184			// comment edition
185			_, err = b.EditCommentRaw(
186				author,
187				note.UpdatedAt.Unix(),
188				git.Hash(firstComment.Id()),
189				issue.Description,
190				map[string]string{
191					keyGitlabId: id,
192				},
193			)
194
195			return err
196		}
197
198	case NOTE_COMMENT:
199		cleanText, err := text.Cleanup(body)
200		if err != nil {
201			return err
202		}
203
204		// if we didn't import the comment
205		if errResolve == cache.ErrNoMatchingOp {
206
207			// add comment operation
208			_, err = b.AddCommentRaw(
209				author,
210				note.CreatedAt.Unix(),
211				cleanText,
212				nil,
213				map[string]string{
214					keyGitlabId: id,
215				},
216			)
217
218			return err
219		}
220
221		// if comment was already exported
222
223		// search for last comment update
224		comment, err := b.Snapshot().SearchComment(hash)
225		if err != nil {
226			return err
227		}
228
229		// compare local bug comment with the new note body
230		if comment.Message != cleanText {
231			// comment edition
232			_, err = b.EditCommentRaw(
233				author,
234				note.UpdatedAt.Unix(),
235				git.Hash(comment.Id()),
236				cleanText,
237				nil,
238			)
239
240			return err
241		}
242
243		return nil
244
245	case NOTE_TITLE_CHANGED:
246		// title change events are given new notes
247		if errResolve == nil {
248			return nil
249		}
250
251		_, err = b.SetTitleRaw(
252			author,
253			note.CreatedAt.Unix(),
254			body,
255			map[string]string{
256				keyGitlabId: id,
257			},
258		)
259
260		return err
261
262	case NOTE_UNKNOWN,
263		NOTE_ASSIGNED,
264		NOTE_UNASSIGNED,
265		NOTE_CHANGED_MILESTONE,
266		NOTE_REMOVED_MILESTONE,
267		NOTE_CHANGED_DUEDATE,
268		NOTE_REMOVED_DUEDATE,
269		NOTE_LOCKED,
270		NOTE_UNLOCKED:
271		return nil
272
273	default:
274		panic("unhandled note type")
275	}
276
277	return nil
278}
279
280func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
281	_, err := b.ResolveOperationWithMetadata(keyGitlabId, parseID(labelEvent.ID))
282	if err != cache.ErrNoMatchingOp {
283		return err
284	}
285
286	// ensure issue author
287	author, err := gi.ensurePerson(repo, labelEvent.User.ID)
288	if err != nil {
289		return err
290	}
291
292	switch labelEvent.Action {
293	case "add":
294		_, err = b.ForceChangeLabelsRaw(
295			author,
296			labelEvent.CreatedAt.Unix(),
297			[]string{labelEvent.Label.Name},
298			nil,
299			map[string]string{
300				keyGitlabId: parseID(labelEvent.ID),
301			},
302		)
303
304	case "remove":
305		_, err = b.ForceChangeLabelsRaw(
306			author,
307			labelEvent.CreatedAt.Unix(),
308			nil,
309			[]string{labelEvent.Label.Name},
310			map[string]string{
311				keyGitlabId: parseID(labelEvent.ID),
312			},
313		)
314
315	default:
316		err = fmt.Errorf("unexpected label event action")
317	}
318
319	return err
320}
321
322func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
323	// Look first in the cache
324	i, err := repo.ResolveIdentityImmutableMetadata(keyGitlabId, strconv.Itoa(id))
325	if err == nil {
326		return i, nil
327	}
328	if _, ok := err.(identity.ErrMultipleMatch); ok {
329		return nil, err
330	}
331
332	client := buildClient(gi.conf["token"])
333
334	user, _, err := client.Users.GetUser(id)
335	if err != nil {
336		return nil, err
337	}
338
339	// importing a new identity
340	gi.importedIdentities++
341
342	return repo.NewIdentityRaw(
343		user.Name,
344		user.PublicEmail,
345		user.Username,
346		user.AvatarURL,
347		map[string]string{
348			// because Gitlab
349			keyGitlabId:    strconv.Itoa(id),
350			keyGitlabLogin: user.Username,
351		},
352	)
353}
354
355func parseID(id int) string {
356	return fmt.Sprintf("%d", id)
357}