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
 17type gitlabImporter struct {
 18	conf core.Configuration
 19
 20	// iterator
 21	iterator *iterator
 22
 23	// number of imported issues
 24	importedIssues int
 25
 26	// number of imported identities
 27	importedIdentities int
 28}
 29
 30func (gi *gitlabImporter) Init(conf core.Configuration) error {
 31	gi.conf = conf
 32	return nil
 33}
 34
 35func (gi *gitlabImporter) ImportAll(repo *cache.RepoCache, since time.Time) error {
 36	gi.iterator = NewIterator(gi.conf[keyProjectID], gi.conf[keyToken], since)
 37
 38	// Loop over all matching issues
 39	for gi.iterator.NextIssue() {
 40		issue := gi.iterator.IssueValue()
 41		fmt.Printf("importing issue: %v\n", issue.Title)
 42
 43		// create issue
 44		b, err := gi.ensureIssue(repo, issue)
 45		if err != nil {
 46			return fmt.Errorf("issue creation: %v", err)
 47		}
 48
 49		// Loop over all notes
 50		for gi.iterator.NextNote() {
 51			note := gi.iterator.NoteValue()
 52			if err := gi.ensureNote(repo, b, note); err != nil {
 53				return fmt.Errorf("note creation: %v", err)
 54			}
 55		}
 56
 57		// Loop over all label events
 58		for gi.iterator.NextLabelEvent() {
 59			labelEvent := gi.iterator.LabelEventValue()
 60			if err := gi.ensureLabelEvent(repo, b, labelEvent); err != nil {
 61				return fmt.Errorf("label event creation: %v", err)
 62			}
 63		}
 64
 65		// commit bug state
 66		if err := b.CommitAsNeeded(); err != nil {
 67			return fmt.Errorf("bug commit: %v", err)
 68		}
 69	}
 70
 71	if err := gi.iterator.Error(); err != nil {
 72		fmt.Printf("import error: %v\n", err)
 73		return err
 74	}
 75
 76	fmt.Printf("Successfully imported %d issues and %d identities from Gitlab\n", gi.importedIssues, gi.importedIdentities)
 77	return nil
 78}
 79
 80func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
 81	// ensure issue author
 82	author, err := gi.ensurePerson(repo, issue.Author.ID)
 83	if err != nil {
 84		return nil, err
 85	}
 86
 87	// resolve bug
 88	b, err := repo.ResolveBugCreateMetadata(keyGitlabUrl, issue.WebURL)
 89	if err != nil && err != bug.ErrBugNotExist {
 90		return nil, err
 91	}
 92
 93	if err == bug.ErrBugNotExist {
 94		cleanText, err := text.Cleanup(string(issue.Description))
 95		if err != nil {
 96			return nil, err
 97		}
 98
 99		// create bug
100		b, _, err = repo.NewBugRaw(
101			author,
102			issue.CreatedAt.Unix(),
103			issue.Title,
104			cleanText,
105			nil,
106			map[string]string{
107				keyOrigin:    target,
108				keyGitlabId:  parseID(issue.ID),
109				keyGitlabUrl: issue.WebURL,
110			},
111		)
112
113		if err != nil {
114			return nil, err
115		}
116
117		// importing a new bug
118		gi.importedIssues++
119
120		return b, nil
121	}
122
123	return b, nil
124}
125
126func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
127	id := parseID(note.ID)
128
129	// ensure issue author
130	author, err := gi.ensurePerson(repo, note.Author.ID)
131	if err != nil {
132		return err
133	}
134
135	hash, errResolve := b.ResolveOperationWithMetadata(keyGitlabId, id)
136	if errResolve != cache.ErrNoMatchingOp {
137		return err
138	}
139
140	noteType, body := GetNoteType(note)
141	switch noteType {
142	case NOTE_CLOSED:
143		_, err = b.CloseRaw(
144			author,
145			note.CreatedAt.Unix(),
146			map[string]string{
147				keyGitlabId:  id,
148				keyGitlabUrl: "",
149			},
150		)
151		return err
152
153	case NOTE_REOPENED:
154		_, err = b.OpenRaw(
155			author,
156			note.CreatedAt.Unix(),
157			map[string]string{
158				keyGitlabId:  id,
159				keyGitlabUrl: "",
160			},
161		)
162		return err
163
164	case NOTE_DESCRIPTION_CHANGED:
165		issue := gi.iterator.IssueValue()
166
167		// since gitlab doesn't provide the issue history
168		// we should check for "changed the description" notes and compare issue texts
169		// TODO: Check only one time and ignore next 'description change' within one issue
170		if issue.Description != b.Snapshot().Comments[0].Message {
171
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 errResolve == 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		comment, err := b.Snapshot().SearchComment(hash)
222		if err != nil {
223			return err
224		}
225
226		// compare local bug comment with the new note body
227		if comment.Message != cleanText {
228			// comment edition
229			_, err = b.EditCommentRaw(
230				author,
231				note.UpdatedAt.Unix(),
232				target,
233				cleanText,
234				map[string]string{
235					// no metadata unique metadata to store
236					keyGitlabId:  "",
237					keyGitlabUrl: "",
238				},
239			)
240
241			return err
242		}
243
244		return nil
245
246	case NOTE_TITLE_CHANGED:
247		// title change events are given new notes
248		_, err = b.SetTitleRaw(
249			author,
250			note.CreatedAt.Unix(),
251			body,
252			map[string]string{
253				keyGitlabId:  id,
254				keyGitlabUrl: "",
255			},
256		)
257
258		return err
259
260	default:
261		// non handled note types, this is not an error
262		//TODO: send warning via channel
263		return nil
264	}
265
266	return nil
267}
268
269func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
270	_, err := b.ResolveOperationWithMetadata(keyGitlabId, parseID(labelEvent.ID))
271	if err != cache.ErrNoMatchingOp {
272		return err
273	}
274
275	// ensure issue author
276	author, err := gi.ensurePerson(repo, labelEvent.User.ID)
277	if err != nil {
278		return err
279	}
280
281	switch labelEvent.Action {
282	case "add":
283		_, err = b.ForceChangeLabelsRaw(
284			author,
285			labelEvent.CreatedAt.Unix(),
286			[]string{labelEvent.Label.Name},
287			nil,
288			map[string]string{
289				keyGitlabId: parseID(labelEvent.ID),
290			},
291		)
292
293	case "remove":
294		_, err = b.ForceChangeLabelsRaw(
295			author,
296			labelEvent.CreatedAt.Unix(),
297			nil,
298			[]string{labelEvent.Label.Name},
299			map[string]string{
300				keyGitlabId: parseID(labelEvent.ID),
301			},
302		)
303
304	default:
305		panic("unexpected label event action")
306	}
307
308	return err
309}
310
311func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
312	// Look first in the cache
313	i, err := repo.ResolveIdentityImmutableMetadata(keyGitlabId, strconv.Itoa(id))
314	if err == nil {
315		return i, nil
316	}
317	if _, ok := err.(identity.ErrMultipleMatch); ok {
318		return nil, err
319	}
320
321	// importing a new identity
322	gi.importedIdentities++
323
324	client := buildClient(gi.conf["token"])
325
326	user, _, err := client.Users.GetUser(id)
327	if err != nil {
328		return nil, err
329	}
330
331	return repo.NewIdentityRaw(
332		user.Name,
333		user.PublicEmail,
334		user.Username,
335		user.AvatarURL,
336		map[string]string{
337			keyGitlabId:    strconv.Itoa(id),
338			keyGitlabLogin: user.Username,
339		},
340	)
341}
342
343func parseID(id int) string {
344	return fmt.Sprintf("%d", id)
345}