@@ -3,34 +3,38 @@ package changes import ( "context" "time" "github.com/shurcooL/issues" "github.com/shurcooL/users" ) // Service defines methods of a change tracking service. type Service interface { // List changes. List(ctx context.Context, repo string) ([]Change, error) List(ctx context.Context, repo string, opt ListOptions) ([]Change, error) // Count changes. Count(ctx context.Context, repo string) (uint64, error) Count(ctx context.Context, repo string, opt ListOptions) (uint64, error) // Get a change. Get(ctx context.Context, repo string, id uint64) (Change, error) // Get a change diff. GetDiff(ctx context.Context, repo string, id uint64) ([]byte, error) // ListComments lists comments for specified change id. //ListComments(ctx context.Context, repo string, id uint64, opt *ListOptions) ([]Comment, error) ListComments(ctx context.Context, repo string, id uint64, opt *ListCommentsOptions) ([]issues.Comment, error) // ListEvents lists events for specified change id. //ListEvents(ctx context.Context, repo string, id uint64, opt *ListOptions) ([]Event, error) ListEvents(ctx context.Context, repo string, id uint64, opt *ListCommentsOptions) ([]issues.Event, error) } // Change represents a change in a repository. type Change struct { ID uint64 State State Title string Labels []issues.Label Author users.User CreatedAt time.Time Replies int // Number of replies to this change (not counting the mandatory change description comment). } @@ -43,5 +47,27 @@ const ( // ClosedState is when a change is closed. ClosedState State = "closed" // MergedState is when a change is merged. MergedState State = "merged" ) // ListOptions are options for list operations. type ListOptions struct { State StateFilter } // StateFilter is a filter by state. type StateFilter State const ( // AllStates is a state filter that includes all issues. AllStates StateFilter = "all" ) // ListCommentsOptions controls pagination. type ListCommentsOptions struct { // Start is the index of first result to retrieve, zero-indexed. Start int // Length is the number of results to include. Length int }
@@ -1,24 +1,25 @@ // Package gerritapi implements issues.Service using Gerrit API client. // Package gerritapi implements a read-only changes.Service using Gerrit API client. package gerritapi import ( "context" "fmt" "os" "sort" "strings" "time" "dmitri.shuralyov.com/changes" "github.com/andygrunwald/go-gerrit" "github.com/shurcooL/issues" "github.com/shurcooL/users" ) // NewService creates a Gerrit-backed issues.Service using given Gerrit client. // client must be non-nil. func NewService(client *gerrit.Client) issues.Service { func NewService(client *gerrit.Client) changes.Service { s := service{ cl: client, domain: client.BaseURL().Host, //users: users, } @@ -36,22 +37,24 @@ type service struct { //currentUser users.UserSpec //currentUserErr error } func (s service) List(ctx context.Context, rs issues.RepoSpec, opt issues.IssueListOptions) ([]issues.Issue, error) { func (s service) List(ctx context.Context, rs string, opt changes.ListOptions) ([]changes.Change, error) { project := project(rs) var query string switch opt.State { case issues.StateFilter(issues.OpenState): case changes.StateFilter(changes.OpenState): query = fmt.Sprintf("project:%s status:open", project) case issues.StateFilter(issues.ClosedState): case changes.StateFilter(changes.ClosedState): query = fmt.Sprintf("project:%s status:closed", project) case issues.AllStates: case changes.StateFilter(changes.MergedState): query = fmt.Sprintf("project:%s status:merged", project) case changes.AllStates: query = fmt.Sprintf("project:%s", project) } changes, _, err := s.cl.Changes.QueryChanges(&gerrit.QueryChangeOptions{ cs, _, err := s.cl.Changes.QueryChanges(&gerrit.QueryChangeOptions{ QueryOptions: gerrit.QueryOptions{ Query: []string{query}, Limit: 25, }, ChangeOptions: gerrit.ChangeOptions{ @@ -59,75 +62,82 @@ func (s service) List(ctx context.Context, rs issues.RepoSpec, opt issues.IssueL }, }) if err != nil { return nil, err } var is []issues.Issue for _, change := range *changes { var is []changes.Change for _, change := range *cs { if change.Status == "DRAFT" { continue } is = append(is, issues.Issue{ is = append(is, changes.Change{ ID: uint64(change.Number), State: state(change.Status), Title: change.Subject, //Labels: labels, // TODO. Comment: issues.Comment{ User: s.gerritUser(change.Owner), CreatedAt: time.Time(change.Created), }, Replies: len(change.Messages), Author: s.gerritUser(change.Owner), CreatedAt: time.Time(change.Created), Replies: len(change.Messages), }) } //sort.Sort(sort.Reverse(byID(is))) // For some reason, IDs don't completely line up with created times. sort.Slice(is, func(i, j int) bool { return is[i].CreatedAt.After(is[j].CreatedAt) }) return is, nil } func (s service) Count(_ context.Context, rs issues.RepoSpec, opt issues.IssueListOptions) (uint64, error) { func (s service) Count(_ context.Context, repo string, opt changes.ListOptions) (uint64, error) { // TODO. return 0, nil } func (s service) Get(ctx context.Context, _ issues.RepoSpec, id uint64) (issues.Issue, error) { func (s service) Get(ctx context.Context, _ string, id uint64) (changes.Change, error) { change, _, err := s.cl.Changes.GetChange(fmt.Sprint(id), &gerrit.ChangeOptions{ AdditionalFields: []string{"DETAILED_ACCOUNTS"}, }) if err != nil { return issues.Issue{}, err return changes.Change{}, err } if change.Status == "DRAFT" { return issues.Issue{}, os.ErrNotExist return changes.Change{}, os.ErrNotExist } return issues.Issue{ ID: id, State: state(change.Status), Title: change.Subject, Comment: issues.Comment{ User: s.gerritUser(change.Owner), CreatedAt: time.Time(change.Created), Editable: false, }, return changes.Change{ ID: id, State: state(change.Status), Title: change.Subject, Author: s.gerritUser(change.Owner), CreatedAt: time.Time(change.Created), }, nil } func state(status string) issues.State { func state(status string) changes.State { switch status { case "NEW": return issues.OpenState case "ABANDONED", "MERGED": return issues.ClosedState return changes.OpenState case "ABANDONED": return changes.ClosedState case "MERGED": return changes.MergedState case "DRAFT": panic("not sure how to deal with DRAFT status") default: panic("unreachable") } } func (s service) ListComments(ctx context.Context, _ issues.RepoSpec, id uint64, opt *issues.ListOptions) ([]issues.Comment, error) { func (s service) GetDiff(ctx context.Context, _ string, id uint64) ([]byte, error) { diff, _, err := s.cl.Changes.GetPatch(fmt.Sprint(id), "current", &gerrit.PatchOptions{ Path: "src", // TODO. }) if err != nil { return nil, err } return []byte(*diff), nil } func (s service) ListComments(ctx context.Context, _ string, id uint64, opt *changes.ListCommentsOptions) ([]issues.Comment, error) { // TODO: Pagination. Respect opt.Start and opt.Length, if given. change, _, err := s.cl.Changes.GetChangeDetail(fmt.Sprint(id), nil) if err != nil { return nil, err @@ -143,35 +153,15 @@ func (s service) ListComments(ctx context.Context, _ issues.RepoSpec, id uint64, }) } return comments, nil } func (s service) ListEvents(ctx context.Context, _ issues.RepoSpec, id uint64, opt *issues.ListOptions) ([]issues.Event, error) { func (s service) ListEvents(ctx context.Context, _ string, id uint64, opt *changes.ListCommentsOptions) ([]issues.Event, error) { // TODO. return nil, nil } func (s service) CreateComment(_ context.Context, rs issues.RepoSpec, id uint64, c issues.Comment) (issues.Comment, error) { // TODO. return issues.Comment{}, fmt.Errorf("CreateComment: not implemented") } func (s service) Create(_ context.Context, rs issues.RepoSpec, i issues.Issue) (issues.Issue, error) { // TODO. return issues.Issue{}, fmt.Errorf("Create: not implemented") } func (s service) Edit(_ context.Context, rs issues.RepoSpec, id uint64, ir issues.IssueRequest) (issues.Issue, []issues.Event, error) { // TODO. return issues.Issue{}, nil, fmt.Errorf("Edit: not implemented") } func (s service) EditComment(ctx context.Context, rs issues.RepoSpec, id uint64, cr issues.CommentRequest) (issues.Comment, error) { // TODO. return issues.Comment{}, fmt.Errorf("EditComment: not implemented") } func (s service) gerritUser(user gerrit.AccountInfo) users.User { return users.User{ UserSpec: users.UserSpec{ ID: uint64(user.AccountID), Domain: s.domain, @@ -181,18 +171,11 @@ func (s service) gerritUser(user gerrit.AccountInfo) users.User { //Email: user.Email, AvatarURL: fmt.Sprintf("https://%s/accounts/%d/avatar?s=96", s.domain, user.AccountID), } } func project(rs issues.RepoSpec) string { if i := strings.IndexByte(rs.URI, '/'); i != -1 { return rs.URI[i+1:] func project(repo string) string { if i := strings.IndexByte(repo, '/'); i != -1 { return repo[i+1:] } return "" } // byID implements sort.Interface. type byID []issues.Issue func (s byID) Len() int { return len(s) } func (s byID) Less(i, j int) bool { return s[i].ID < s[j].ID } func (s byID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
@@ -1,6 +1,6 @@ // Package githubapi implements a read-only issues.Service using // Package githubapi implements a read-only changes.Service using // using GitHub GraphQL API v4 clients that serves PRs. package githubapi import ( "context" @@ -15,14 +15,14 @@ import ( "github.com/shurcooL/notifications" "github.com/shurcooL/reactions" "github.com/shurcooL/users" ) // NewService creates a GitHub-backed issues.Service using given GitHub clients. // NewService creates a GitHub-backed changes.Service using given GitHub clients. // It uses notifications service, if not nil. At this time it infers the current user // from the client (its authentication info), and cannot be used to serve multiple users. func NewService(clientV3 *github.Client, clientV4 *githubql.Client, notifications notifications.ExternalService, users users.Service) issues.Service { func NewService(clientV3 *github.Client, clientV4 *githubql.Client, notifications notifications.ExternalService, users users.Service) changes.Service { s := service{ clV3: clientV3, clV4: clientV4, notifications: notifications, users: users, @@ -47,25 +47,25 @@ type service struct { } // We use 0 as a special ID for the comment that is the issue description. This comment is edited differently. const issueDescriptionCommentID uint64 = 0 func (s service) List(ctx context.Context, rs issues.RepoSpec, opt issues.IssueListOptions) ([]issues.Issue, error) { func (s service) List(ctx context.Context, rs string, opt changes.ListOptions) ([]changes.Change, error) { repo, err := ghRepoSpec(rs) if err != nil { // TODO: Map to 400 Bad Request HTTP error. return nil, err } var states []githubql.PullRequestState switch opt.State { case issues.StateFilter(changes.OpenState): case changes.StateFilter(changes.OpenState): states = []githubql.PullRequestState{githubql.PullRequestStateOpen} case issues.StateFilter(changes.ClosedState): case changes.StateFilter(changes.ClosedState): states = []githubql.PullRequestState{githubql.PullRequestStateClosed} case issues.StateFilter(changes.MergedState): case changes.StateFilter(changes.MergedState): states = []githubql.PullRequestState{githubql.PullRequestStateMerged} case issues.AllStates: case changes.AllStates: states = nil // No states to filter the PRs by. default: // TODO: Map to 400 Bad Request HTTP error. return nil, fmt.Errorf("opt.State has unsupported value %q", opt.State) } @@ -98,49 +98,47 @@ func (s service) List(ctx context.Context, rs issues.RepoSpec, opt issues.IssueL } err = s.clV4.Query(ctx, &q, variables) if err != nil { return nil, err } var is []issues.Issue for _, issue := range q.Repository.PullRequests.Nodes { var is []changes.Change for _, pr := range q.Repository.PullRequests.Nodes { var labels []issues.Label for _, l := range issue.Labels.Nodes { for _, l := range pr.Labels.Nodes { labels = append(labels, issues.Label{ Name: l.Name, Color: ghColor(l.Color), }) } is = append(is, issues.Issue{ ID: issue.Number, State: issues.State(ghPRState(issue.State)), Title: issue.Title, Labels: labels, Comment: issues.Comment{ User: ghActor(issue.Author), CreatedAt: issue.CreatedAt.Time, }, Replies: issue.Comments.TotalCount, is = append(is, changes.Change{ ID: pr.Number, State: ghPRState(pr.State), Title: pr.Title, Labels: labels, Author: ghActor(pr.Author), CreatedAt: pr.CreatedAt.Time, Replies: pr.Comments.TotalCount, }) } return is, nil } func (s service) Count(ctx context.Context, rs issues.RepoSpec, opt issues.IssueListOptions) (uint64, error) { func (s service) Count(ctx context.Context, rs string, opt changes.ListOptions) (uint64, error) { repo, err := ghRepoSpec(rs) if err != nil { // TODO: Map to 400 Bad Request HTTP error. return 0, err } var states []githubql.PullRequestState switch opt.State { case issues.StateFilter(changes.OpenState): case changes.StateFilter(changes.OpenState): states = []githubql.PullRequestState{githubql.PullRequestStateOpen} case issues.StateFilter(changes.ClosedState): case changes.StateFilter(changes.ClosedState): states = []githubql.PullRequestState{githubql.PullRequestStateClosed} case issues.StateFilter(changes.MergedState): case changes.StateFilter(changes.MergedState): states = []githubql.PullRequestState{githubql.PullRequestStateMerged} case issues.AllStates: case changes.AllStates: states = nil // No states to filter the PRs by. default: // TODO: Map to 400 Bad Request HTTP error. return 0, fmt.Errorf("opt.State has unsupported value %q", opt.State) } @@ -158,15 +156,15 @@ func (s service) Count(ctx context.Context, rs issues.RepoSpec, opt issues.Issue } err = s.clV4.Query(ctx, &q, variables) return q.Repository.PullRequests.TotalCount, err } func (s service) Get(ctx context.Context, rs issues.RepoSpec, id uint64) (issues.Issue, error) { func (s service) Get(ctx context.Context, rs string, id uint64) (changes.Change, error) { repo, err := ghRepoSpec(rs) if err != nil { // TODO: Map to 400 Bad Request HTTP error. return issues.Issue{}, err return changes.Change{}, err } var q struct { Repository struct { PullRequest struct { Number uint64 @@ -183,11 +181,11 @@ func (s service) Get(ctx context.Context, rs issues.RepoSpec, id uint64) (issues "repositoryName": githubql.String(repo.Repo), "prNumber": githubql.Int(id), } err = s.clV4.Query(ctx, &q, variables) if err != nil { return issues.Issue{}, err return changes.Change{}, err } if s.currentUser.ID != 0 { // Mark as read. err = s.markRead(ctx, rs, id) @@ -196,23 +194,33 @@ func (s service) Get(ctx context.Context, rs issues.RepoSpec, id uint64) (issues } } // TODO: Eliminate comment body properties from issues.Issue. It's missing increasingly more fields, like Edited, etc. pr := q.Repository.PullRequest return issues.Issue{ ID: pr.Number, State: issues.State(ghPRState(pr.State)), Title: pr.Title, Comment: issues.Comment{ User: ghActor(pr.Author), CreatedAt: pr.CreatedAt.Time, Editable: bool(pr.ViewerCanUpdate), }, return changes.Change{ ID: pr.Number, State: ghPRState(pr.State), Title: pr.Title, Author: ghActor(pr.Author), CreatedAt: pr.CreatedAt.Time, }, nil } func (s service) ListComments(ctx context.Context, rs issues.RepoSpec, id uint64, opt *issues.ListOptions) ([]issues.Comment, error) { func (s service) GetDiff(ctx context.Context, rs string, id uint64) ([]byte, error) { repo, err := ghRepoSpec(rs) if err != nil { // TODO: Map to 400 Bad Request HTTP error. return nil, err } diff, _, err := s.clV3.PullRequests.GetRaw(ctx, repo.Owner, repo.Repo, int(id), github.RawOptions{Type: github.Diff}) if err != nil { return nil, err } return []byte(diff), nil } func (s service) ListComments(ctx context.Context, rs string, id uint64, opt *changes.ListCommentsOptions) ([]issues.Comment, error) { // TODO: Respect opt.Start and opt.Length, if given. repo, err := ghRepoSpec(rs) if err != nil { return nil, err @@ -329,11 +337,11 @@ func (s service) ListComments(ctx context.Context, rs issues.RepoSpec, id uint64 } return comments, nil } func (s service) ListEvents(ctx context.Context, rs issues.RepoSpec, id uint64, opt *issues.ListOptions) ([]issues.Event, error) { func (s service) ListEvents(ctx context.Context, rs string, id uint64, opt *changes.ListCommentsOptions) ([]issues.Event, error) { repo, err := ghRepoSpec(rs) if err != nil { // TODO: Map to 400 Bad Request HTTP error. return nil, err } @@ -443,39 +451,20 @@ func (s service) ListEvents(ctx context.Context, rs issues.RepoSpec, id uint64, events = events[start:end] } return events, nil } func (s service) CreateComment(ctx context.Context, rs issues.RepoSpec, id uint64, c issues.Comment) (issues.Comment, error) { return issues.Comment{}, fmt.Errorf("CreateComment: not implemented") } func (s service) Create(ctx context.Context, rs issues.RepoSpec, i issues.Issue) (issues.Issue, error) { return issues.Issue{}, fmt.Errorf("Create: not implemented") } func (s service) Edit(ctx context.Context, rs issues.RepoSpec, id uint64, ir issues.IssueRequest) (issues.Issue, []issues.Event, error) { return issues.Issue{}, nil, fmt.Errorf("Edit: not implemented") } func (s service) EditComment(ctx context.Context, rs issues.RepoSpec, id uint64, cr issues.CommentRequest) (issues.Comment, error) { return issues.Comment{}, fmt.Errorf("EditComment: not implemented") } type repoSpec struct { Owner string Repo string } func ghRepoSpec(repo issues.RepoSpec) (repoSpec, error) { // TODO, THINK: Include "github.com/" prefix or not? // So far I'm leaning towards "yes", because it's more definitive and matches // local uris that also include host. This way, the host can be checked as part of // request, rather than kept implicit. ghOwnerRepo := strings.Split(repo.URI, "/") func ghRepoSpec(rs string) (repoSpec, error) { // The "github.com/" prefix is expected to be included. ghOwnerRepo := strings.Split(rs, "/") if len(ghOwnerRepo) != 3 || ghOwnerRepo[0] != "github.com" || ghOwnerRepo[1] == "" || ghOwnerRepo[2] == "" { return repoSpec{}, fmt.Errorf(`RepoSpec is not of form "github.com/owner/repo": %q`, repo.URI) return repoSpec{}, fmt.Errorf(`RepoSpec is not of form "github.com/owner/repo": %q`, rs) } return repoSpec{ Owner: ghOwnerRepo[1], Repo: ghOwnerRepo[2], }, nil @@ -645,12 +634,12 @@ const threadType = "Issue" // ThreadType returns the notifications thread type for this service. func (service) ThreadType() string { return threadType } // markRead marks the specified issue as read for current user. func (s service) markRead(ctx context.Context, repo issues.RepoSpec, id uint64) error { func (s service) markRead(ctx context.Context, repo string, id uint64) error { if s.notifications == nil { return nil } return s.notifications.MarkRead(ctx, notifications.RepoSpec(repo), threadType, id) return s.notifications.MarkRead(ctx, notifications.RepoSpec{URI: repo}, threadType, id) }
@@ -1,61 +1,62 @@ // Package maintner implements a read-only issues.Service using // Package maintner implements a read-only changes.Service using // a x/build/maintner corpus that serves Gerrit changes. package maintner import ( "context" "fmt" "log" "sort" "strings" "dmitri.shuralyov.com/changes" "github.com/shurcooL/issues" "github.com/shurcooL/users" "golang.org/x/build/maintner" ) // NewService creates an issues.Service backed with the given corpus. // NewService creates an changes.Service backed with the given corpus. // However, it serves Gerrit changes, not GitHub issues. func NewService(corpus *maintner.Corpus) issues.Service { func NewService(corpus *maintner.Corpus) changes.Service { return service{ c: corpus, } } type service struct { c *maintner.Corpus } func (s service) List(ctx context.Context, rs issues.RepoSpec, opt issues.IssueListOptions) ([]issues.Issue, error) { func (s service) List(ctx context.Context, repo string, opt changes.ListOptions) ([]changes.Change, error) { // TODO: Pagination. Respect opt.Start and opt.Length, if given. var is []issues.Issue var is []changes.Change project := s.c.Gerrit().Project(serverProject(rs)) project := s.c.Gerrit().Project(serverProject(repo)) err := project.ForeachCLUnsorted(func(cl *maintner.GerritCL) error { if cl.Status == "" { log.Printf("empty status for CL %d\n", cl.Number) return nil } state := state(cl.Status) switch { case opt.State == issues.StateFilter(issues.OpenState) && state != issues.OpenState: case opt.State == changes.StateFilter(changes.OpenState) && state != changes.OpenState: return nil case opt.State == issues.StateFilter(issues.ClosedState) && state != issues.ClosedState: case opt.State == changes.StateFilter(changes.ClosedState) && state != changes.ClosedState: return nil case opt.State == changes.StateFilter(changes.MergedState) && state != changes.MergedState: return nil } is = append(is, issues.Issue{ is = append(is, changes.Change{ ID: uint64(cl.Number), State: state, Title: firstParagraph(cl.Commit.Msg), //Labels: labels, // TODO. Comment: issues.Comment{ User: gerritUser(cl.Commit.Author), CreatedAt: cl.Created, }, Author: gerritUser(cl.Commit.Author), CreatedAt: cl.Created, //Replies: len(cl.Messages), }) return nil }) @@ -69,23 +70,25 @@ func (s service) List(ctx context.Context, rs issues.RepoSpec, opt issues.IssueL }) return is, nil } func (s service) Count(_ context.Context, rs issues.RepoSpec, opt issues.IssueListOptions) (uint64, error) { func (s service) Count(_ context.Context, repo string, opt changes.ListOptions) (uint64, error) { var count uint64 project := s.c.Gerrit().Project(serverProject(rs)) project := s.c.Gerrit().Project(serverProject(repo)) err := project.ForeachCLUnsorted(func(cl *maintner.GerritCL) error { if cl.Status == "" { return nil } state := state(cl.Status) switch { case opt.State == issues.StateFilter(issues.OpenState) && state != issues.OpenState: case opt.State == changes.StateFilter(changes.OpenState) && state != changes.OpenState: return nil case opt.State == changes.StateFilter(changes.ClosedState) && state != changes.ClosedState: return nil case opt.State == issues.StateFilter(issues.ClosedState) && state != issues.ClosedState: case opt.State == changes.StateFilter(changes.MergedState) && state != changes.MergedState: return nil } count++ @@ -96,58 +99,45 @@ func (s service) Count(_ context.Context, rs issues.RepoSpec, opt issues.IssueLi } return count, nil } func (s service) Get(ctx context.Context, _ issues.RepoSpec, id uint64) (issues.Issue, error) { func (s service) Get(ctx context.Context, _ string, id uint64) (changes.Change, error) { // TODO. return changes.Change{}, fmt.Errorf("Get: not implemented") } func (s service) GetDiff(ctx context.Context, _ string, id uint64) ([]byte, error) { // TODO. return issues.Issue{}, fmt.Errorf("Get: not implemented") return nil, fmt.Errorf("GetDiff: not implemented") } func state(status string) issues.State { func state(status string) changes.State { switch status { case "new": return issues.OpenState case "abandoned", "merged": return issues.ClosedState return changes.OpenState case "abandoned": return changes.ClosedState case "merged": return changes.MergedState case "draft": panic("not sure how to deal with draft status") default: panic(fmt.Errorf("unrecognized status %q", status)) } } func (s service) ListComments(ctx context.Context, _ issues.RepoSpec, id uint64, opt *issues.ListOptions) ([]issues.Comment, error) { func (s service) ListComments(ctx context.Context, _ string, id uint64, opt *changes.ListCommentsOptions) ([]issues.Comment, error) { // TODO. return nil, fmt.Errorf("ListComments: not implemented") } func (s service) ListEvents(ctx context.Context, _ issues.RepoSpec, id uint64, opt *issues.ListOptions) ([]issues.Event, error) { func (s service) ListEvents(ctx context.Context, _ string, id uint64, opt *changes.ListCommentsOptions) ([]issues.Event, error) { // TODO. return nil, fmt.Errorf("ListEvents: not implemented") } func (s service) CreateComment(_ context.Context, rs issues.RepoSpec, id uint64, c issues.Comment) (issues.Comment, error) { // TODO. return issues.Comment{}, fmt.Errorf("CreateComment: not implemented") } func (s service) Create(_ context.Context, rs issues.RepoSpec, i issues.Issue) (issues.Issue, error) { // TODO. return issues.Issue{}, fmt.Errorf("Create: not implemented") } func (s service) Edit(_ context.Context, rs issues.RepoSpec, id uint64, ir issues.IssueRequest) (issues.Issue, []issues.Event, error) { // TODO. return issues.Issue{}, nil, fmt.Errorf("Edit: not implemented") } func (s service) EditComment(ctx context.Context, rs issues.RepoSpec, id uint64, cr issues.CommentRequest) (issues.Comment, error) { // TODO. return issues.Comment{}, fmt.Errorf("EditComment: not implemented") } func gerritUser(user *maintner.GitPerson) users.User { return users.User{ UserSpec: users.UserSpec{ ID: 0, // TODO. Domain: "", // TODO. @@ -157,32 +147,32 @@ func gerritUser(user *maintner.GitPerson) users.User { Email: user.Email(), //AvatarURL: fmt.Sprintf("https://%s/accounts/%d/avatar?s=96", s.domain, user.AccountID), } } func serverProject(rs issues.RepoSpec) (server, project string) { i := strings.IndexByte(rs.URI, '/') func serverProject(repo string) (server, project string) { i := strings.IndexByte(repo, '/') if i == -1 { return "", "" } return rs.URI[:i], rs.URI[i+1:] return repo[:i], repo[i+1:] } func server(rs issues.RepoSpec) string { i := strings.IndexByte(rs.URI, '/') func server(repo string) string { i := strings.IndexByte(repo, '/') if i == -1 { return "" } return rs.URI[:i] return repo[:i] } func project(rs issues.RepoSpec) string { i := strings.IndexByte(rs.URI, '/') func project(repo string) string { i := strings.IndexByte(repo, '/') if i == -1 { return "" } return rs.URI[i+1:] return repo[i+1:] } // firstParagraph returns the first paragraph of text s. func firstParagraph(s string) string { i := strings.Index(s, "\n\n")