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