import.go

  1package github
  2
  3import (
  4	"context"
  5	"fmt"
  6	"strings"
  7
  8	"github.com/MichaelMure/git-bug/bridge/core"
  9	"github.com/MichaelMure/git-bug/bug"
 10	"github.com/MichaelMure/git-bug/cache"
 11	"github.com/shurcooL/githubv4"
 12)
 13
 14const keyGithubId = "github-id"
 15const keyGithubUrl = "github-url"
 16
 17// githubImporter implement the Importer interface
 18type githubImporter struct{}
 19
 20type Actor struct {
 21	Login     githubv4.String
 22	AvatarUrl githubv4.String
 23}
 24
 25type ActorEvent struct {
 26	Id        githubv4.ID
 27	CreatedAt githubv4.DateTime
 28	Actor     Actor
 29}
 30
 31type AuthorEvent struct {
 32	Id        githubv4.ID
 33	CreatedAt githubv4.DateTime
 34	Author    Actor
 35}
 36
 37type TimelineItem struct {
 38	Typename githubv4.String `graphql:"__typename"`
 39
 40	// Issue
 41	IssueComment struct {
 42		AuthorEvent
 43		Body githubv4.String
 44		Url  githubv4.URI
 45		// TODO: edition
 46	} `graphql:"... on IssueComment"`
 47
 48	// Label
 49	LabeledEvent struct {
 50		ActorEvent
 51		Label struct {
 52			// Color githubv4.String
 53			Name githubv4.String
 54		}
 55	} `graphql:"... on LabeledEvent"`
 56	UnlabeledEvent struct {
 57		ActorEvent
 58		Label struct {
 59			// Color githubv4.String
 60			Name githubv4.String
 61		}
 62	} `graphql:"... on UnlabeledEvent"`
 63
 64	// Status
 65	ClosedEvent struct {
 66		ActorEvent
 67		// Url githubv4.URI
 68	} `graphql:"... on  ClosedEvent"`
 69	ReopenedEvent struct {
 70		ActorEvent
 71	} `graphql:"... on  ReopenedEvent"`
 72
 73	// Title
 74	RenamedTitleEvent struct {
 75		ActorEvent
 76		CurrentTitle  githubv4.String
 77		PreviousTitle githubv4.String
 78	} `graphql:"... on RenamedTitleEvent"`
 79}
 80
 81type Issue struct {
 82	AuthorEvent
 83	Title string
 84	Body  githubv4.String
 85	Url   githubv4.URI
 86
 87	Timeline struct {
 88		Nodes    []TimelineItem
 89		PageInfo struct {
 90			EndCursor   githubv4.String
 91			HasNextPage bool
 92		}
 93	} `graphql:"timeline(first: $timelineFirst, after: $timelineAfter)"`
 94}
 95
 96var q struct {
 97	Repository struct {
 98		Issues struct {
 99			Nodes    []Issue
100			PageInfo struct {
101				EndCursor   githubv4.String
102				HasNextPage bool
103			}
104		} `graphql:"issues(first: $issueFirst, after: $issueAfter, orderBy: {field: CREATED_AT, direction: ASC})"`
105	} `graphql:"repository(owner: $owner, name: $name)"`
106}
107
108func (*githubImporter) ImportAll(repo *cache.RepoCache, conf core.Configuration) error {
109	client := buildClient(conf)
110
111	variables := map[string]interface{}{
112		"owner":         githubv4.String(conf[keyUser]),
113		"name":          githubv4.String(conf[keyProject]),
114		"issueFirst":    githubv4.Int(1),
115		"issueAfter":    (*githubv4.String)(nil),
116		"timelineFirst": githubv4.Int(10),
117		"timelineAfter": (*githubv4.String)(nil),
118	}
119
120	var b *cache.BugCache
121
122	for {
123		err := client.Query(context.TODO(), &q, variables)
124		if err != nil {
125			return err
126		}
127
128		if len(q.Repository.Issues.Nodes) != 1 {
129			return fmt.Errorf("Something went wrong when iterating issues, len is %d", len(q.Repository.Issues.Nodes))
130		}
131
132		issue := q.Repository.Issues.Nodes[0]
133
134		if b == nil {
135			b, err = importIssue(repo, issue)
136			if err != nil {
137				return err
138			}
139		}
140
141		for _, item := range q.Repository.Issues.Nodes[0].Timeline.Nodes {
142			importTimelineItem(b, item)
143		}
144
145		if !issue.Timeline.PageInfo.HasNextPage {
146			err = b.CommitAsNeeded()
147			if err != nil {
148				return err
149			}
150
151			b = nil
152
153			if !q.Repository.Issues.PageInfo.HasNextPage {
154				break
155			}
156
157			variables["issueAfter"] = githubv4.NewString(q.Repository.Issues.PageInfo.EndCursor)
158			variables["timelineAfter"] = (*githubv4.String)(nil)
159			continue
160		}
161
162		variables["timelineAfter"] = githubv4.NewString(issue.Timeline.PageInfo.EndCursor)
163	}
164
165	return nil
166}
167
168func (*githubImporter) Import(repo *cache.RepoCache, conf core.Configuration, id string) error {
169	fmt.Println(conf)
170	fmt.Println("IMPORT")
171
172	return nil
173}
174
175func importIssue(repo *cache.RepoCache, issue Issue) (*cache.BugCache, error) {
176	fmt.Printf("import issue: %s\n", issue.Title)
177
178	// TODO: check + import files
179
180	return repo.NewBugRaw(
181		makePerson(issue.Author),
182		issue.CreatedAt.Unix(),
183		issue.Title,
184		cleanupText(string(issue.Body)),
185		nil,
186		map[string]string{
187			keyGithubId:  parseId(issue.Id),
188			keyGithubUrl: issue.Url.String(),
189		},
190	)
191}
192
193func importTimelineItem(b *cache.BugCache, item TimelineItem) error {
194	switch item.Typename {
195	case "IssueComment":
196		// fmt.Printf("import %s: %s\n", item.Typename, item.IssueComment)
197		return b.AddCommentRaw(
198			makePerson(item.IssueComment.Author),
199			item.IssueComment.CreatedAt.Unix(),
200			cleanupText(string(item.IssueComment.Body)),
201			nil,
202			map[string]string{
203				keyGithubId:  parseId(item.IssueComment.Id),
204				keyGithubUrl: item.IssueComment.Url.String(),
205			},
206		)
207
208	case "LabeledEvent":
209		// fmt.Printf("import %s: %s\n", item.Typename, item.LabeledEvent)
210		_, err := b.ChangeLabelsRaw(
211			makePerson(item.LabeledEvent.Actor),
212			item.LabeledEvent.CreatedAt.Unix(),
213			[]string{
214				string(item.LabeledEvent.Label.Name),
215			},
216			nil,
217		)
218		return err
219
220	case "UnlabeledEvent":
221		// fmt.Printf("import %s: %s\n", item.Typename, item.UnlabeledEvent)
222		_, err := b.ChangeLabelsRaw(
223			makePerson(item.UnlabeledEvent.Actor),
224			item.UnlabeledEvent.CreatedAt.Unix(),
225			nil,
226			[]string{
227				string(item.UnlabeledEvent.Label.Name),
228			},
229		)
230		return err
231
232	case "ClosedEvent":
233		// fmt.Printf("import %s: %s\n", item.Typename, item.ClosedEvent)
234		return b.CloseRaw(
235			makePerson(item.ClosedEvent.Actor),
236			item.ClosedEvent.CreatedAt.Unix(),
237		)
238
239	case "ReopenedEvent":
240		// fmt.Printf("import %s: %s\n", item.Typename, item.ReopenedEvent)
241		return b.OpenRaw(
242			makePerson(item.ReopenedEvent.Actor),
243			item.ReopenedEvent.CreatedAt.Unix(),
244		)
245
246	case "RenamedTitleEvent":
247		// fmt.Printf("import %s: %s\n", item.Typename, item.RenamedTitleEvent)
248		return b.SetTitleRaw(
249			makePerson(item.RenamedTitleEvent.Actor),
250			item.RenamedTitleEvent.CreatedAt.Unix(),
251			string(item.RenamedTitleEvent.CurrentTitle),
252		)
253
254	default:
255		fmt.Println("ignore event ", item.Typename)
256	}
257
258	return nil
259}
260
261// makePerson create a bug.Person from the Github data
262func makePerson(actor Actor) bug.Person {
263	return bug.Person{
264		Name:      string(actor.Login),
265		AvatarUrl: string(actor.AvatarUrl),
266	}
267}
268
269// parseId convert the unusable githubv4.ID (an interface{}) into a string
270func parseId(id githubv4.ID) string {
271	return fmt.Sprintf("%v", id)
272}
273
274func cleanupText(text string) string {
275	// windows new line, Github, really ?
276	return strings.Replace(text, "\r\n", "\n", -1)
277}