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/entity"
 14	"github.com/MichaelMure/git-bug/util/text"
 15)
 16
 17// gitlabImporter implement the Importer interface
 18type gitlabImporter struct {
 19	conf core.Configuration
 20
 21	// iterator
 22	iterator *iterator
 23
 24	// number of imported issues
 25	importedIssues int
 26
 27	// number of imported identities
 28	importedIdentities int
 29}
 30
 31func (gi *gitlabImporter) Init(conf core.Configuration) error {
 32	gi.conf = conf
 33	return nil
 34}
 35
 36// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
 37// of the missing issues / comments / label events / title changes ...
 38func (gi *gitlabImporter) ImportAll(repo *cache.RepoCache, since time.Time) error {
 39	gi.iterator = NewIterator(gi.conf[keyProjectID], gi.conf[keyToken], since)
 40
 41	// Loop over all matching issues
 42	for gi.iterator.NextIssue() {
 43		issue := gi.iterator.IssueValue()
 44		fmt.Printf("importing issue: %v\n", issue.Title)
 45
 46		// create issue
 47		b, err := gi.ensureIssue(repo, issue)
 48		if err != nil {
 49			return fmt.Errorf("issue creation: %v", err)
 50		}
 51
 52		// Loop over all notes
 53		for gi.iterator.NextNote() {
 54			note := gi.iterator.NoteValue()
 55			if err := gi.ensureNote(repo, b, note); err != nil {
 56				return fmt.Errorf("note creation: %v", err)
 57			}
 58		}
 59
 60		// Loop over all label events
 61		for gi.iterator.NextLabelEvent() {
 62			labelEvent := gi.iterator.LabelEventValue()
 63			if err := gi.ensureLabelEvent(repo, b, labelEvent); err != nil {
 64				return fmt.Errorf("label event creation: %v", err)
 65			}
 66		}
 67
 68		if err := gi.iterator.Error(); err != nil {
 69			fmt.Printf("import error: %v\n", err)
 70			return err
 71		}
 72
 73		// commit bug state
 74		if err := b.CommitAsNeeded(); err != nil {
 75			return fmt.Errorf("bug commit: %v", err)
 76		}
 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 == nil {
 97		return b, nil
 98	}
 99
100	// if bug was never imported
101	cleanText, err := text.Cleanup(issue.Description)
102	if err != nil {
103		return nil, err
104	}
105
106	// create bug
107	b, _, err = repo.NewBugRaw(
108		author,
109		issue.CreatedAt.Unix(),
110		issue.Title,
111		cleanText,
112		nil,
113		map[string]string{
114			core.KeyOrigin:   target,
115			keyGitlabId:      parseID(issue.ID),
116			keyGitlabUrl:     issue.WebURL,
117			keyGitlabProject: gi.conf[keyProjectID],
118		},
119	)
120
121	if err != nil {
122		return nil, err
123	}
124
125	// importing a new bug
126	gi.importedIssues++
127
128	return b, nil
129}
130
131func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
132	gitlabID := parseID(note.ID)
133
134	id, errResolve := b.ResolveOperationWithMetadata(keyGitlabId, gitlabID)
135	if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
136		return errResolve
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		if errResolve == nil {
149			return nil
150		}
151
152		_, err = b.CloseRaw(
153			author,
154			note.CreatedAt.Unix(),
155			map[string]string{
156				keyGitlabId: gitlabID,
157			},
158		)
159		return err
160
161	case NOTE_REOPENED:
162		if errResolve == nil {
163			return nil
164		}
165
166		_, err = b.OpenRaw(
167			author,
168			note.CreatedAt.Unix(),
169			map[string]string{
170				keyGitlabId: gitlabID,
171			},
172		)
173		return err
174
175	case NOTE_DESCRIPTION_CHANGED:
176		issue := gi.iterator.IssueValue()
177
178		firstComment := b.Snapshot().Comments[0]
179		// since gitlab doesn't provide the issue history
180		// we should check for "changed the description" notes and compare issue texts
181		// TODO: Check only one time and ignore next 'description change' within one issue
182		if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
183			// comment edition
184			_, err = b.EditCommentRaw(
185				author,
186				note.UpdatedAt.Unix(),
187				firstComment.Id(),
188				issue.Description,
189				map[string]string{
190					keyGitlabId: gitlabID,
191				},
192			)
193
194			return err
195		}
196
197	case NOTE_COMMENT:
198		cleanText, err := text.Cleanup(body)
199		if err != nil {
200			return err
201		}
202
203		// if we didn't import the comment
204		if errResolve == cache.ErrNoMatchingOp {
205
206			// add comment operation
207			_, err = b.AddCommentRaw(
208				author,
209				note.CreatedAt.Unix(),
210				cleanText,
211				nil,
212				map[string]string{
213					keyGitlabId: gitlabID,
214				},
215			)
216
217			return err
218		}
219
220		// if comment was already exported
221
222		// search for last comment update
223		comment, err := b.Snapshot().SearchComment(id)
224		if err != nil {
225			return err
226		}
227
228		// compare local bug comment with the new note body
229		if comment.Message != cleanText {
230			// comment edition
231			_, err = b.EditCommentRaw(
232				author,
233				note.UpdatedAt.Unix(),
234				comment.Id(),
235				cleanText,
236				nil,
237			)
238
239			return err
240		}
241
242		return nil
243
244	case NOTE_TITLE_CHANGED:
245		// title change events are given new notes
246		if errResolve == nil {
247			return nil
248		}
249
250		_, err = b.SetTitleRaw(
251			author,
252			note.CreatedAt.Unix(),
253			body,
254			map[string]string{
255				keyGitlabId: gitlabID,
256			},
257		)
258
259		return err
260
261	case NOTE_UNKNOWN,
262		NOTE_ASSIGNED,
263		NOTE_UNASSIGNED,
264		NOTE_CHANGED_MILESTONE,
265		NOTE_REMOVED_MILESTONE,
266		NOTE_CHANGED_DUEDATE,
267		NOTE_REMOVED_DUEDATE,
268		NOTE_LOCKED,
269		NOTE_UNLOCKED,
270		NOTE_MENTIONED_IN_ISSUE,
271		NOTE_MENTIONED_IN_MERGE_REQUEST:
272		return nil
273
274	default:
275		panic("unhandled note type")
276	}
277
278	return nil
279}
280
281func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
282	_, err := b.ResolveOperationWithMetadata(keyGitlabId, parseID(labelEvent.ID))
283	if err != cache.ErrNoMatchingOp {
284		return err
285	}
286
287	// ensure issue author
288	author, err := gi.ensurePerson(repo, labelEvent.User.ID)
289	if err != nil {
290		return err
291	}
292
293	switch labelEvent.Action {
294	case "add":
295		_, err = b.ForceChangeLabelsRaw(
296			author,
297			labelEvent.CreatedAt.Unix(),
298			[]string{labelEvent.Label.Name},
299			nil,
300			map[string]string{
301				keyGitlabId: parseID(labelEvent.ID),
302			},
303		)
304
305	case "remove":
306		_, err = b.ForceChangeLabelsRaw(
307			author,
308			labelEvent.CreatedAt.Unix(),
309			nil,
310			[]string{labelEvent.Label.Name},
311			map[string]string{
312				keyGitlabId: parseID(labelEvent.ID),
313			},
314		)
315
316	default:
317		err = fmt.Errorf("unexpected label event action")
318	}
319
320	return err
321}
322
323func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
324	// Look first in the cache
325	i, err := repo.ResolveIdentityImmutableMetadata(keyGitlabId, strconv.Itoa(id))
326	if err == nil {
327		return i, nil
328	}
329	if _, ok := err.(entity.ErrMultipleMatch); ok {
330		return nil, err
331	}
332
333	client := buildClient(gi.conf["token"])
334
335	user, _, err := client.Users.GetUser(id)
336	if err != nil {
337		return nil, err
338	}
339
340	// importing a new identity
341	gi.importedIdentities++
342
343	return repo.NewIdentityRaw(
344		user.Name,
345		user.PublicEmail,
346		user.Username,
347		user.AvatarURL,
348		map[string]string{
349			// because Gitlab
350			keyGitlabId:    strconv.Itoa(id),
351			keyGitlabLogin: user.Username,
352		},
353	)
354}
355
356func parseID(id int) string {
357	return fmt.Sprintf("%d", id)
358}