dmitri.shuralyov.com/service/change/...

Initial commit.
dmitshur committed 3 years ago commit 91737e7bcf7f5bea8aa3dcd6104405ee5598e672
gerritapi/gerritapi.go
@@ -0,0 +1,198 @@
+// Package gerritapi implements issues.Service using Gerrit API client.
+package gerritapi
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"sort"
+	"strings"
+	"time"
+
+	"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 {
+	s := service{
+		cl:     client,
+		domain: client.BaseURL().Host,
+		//users: users,
+	}
+
+	//s.currentUser, s.currentUserErr = s.users.GetAuthenticatedSpec(context.TODO())
+
+	return s
+}
+
+type service struct {
+	cl     *gerrit.Client
+	domain string
+
+	//users users.Service
+
+	//currentUser    users.UserSpec
+	//currentUserErr error
+}
+
+func (s service) List(ctx context.Context, rs issues.RepoSpec, opt issues.IssueListOptions) ([]issues.Issue, error) {
+	project := project(rs)
+	var query string
+	switch opt.State {
+	case issues.StateFilter(issues.OpenState):
+		query = fmt.Sprintf("project:%s status:open", project)
+	case issues.StateFilter(issues.ClosedState):
+		query = fmt.Sprintf("project:%s status:closed", project)
+	case issues.AllStates:
+		query = fmt.Sprintf("project:%s", project)
+	}
+	changes, _, err := s.cl.Changes.QueryChanges(&gerrit.QueryChangeOptions{
+		QueryOptions: gerrit.QueryOptions{
+			Query: []string{query},
+			Limit: 25,
+		},
+		ChangeOptions: gerrit.ChangeOptions{
+			AdditionalFields: []string{"DETAILED_ACCOUNTS", "MESSAGES"},
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+	var is []issues.Issue
+	for _, change := range *changes {
+		if change.Status == "DRAFT" {
+			continue
+		}
+		is = append(is, issues.Issue{
+			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),
+		})
+	}
+	//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) {
+	// TODO.
+	return 0, nil
+}
+
+func (s service) Get(ctx context.Context, _ issues.RepoSpec, id uint64) (issues.Issue, error) {
+	change, _, err := s.cl.Changes.GetChange(fmt.Sprint(id), &gerrit.ChangeOptions{
+		AdditionalFields: []string{"DETAILED_ACCOUNTS"},
+	})
+	if err != nil {
+		return issues.Issue{}, err
+	}
+	if change.Status == "DRAFT" {
+		return issues.Issue{}, 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,
+		},
+	}, nil
+}
+
+func state(status string) issues.State {
+	switch status {
+	case "NEW":
+		return issues.OpenState
+	case "ABANDONED", "MERGED":
+		return issues.ClosedState
+	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) {
+	// 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
+	}
+	var comments []issues.Comment
+	for idx, message := range change.Messages {
+		comments = append(comments, issues.Comment{
+			ID:        uint64(idx), // TODO: message.ID is not uint64; e.g., "bfba753d015916303152305cee7152ea7a112fe0".
+			User:      s.gerritUser(message.Author),
+			CreatedAt: time.Time(message.Date),
+			Body:      message.Message,
+			Editable:  false,
+		})
+	}
+	return comments, nil
+}
+
+func (s service) ListEvents(ctx context.Context, _ issues.RepoSpec, id uint64, opt *issues.ListOptions) ([]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,
+		},
+		Login: user.Name, //user.Username, // TODO.
+		Name:  user.Name,
+		//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:]
+	}
+	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] }
maintner/maintner.go
@@ -0,0 +1,182 @@
+// Package maintner implements issues.Service using a x/build/maintner corpus.
+package maintner
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"sort"
+	"strings"
+
+	"github.com/shurcooL/issues"
+	"github.com/shurcooL/users"
+	"golang.org/x/build/maintner"
+)
+
+// NewService creates a Gerrit-backed issues.Service using given Gerrit client.
+// corpus must be non-nil.
+func NewService(corpus *maintner.Corpus) issues.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) {
+	// TODO: Pagination. Respect opt.Start and opt.Length, if given.
+
+	var is []issues.Issue
+
+	project := s.c.Gerrit().Project(serverProject(rs))
+	err := project.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
+		state := state(cl.Status)
+		switch {
+		case opt.State == issues.StateFilter(issues.OpenState) && state != issues.OpenState:
+			return nil
+		case opt.State == issues.StateFilter(issues.ClosedState) && state != issues.ClosedState:
+			return nil
+		}
+
+		is = append(is, issues.Issue{
+			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,
+			},
+			//Replies: len(cl.Messages),
+		})
+
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	//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) {
+	var count uint64
+
+	project := s.c.Gerrit().Project(serverProject(rs))
+	err := project.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
+		state := state(cl.Status)
+		switch {
+		case opt.State == issues.StateFilter(issues.OpenState) && state != issues.OpenState:
+			return nil
+		case opt.State == issues.StateFilter(issues.ClosedState) && state != issues.ClosedState:
+			return nil
+		}
+
+		count++
+
+		return nil
+	})
+	if err != nil {
+		return 0, err
+	}
+
+	return count, nil
+}
+
+func (s service) Get(ctx context.Context, _ issues.RepoSpec, id uint64) (issues.Issue, error) {
+	// TODO.
+	return issues.Issue{}, os.ErrNotExist
+}
+
+func state(status string) issues.State {
+	switch status {
+	case "new":
+		return issues.OpenState
+	case "abandoned", "merged":
+		return issues.ClosedState
+	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) {
+	// TODO.
+	return nil, nil
+}
+
+func (s service) ListEvents(ctx context.Context, _ issues.RepoSpec, id uint64, opt *issues.ListOptions) ([]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 gerritUser(user *maintner.GitPerson) users.User {
+	return users.User{
+		UserSpec: users.UserSpec{
+			ID:     0,  // TODO.
+			Domain: "", // TODO.
+		},
+		Login: user.Name(), //user.Username, // TODO.
+		Name:  user.Name(),
+		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) {
+	if i := strings.IndexByte(rs.URI, '/'); i != -1 {
+		return rs.URI[:i], rs.URI[i+1:]
+	}
+	return "", ""
+}
+
+func server(rs issues.RepoSpec) string {
+	if i := strings.IndexByte(rs.URI, '/'); i != -1 {
+		return rs.URI[:i]
+	}
+	return ""
+}
+
+func project(rs issues.RepoSpec) string {
+	if i := strings.IndexByte(rs.URI, '/'); i != -1 {
+		return rs.URI[i+1:]
+	}
+	return ""
+}
+
+// firstParagraph returns the first paragraph of text s.
+func firstParagraph(s string) string {
+	i := strings.Index(s, "\n\n")
+	if i == -1 {
+		return s
+	}
+	return s[:i]
+}