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

Initial commit.
dmitshur committed 7 years ago commit 91737e7bcf7f5bea8aa3dcd6104405ee5598e672
Collapse all
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]
}