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

Rename package changes to change.
dmitshur committed 6 years ago commit 852ae6208008be4cc1eb4ec6c688ab250e31cd3d
Collapse all
changes.go → change.go
@@ -1,7 +1,7 @@
// Package changes provides a changes service definition.
package changes
// Package change provides a change service definition.
package change

import (
	"context"
	"time"

@@ -17,11 +17,11 @@ type Service interface {
	Count(ctx context.Context, repo string, opt ListOptions) (uint64, error)

	// Get a change.
	Get(ctx context.Context, repo string, id uint64) (Change, error)

	// ListTimeline lists timeline items (changes.Comment, changes.Review, changes.TimelineItem) for specified change id.
	// ListTimeline lists timeline items (change.Comment, change.Review, change.TimelineItem) for specified change id.
	ListTimeline(ctx context.Context, repo string, id uint64, opt *ListTimelineOptions) ([]interface{}, error)
	// ListCommits lists change commits.
	ListCommits(ctx context.Context, repo string, id uint64) ([]Commit, error)
	// Get a change diff.
	GetDiff(ctx context.Context, repo string, id uint64, opt *GetDiffOptions) ([]byte, error)
fs/fs.go
@@ -4,48 +4,48 @@ import (
	"context"
	"fmt"
	"os"
	"time"

	"dmitri.shuralyov.com/changes"
	"dmitri.shuralyov.com/service/change"
	"github.com/shurcooL/users"
)

type Service struct{}

var s = struct {
	changes []struct {
		changes.Change
		change.Change
		Timeline []interface{}
		Commits  []changes.Commit
		Commits  []change.Commit
		Diff     []byte
	}
}{
	changes: []struct {
		changes.Change
		change.Change
		Timeline []interface{}
		Commits  []changes.Commit
		Commits  []change.Commit
		Diff     []byte
	}{
		{
			Change: changes.Change{
			Change: change.Change{
				ID:        1,
				State:     changes.OpenState,
				State:     change.OpenState,
				Title:     "Initial implementation of woff2.",
				Labels:    nil,
				Author:    shurcool,
				CreatedAt: time.Now().UTC().Add(-5 * time.Minute),
				Replies:   0,

				Commits: 1,
			},
			Timeline: []interface{}{
				changes.Review{
				change.Review{
					User:      users.User{Login: "Eric Grosse", AvatarURL: "https://lh6.googleusercontent.com/-_sdEtv2PRxk/AAAAAAAAAAI/AAAAAAAAAAA/aE1Q66Cuvb4/s100-p/photo.jpg"},
					CreatedAt: time.Now().UTC().Add(-1 * time.Minute),
					State:     changes.Approved,
					Comments: []changes.InlineComment{
					State:     change.Approved,
					Comments: []change.InlineComment{
						{
							File: "LICENSE",
							Line: 26,
							Body: "Ok by me, but how was this chosen?",
						},
@@ -55,11 +55,11 @@ var s = struct {
							Body: "As someone who reads the server logs, my gut feeling is that 1 QPS of Lookup logs will give me sufficient data to tell me the system is working, without creating a big mess.",
						},
					},
				},
			},
			Commits: []changes.Commit{{
			Commits: []change.Commit{{
				SHA:        "4a911c4a1eabcc20a66ccc5c983dede401da2796",
				Message:    "Initial implementation of woff2.\n\nMaybe some additional details here.",
				Author:     shurcool,
				AuthorTime: time.Now().UTC().Add(-10 * time.Minute),
			}},
@@ -67,34 +67,34 @@ var s = struct {
		},
	},
}

// List changes.
func (*Service) List(ctx context.Context, repo string, opt changes.ListOptions) ([]changes.Change, error) {
func (*Service) List(ctx context.Context, repo string, opt change.ListOptions) ([]change.Change, error) {
	if repo != "dmitri.shuralyov.com/font/woff2" {
		return nil, os.ErrNotExist
	}
	var cs []changes.Change
	var cs []change.Change
	for _, c := range s.changes {
		cs = append(cs, c.Change)
	}
	return cs, nil
}

// Count changes.
func (*Service) Count(ctx context.Context, repo string, opt changes.ListOptions) (uint64, error) {
func (*Service) Count(ctx context.Context, repo string, opt change.ListOptions) (uint64, error) {
	if repo != "dmitri.shuralyov.com/font/woff2" {
		return 0, os.ErrNotExist
	}
	var counts func(s changes.State) bool
	var counts func(s change.State) bool
	switch opt.Filter {
	case changes.FilterOpen:
		counts = func(s changes.State) bool { return s == changes.OpenState }
	case changes.FilterClosedMerged:
		counts = func(s changes.State) bool { return s == changes.ClosedState || s == changes.MergedState }
	case changes.FilterAll:
		counts = func(s changes.State) bool { return true }
	case change.FilterOpen:
		counts = func(s change.State) bool { return s == change.OpenState }
	case change.FilterClosedMerged:
		counts = func(s change.State) bool { return s == change.ClosedState || s == change.MergedState }
	case change.FilterAll:
		counts = func(s change.State) bool { return true }
	default:
		// TODO: Map to 400 Bad Request HTTP error.
		return 0, fmt.Errorf("opt.State has unsupported value %q", opt.Filter)
	}
	var count uint64
@@ -105,35 +105,35 @@ func (*Service) Count(ctx context.Context, repo string, opt changes.ListOptions)
	}
	return count, nil
}

// Get a change.
func (*Service) Get(ctx context.Context, repo string, id uint64) (changes.Change, error) {
func (*Service) Get(ctx context.Context, repo string, id uint64) (change.Change, error) {
	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
		return changes.Change{}, os.ErrNotExist
		return change.Change{}, os.ErrNotExist
	}
	return s.changes[0].Change, nil
}

// ListTimeline lists timeline items (changes.Comment, changes.TimelineItem) for specified change id.
func (*Service) ListTimeline(ctx context.Context, repo string, id uint64, opt *changes.ListTimelineOptions) ([]interface{}, error) {
// ListTimeline lists timeline items (change.Comment, change.Review, change.TimelineItem) for specified change id.
func (*Service) ListTimeline(ctx context.Context, repo string, id uint64, opt *change.ListTimelineOptions) ([]interface{}, error) {
	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
		return nil, os.ErrNotExist
	}
	return s.changes[0].Timeline, nil
}

// ListCommits lists change commits.
func (*Service) ListCommits(ctx context.Context, repo string, id uint64) ([]changes.Commit, error) {
func (*Service) ListCommits(ctx context.Context, repo string, id uint64) ([]change.Commit, error) {
	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
		return nil, os.ErrNotExist
	}
	return s.changes[0].Commits, nil
}

// Get a change diff.
func (*Service) GetDiff(ctx context.Context, repo string, id uint64, opt *changes.GetDiffOptions) ([]byte, error) {
func (*Service) GetDiff(ctx context.Context, repo string, id uint64, opt *change.GetDiffOptions) ([]byte, error) {
	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
		return nil, os.ErrNotExist
	}
	return s.changes[0].Diff, nil
}
gerritapi/gerritapi.go
@@ -1,6 +1,6 @@
// Package gerritapi implements a read-only changes.Service using Gerrit API client.
// Package gerritapi implements a read-only change.Service using Gerrit API client.
package gerritapi

import (
	"context"
	"fmt"
@@ -8,18 +8,18 @@ import (
	"sort"
	"strings"
	"time"
	"unicode"

	"dmitri.shuralyov.com/changes"
	"dmitri.shuralyov.com/service/change"
	"github.com/andygrunwald/go-gerrit"
	"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) changes.Service {
func NewService(client *gerrit.Client) change.Service {
	s := service{
		cl:     client,
		domain: client.BaseURL().Host,
		//users: users,
	}
@@ -37,20 +37,20 @@ type service struct {

	//currentUser    users.UserSpec
	//currentUserErr error
}

func (s service) List(ctx context.Context, rs string, opt changes.ListOptions) ([]changes.Change, error) {
func (s service) List(ctx context.Context, rs string, opt change.ListOptions) ([]change.Change, error) {
	project := project(rs)
	var query string
	switch opt.Filter {
	case changes.FilterOpen:
	case change.FilterOpen:
		query = fmt.Sprintf("project:%s status:open", project)
	case changes.FilterClosedMerged:
	case change.FilterClosedMerged:
		// "status:closed" is equivalent to "(status:abandoned OR status:merged)".
		query = fmt.Sprintf("project:%s status:closed", project)
	case changes.FilterAll:
	case change.FilterAll:
		query = fmt.Sprintf("project:%s", project)
	}
	cs, _, err := s.cl.Changes.QueryChanges(&gerrit.QueryChangeOptions{
		QueryOptions: gerrit.QueryOptions{
			Query: []string{query},
@@ -61,16 +61,16 @@ func (s service) List(ctx context.Context, rs string, opt changes.ListOptions) (
		},
	})
	if err != nil {
		return nil, err
	}
	var is []changes.Change
	var is []change.Change
	for _, chg := range *cs {
		if chg.Status == "DRAFT" {
			continue
		}
		is = append(is, changes.Change{
		is = append(is, change.Change{
			ID:    uint64(chg.Number),
			State: state(chg.Status),
			Title: chg.Subject,
			//Labels: labels, // TODO.
			Author:    s.gerritUser(chg.Owner),
@@ -83,75 +83,75 @@ func (s service) List(ctx context.Context, rs string, opt changes.ListOptions) (
		return is[i].CreatedAt.After(is[j].CreatedAt)
	})
	return is, nil
}

func (s service) Count(_ context.Context, repo string, opt changes.ListOptions) (uint64, error) {
func (s service) Count(_ context.Context, repo string, opt change.ListOptions) (uint64, error) {
	// TODO.
	return 0, nil
}

func (s service) Get(ctx context.Context, _ string, id uint64) (changes.Change, error) {
func (s service) Get(ctx context.Context, _ string, id uint64) (change.Change, error) {
	chg, _, err := s.cl.Changes.GetChange(fmt.Sprint(id), &gerrit.ChangeOptions{
		AdditionalFields: []string{"DETAILED_ACCOUNTS", "ALL_REVISIONS"},
	})
	if err != nil {
		return changes.Change{}, err
		return change.Change{}, err
	}
	if chg.Status == "DRAFT" {
		return changes.Change{}, os.ErrNotExist
		return change.Change{}, os.ErrNotExist
	}
	return changes.Change{
	return change.Change{
		ID:        id,
		State:     state(chg.Status),
		Title:     chg.Subject,
		Author:    s.gerritUser(chg.Owner),
		CreatedAt: time.Time(chg.Created),
		Commits:   len(chg.Revisions),
	}, nil
}

func state(status string) changes.State {
func state(status string) change.State {
	switch status {
	case "NEW":
		return changes.OpenState
		return change.OpenState
	case "ABANDONED":
		return changes.ClosedState
		return change.ClosedState
	case "MERGED":
		return changes.MergedState
		return change.MergedState
	case "DRAFT":
		panic("not sure how to deal with DRAFT status")
	default:
		panic("unreachable")
	}
}

func (s service) ListCommits(ctx context.Context, _ string, id uint64) ([]changes.Commit, error) {
func (s service) ListCommits(ctx context.Context, _ string, id uint64) ([]change.Commit, error) {
	chg, _, err := s.cl.Changes.GetChange(fmt.Sprint(id), &gerrit.ChangeOptions{
		AdditionalFields: []string{"DETAILED_ACCOUNTS", "ALL_REVISIONS"},
		//AdditionalFields: []string{"ALL_REVISIONS", "ALL_COMMITS"}, // TODO: Consider using git committer/author instead...
	})
	if err != nil {
		return nil, err
	}
	if chg.Status == "DRAFT" {
		return nil, os.ErrNotExist
	}
	commits := make([]changes.Commit, len(chg.Revisions))
	commits := make([]change.Commit, len(chg.Revisions))
	for sha, r := range chg.Revisions {
		commits[r.Number-1] = changes.Commit{
		commits[r.Number-1] = change.Commit{
			SHA:     sha,
			Message: fmt.Sprintf("Patch Set %d", r.Number),
			// TODO: r.Uploader and r.Created describe the committer, not author.
			Author:     s.gerritUser(r.Uploader),
			AuthorTime: time.Time(r.Created),
		}
	}
	return commits, nil
}

func (s service) GetDiff(ctx context.Context, _ string, id uint64, opt *changes.GetDiffOptions) ([]byte, error) {
func (s service) GetDiff(ctx context.Context, _ string, id uint64, opt *change.GetDiffOptions) ([]byte, error) {
	switch opt {
	case nil:
		diff, _, err := s.cl.Changes.GetPatch(fmt.Sprint(id), "current", nil)
		if err != nil {
			return nil, err
@@ -241,11 +241,11 @@ func (s service) GetDiff(ctx context.Context, _ string, id uint64, opt *changes.
		}
		return []byte(diff), nil
	}
}

func (s service) ListTimeline(ctx context.Context, _ string, id uint64, opt *changes.ListTimelineOptions) ([]interface{}, error) {
func (s service) ListTimeline(ctx context.Context, _ string, id uint64, opt *change.ListTimelineOptions) ([]interface{}, error) {
	// TODO: Pagination. Respect opt.Start and opt.Length, if given.

	chg, _, err := s.cl.Changes.GetChangeDetail(fmt.Sprint(id), nil)
	if err != nil {
		return nil, err
@@ -254,11 +254,11 @@ func (s service) ListTimeline(ctx context.Context, _ string, id uint64, opt *cha
	if err != nil {
		return nil, err
	}
	var timeline []interface{}
	{
		timeline = append(timeline, changes.Comment{
		timeline = append(timeline, change.Comment{
			ID:        0,
			User:      s.gerritUser(chg.Owner),
			CreatedAt: time.Time(chg.Created),
			Body:      "", // THINK: Include commit message or no?
			Editable:  false,
@@ -266,14 +266,14 @@ func (s service) ListTimeline(ctx context.Context, _ string, id uint64, opt *cha
	}
	for idx, message := range chg.Messages {
		if strings.HasPrefix(message.Tag, "autogenerated:") {
			switch message.Tag[len("autogenerated:"):] {
			case "gerrit:merged":
				timeline = append(timeline, changes.TimelineItem{
				timeline = append(timeline, change.TimelineItem{
					Actor:     s.gerritUser(message.Author),
					CreatedAt: time.Time(message.Date),
					Payload: changes.MergedEvent{
					Payload: change.MergedEvent{
						CommitID: message.Message[46:86], // TODO: Make safer.
						RefName:  chg.Branch,
					},
				})
			}
@@ -281,32 +281,32 @@ func (s service) ListTimeline(ctx context.Context, _ string, id uint64, opt *cha
		}
		label, body, ok := parseMessage(message.Message)
		if !ok {
			continue
		}
		var state changes.ReviewState
		var state change.ReviewState
		switch label {
		default:
			state = changes.Commented
			state = change.Commented
		case "Code-Review+2":
			state = changes.Approved
			state = change.Approved
		case "Code-Review-2":
			state = changes.ChangesRequested
			state = change.ChangesRequested
		}
		var cs []changes.InlineComment
		var cs []change.InlineComment
		for file, comments := range *comments {
			for _, c := range comments {
				if time.Time(c.Updated).Equal(time.Time(message.Date)) {
					cs = append(cs, changes.InlineComment{
					cs = append(cs, change.InlineComment{
						File: file,
						Line: c.Line,
						Body: c.Message,
					})
				}
			}
		}
		timeline = append(timeline, changes.Review{
		timeline = append(timeline, change.Review{
			ID:        uint64(idx), // TODO: message.ID is not uint64; e.g., "bfba753d015916303152305cee7152ea7a112fe0".
			User:      s.gerritUser(message.Author),
			CreatedAt: time.Time(message.Date),
			State:     state,
			Body:      body,
githubapi/githubapi.go
@@ -1,29 +1,29 @@
// Package githubapi implements a read-only changes.Service using GitHub API clients.
// Package githubapi implements a read-only change.Service using GitHub API clients.
package githubapi

import (
	"context"
	"fmt"
	"log"
	"sort"
	"strings"

	"dmitri.shuralyov.com/changes"
	"dmitri.shuralyov.com/service/change"
	"github.com/google/go-github/github"
	"github.com/shurcooL/githubql"
	"github.com/shurcooL/issues"
	"github.com/shurcooL/notifications"
	"github.com/shurcooL/reactions"
	"github.com/shurcooL/users"
	ghusers "github.com/shurcooL/users/githubapi"
)

// NewService creates a GitHub-backed changes.Service using given GitHub clients.
// NewService creates a GitHub-backed change.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) (changes.Service, error) {
func NewService(clientV3 *github.Client, clientV4 *githubql.Client, notifications notifications.ExternalService) (change.Service, error) {
	users, err := ghusers.NewService(clientV3)
	if err != nil {
		return nil, err
	}
	currentUser, err := users.GetAuthenticated(context.Background())
@@ -49,23 +49,23 @@ type service struct {
}

// We use 0 as a special ID for the comment that is the PR description. This comment is edited differently.
const prDescriptionCommentID uint64 = 0

func (s service) List(ctx context.Context, rs string, opt changes.ListOptions) ([]changes.Change, error) {
func (s service) List(ctx context.Context, rs string, opt change.ListOptions) ([]change.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.Filter {
	case changes.FilterOpen:
	case change.FilterOpen:
		states = []githubql.PullRequestState{githubql.PullRequestStateOpen}
	case changes.FilterClosedMerged:
	case change.FilterClosedMerged:
		states = []githubql.PullRequestState{githubql.PullRequestStateClosed, githubql.PullRequestStateMerged}
	case changes.FilterAll:
	case change.FilterAll:
		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.Filter)
	}
@@ -98,20 +98,20 @@ func (s service) List(ctx context.Context, rs string, opt changes.ListOptions) (
	}
	err = s.clV4.Query(ctx, &q, variables)
	if err != nil {
		return nil, err
	}
	var is []changes.Change
	var is []change.Change
	for _, pr := range q.Repository.PullRequests.Nodes {
		var labels []issues.Label
		for _, l := range pr.Labels.Nodes {
			labels = append(labels, issues.Label{
				Name:  l.Name,
				Color: ghColor(l.Color),
			})
		}
		is = append(is, changes.Change{
		is = append(is, change.Change{
			ID:        pr.Number,
			State:     ghPRState(pr.State),
			Title:     pr.Title,
			Labels:    labels,
			Author:    ghActor(pr.Author),
@@ -120,23 +120,23 @@ func (s service) List(ctx context.Context, rs string, opt changes.ListOptions) (
		})
	}
	return is, nil
}

func (s service) Count(ctx context.Context, rs string, opt changes.ListOptions) (uint64, error) {
func (s service) Count(ctx context.Context, rs string, opt change.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.Filter {
	case changes.FilterOpen:
	case change.FilterOpen:
		states = []githubql.PullRequestState{githubql.PullRequestStateOpen}
	case changes.FilterClosedMerged:
	case change.FilterClosedMerged:
		states = []githubql.PullRequestState{githubql.PullRequestStateClosed, githubql.PullRequestStateMerged}
	case changes.FilterAll:
	case change.FilterAll:
		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.Filter)
	}
@@ -154,15 +154,15 @@ func (s service) Count(ctx context.Context, rs string, opt changes.ListOptions)
	}
	err = s.clV4.Query(ctx, &q, variables)
	return q.Repository.PullRequests.TotalCount, err
}

func (s service) Get(ctx context.Context, rs string, id uint64) (changes.Change, error) {
func (s service) Get(ctx context.Context, rs string, id uint64) (change.Change, error) {
	repo, err := ghRepoSpec(rs)
	if err != nil {
		// TODO: Map to 400 Bad Request HTTP error.
		return changes.Change{}, err
		return change.Change{}, err
	}
	var q struct {
		Repository struct {
			PullRequest struct {
				Number    uint64
@@ -181,11 +181,11 @@ func (s service) Get(ctx context.Context, rs string, id uint64) (changes.Change,
		"repositoryName":  githubql.String(repo.Repo),
		"prNumber":        githubql.Int(id),
	}
	err = s.clV4.Query(ctx, &q, variables)
	if err != nil {
		return changes.Change{}, err
		return change.Change{}, err
	}

	if s.currentUser.ID != 0 {
		// Mark as read.
		err = s.markRead(ctx, rs, id)
@@ -194,43 +194,43 @@ func (s service) Get(ctx context.Context, rs string, id uint64) (changes.Change,
		}
	}

	// TODO: Eliminate comment body properties from issues.Issue. It's missing increasingly more fields, like Edited, etc.
	pr := q.Repository.PullRequest
	return changes.Change{
	return change.Change{
		ID:        pr.Number,
		State:     ghPRState(pr.State),
		Title:     pr.Title,
		Author:    ghActor(pr.Author),
		CreatedAt: pr.CreatedAt.Time,
		Commits:   pr.Commits.TotalCount,
	}, nil
}

func (s service) ListCommits(ctx context.Context, rs string, id uint64) ([]changes.Commit, error) {
func (s service) ListCommits(ctx context.Context, rs string, id uint64) ([]change.Commit, error) {
	repo, err := ghRepoSpec(rs)
	if err != nil {
		// TODO: Map to 400 Bad Request HTTP error.
		return nil, err
	}
	cs, _, err := s.clV3.PullRequests.ListCommits(ctx, repo.Owner, repo.Repo, int(id), nil)
	if err != nil {
		return nil, err
	}
	var commits []changes.Commit
	var commits []change.Commit
	for _, c := range cs {
		commits = append(commits, changes.Commit{
		commits = append(commits, change.Commit{
			SHA:        *c.SHA,
			Message:    *c.Commit.Message,
			Author:     ghV3User(c.Author),
			AuthorTime: *c.Commit.Author.Date,
		})
	}
	return commits, nil
}

func (s service) GetDiff(ctx context.Context, rs string, id uint64, opt *changes.GetDiffOptions) ([]byte, error) {
func (s service) GetDiff(ctx context.Context, rs string, id uint64, opt *change.GetDiffOptions) ([]byte, error) {
	repo, err := ghRepoSpec(rs)
	if err != nil {
		// TODO: Map to 400 Bad Request HTTP error.
		return nil, err
	}
@@ -248,11 +248,11 @@ func (s service) GetDiff(ctx context.Context, rs string, id uint64, opt *changes
		}
		return []byte(diff), nil
	}
}

func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *changes.ListTimelineOptions) ([]interface{}, error) {
func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *change.ListTimelineOptions) ([]interface{}, error) {
	repo, err := ghRepoSpec(rs)
	if err != nil {
		// TODO: Map to 400 Bad Request HTTP error.
		return nil, err
	}
@@ -379,18 +379,18 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch
		pr := q.Repository.PullRequest
		reactions, err := s.reactions(pr.ReactionGroups)
		if err != nil {
			return timeline, err
		}
		var edited *changes.Edited
		var edited *change.Edited
		if pr.LastEditedAt != nil {
			edited = &changes.Edited{
			edited = &change.Edited{
				By: ghActor(*pr.Editor),
				At: pr.LastEditedAt.Time,
			}
		}
		timeline = append(timeline, changes.Comment{
		timeline = append(timeline, change.Comment{
			ID:        prDescriptionCommentID,
			User:      ghActor(pr.Author),
			CreatedAt: pr.PublishedAt.Time,
			Edited:    edited,
			Body:      string(pr.Body),
@@ -405,38 +405,38 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch
		comment := node.IssueComment
		reactions, err := s.reactions(comment.ReactionGroups)
		if err != nil {
			return timeline, err
		}
		var edited *changes.Edited
		var edited *change.Edited
		if comment.LastEditedAt != nil {
			edited = &changes.Edited{
			edited = &change.Edited{
				By: ghActor(*comment.Editor),
				At: comment.LastEditedAt.Time,
			}
		}
		timeline = append(timeline, changes.Comment{
		timeline = append(timeline, change.Comment{
			ID:        comment.DatabaseID,
			User:      ghActor(comment.Author),
			CreatedAt: comment.PublishedAt.Time,
			Edited:    edited,
			Body:      comment.Body,
			Reactions: reactions,
			Editable:  comment.ViewerCanUpdate,
		})
	}
	for _, review := range q.Repository.PullRequest.Reviews.Nodes {
		var edited *changes.Edited
		var edited *change.Edited
		if review.LastEditedAt != nil {
			edited = &changes.Edited{
			edited = &change.Edited{
				By: ghActor(*review.Editor),
				At: review.LastEditedAt.Time,
			}
		}
		var cs []changes.InlineComment
		var cs []change.InlineComment
		for _, comment := range review.Comments.Nodes {
			cs = append(cs, changes.InlineComment{
			cs = append(cs, change.InlineComment{
				File: comment.Path,
				Line: comment.OriginalPosition,
				Body: comment.Body,
			})
		}
@@ -444,11 +444,11 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch
			if cs[i].File == cs[j].File {
				return cs[i].Line < cs[j].Line
			}
			return cs[i].File < cs[j].File
		})
		timeline = append(timeline, changes.Review{
		timeline = append(timeline, change.Review{
			ID:        review.DatabaseID,
			User:      ghActor(review.Author),
			CreatedAt: review.PublishedAt.Time,
			Edited:    edited,
			State:     ghPRReviewState(review.State),
@@ -456,71 +456,71 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch
			Editable:  review.ViewerCanUpdate,
			Comments:  cs,
		})
	}
	for _, event := range q.Repository.PullRequest.Timeline.Nodes {
		e := changes.TimelineItem{
		//ID: 0, // TODO.
		e := change.TimelineItem{
			//ID: 0, // TODO.
		}
		switch event.Typename {
		case "ClosedEvent":
			e.Actor = ghActor(event.ClosedEvent.Actor)
			e.CreatedAt = event.ClosedEvent.CreatedAt.Time
			e.Payload = changes.ClosedEvent{}
			e.Payload = change.ClosedEvent{}
		case "ReopenedEvent":
			e.Actor = ghActor(event.ReopenedEvent.Actor)
			e.CreatedAt = event.ReopenedEvent.CreatedAt.Time
			e.Payload = changes.ReopenedEvent{}
			e.Payload = change.ReopenedEvent{}
		case "RenamedTitleEvent":
			e.Actor = ghActor(event.RenamedTitleEvent.Actor)
			e.CreatedAt = event.RenamedTitleEvent.CreatedAt.Time
			e.Payload = changes.RenamedEvent{
			e.Payload = change.RenamedEvent{
				From: event.RenamedTitleEvent.PreviousTitle,
				To:   event.RenamedTitleEvent.CurrentTitle,
			}
		case "LabeledEvent":
			e.Actor = ghActor(event.LabeledEvent.Actor)
			e.CreatedAt = event.LabeledEvent.CreatedAt.Time
			e.Payload = changes.LabeledEvent{
			e.Payload = change.LabeledEvent{
				Label: issues.Label{
					Name:  event.LabeledEvent.Label.Name,
					Color: ghColor(event.LabeledEvent.Label.Color),
				},
			}
		case "UnlabeledEvent":
			e.Actor = ghActor(event.UnlabeledEvent.Actor)
			e.CreatedAt = event.UnlabeledEvent.CreatedAt.Time
			e.Payload = changes.UnlabeledEvent{
			e.Payload = change.UnlabeledEvent{
				Label: issues.Label{
					Name:  event.UnlabeledEvent.Label.Name,
					Color: ghColor(event.UnlabeledEvent.Label.Color),
				},
			}
		case "ReviewRequestedEvent":
			e.Actor = ghActor(event.ReviewRequestedEvent.Actor)
			e.CreatedAt = event.ReviewRequestedEvent.CreatedAt.Time
			e.Payload = changes.ReviewRequestedEvent{
			e.Payload = change.ReviewRequestedEvent{
				RequestedReviewer: ghActor(event.ReviewRequestedEvent.RequestedReviewer.githubqlActor),
			}
		case "ReviewRequestRemovedEvent":
			e.Actor = ghActor(event.ReviewRequestRemovedEvent.Actor)
			e.CreatedAt = event.ReviewRequestRemovedEvent.CreatedAt.Time
			e.Payload = changes.ReviewRequestRemovedEvent{
			e.Payload = change.ReviewRequestRemovedEvent{
				RequestedReviewer: ghActor(event.ReviewRequestRemovedEvent.RequestedReviewer.githubqlActor),
			}
		case "MergedEvent":
			e.Actor = ghActor(event.MergedEvent.Actor)
			e.CreatedAt = event.MergedEvent.CreatedAt.Time
			e.Payload = changes.MergedEvent{
			e.Payload = change.MergedEvent{
				CommitID:      event.MergedEvent.Commit.OID,
				CommitHTMLURL: event.MergedEvent.Commit.URL,
				RefName:       event.MergedEvent.MergeRefName,
			}
		case "HeadRefDeletedEvent":
			e.Actor = ghActor(event.HeadRefDeletedEvent.Actor)
			e.CreatedAt = event.HeadRefDeletedEvent.CreatedAt.Time
			e.Payload = changes.DeletedEvent{
			e.Payload = change.DeletedEvent{
				Type: "branch",
				Name: event.HeadRefDeletedEvent.HeadRefName,
			}
		// TODO: Wait for GitHub to add support.
		//case "CommentDeletedEvent":
@@ -638,33 +638,33 @@ var ghost = users.User{
	Login:     "ghost",
	AvatarURL: "https://avatars3.githubusercontent.com/u/10137?v=4",
	HTMLURL:   "https://github.com/ghost",
}

// ghPRState converts a GitHub PullRequestState to changes.State.
func ghPRState(state githubql.PullRequestState) changes.State {
// ghPRState converts a GitHub PullRequestState to change.State.
func ghPRState(state githubql.PullRequestState) change.State {
	switch state {
	case githubql.PullRequestStateOpen:
		return changes.OpenState
		return change.OpenState
	case githubql.PullRequestStateClosed:
		return changes.ClosedState
		return change.ClosedState
	case githubql.PullRequestStateMerged:
		return changes.MergedState
		return change.MergedState
	default:
		panic("unreachable")
	}
}

// ghPRReviewState converts a GitHub PullRequestReviewState to changes.ReviewState.
func ghPRReviewState(state githubql.PullRequestReviewState) changes.ReviewState {
// ghPRReviewState converts a GitHub PullRequestReviewState to change.ReviewState.
func ghPRReviewState(state githubql.PullRequestReviewState) change.ReviewState {
	switch state {
	case githubql.PullRequestReviewStateApproved:
		return changes.Approved
		return change.Approved
	case githubql.PullRequestReviewStateCommented:
		return changes.Commented
		return change.Commented
	case githubql.PullRequestReviewStateChangesRequested:
		return changes.ChangesRequested
		return change.ChangesRequested
	case githubql.PullRequestReviewStatePending:
		panic("PullRequestReviewStatePending not implemented") // TODO.
	case githubql.PullRequestReviewStateDismissed:
		panic("PullRequestReviewStateDismissed not implemented") // TODO.
	default:
maintner/maintner.go
@@ -1,53 +1,53 @@
// Package maintner implements a read-only changes.Service using
// Package maintner implements a read-only change.Service using
// a x/build/maintner corpus that serves Gerrit changes.
package maintner

import (
	"context"
	"fmt"
	"log"
	"sort"
	"strings"

	"dmitri.shuralyov.com/changes"
	"dmitri.shuralyov.com/service/change"
	"github.com/shurcooL/users"
	"golang.org/x/build/maintner"
)

// NewService creates an changes.Service backed with the given corpus.
// NewService creates an change.Service backed with the given corpus.
// However, it serves Gerrit changes, not GitHub issues.
func NewService(corpus *maintner.Corpus) changes.Service {
func NewService(corpus *maintner.Corpus) change.Service {
	return service{
		c: corpus,
	}
}

type service struct {
	c *maintner.Corpus
}

func (s service) List(ctx context.Context, repo string, opt changes.ListOptions) ([]changes.Change, error) {
func (s service) List(ctx context.Context, repo string, opt change.ListOptions) ([]change.Change, error) {
	// TODO: Pagination. Respect opt.Start and opt.Length, if given.

	var is []changes.Change
	var is []change.Change

	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.Filter == changes.FilterOpen && state != changes.OpenState:
		case opt.Filter == change.FilterOpen && state != change.OpenState:
			return nil
		case opt.Filter == changes.FilterClosedMerged && !(state == changes.ClosedState || state == changes.MergedState):
		case opt.Filter == change.FilterClosedMerged && !(state == change.ClosedState || state == change.MergedState):
			return nil
		}

		is = append(is, changes.Change{
		is = append(is, change.Change{
			ID:    uint64(cl.Number),
			State: state,
			Title: firstParagraph(cl.Commit.Msg),
			//Labels: labels, // TODO.
			Author:    gerritUser(cl.Commit.Author),
@@ -67,23 +67,23 @@ func (s service) List(ctx context.Context, repo string, opt changes.ListOptions)
	})

	return is, nil
}

func (s service) Count(_ context.Context, repo string, opt changes.ListOptions) (uint64, error) {
func (s service) Count(_ context.Context, repo string, opt change.ListOptions) (uint64, error) {
	var count uint64

	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.Filter == changes.FilterOpen && state != changes.OpenState:
		case opt.Filter == change.FilterOpen && state != change.OpenState:
			return nil
		case opt.Filter == changes.FilterClosedMerged && !(state == changes.ClosedState || state == changes.MergedState):
		case opt.Filter == change.FilterClosedMerged && !(state == change.ClosedState || state == change.MergedState):
			return nil
		}

		count++

@@ -94,40 +94,40 @@ func (s service) Count(_ context.Context, repo string, opt changes.ListOptions)
	}

	return count, nil
}

func (s service) Get(ctx context.Context, _ string, id uint64) (changes.Change, error) {
func (s service) Get(ctx context.Context, _ string, id uint64) (change.Change, error) {
	// TODO.
	return changes.Change{}, fmt.Errorf("Get: not implemented")
	return change.Change{}, fmt.Errorf("Get: not implemented")
}

func (s service) ListCommits(ctx context.Context, _ string, id uint64) ([]changes.Commit, error) {
func (s service) ListCommits(ctx context.Context, _ string, id uint64) ([]change.Commit, error) {
	return nil, fmt.Errorf("ListCommits: not implemented")
}

func (s service) GetDiff(ctx context.Context, _ string, id uint64, opt *changes.GetDiffOptions) ([]byte, error) {
func (s service) GetDiff(ctx context.Context, _ string, id uint64, opt *change.GetDiffOptions) ([]byte, error) {
	// TODO.
	return nil, fmt.Errorf("GetDiff: not implemented")
}

func state(status string) changes.State {
func state(status string) change.State {
	switch status {
	case "new":
		return changes.OpenState
		return change.OpenState
	case "abandoned":
		return changes.ClosedState
		return change.ClosedState
	case "merged":
		return changes.MergedState
		return change.MergedState
	case "draft":
		panic("not sure how to deal with draft status")
	default:
		panic(fmt.Errorf("unrecognized status %q", status))
	}
}

func (s service) ListTimeline(ctx context.Context, _ string, id uint64, opt *changes.ListTimelineOptions) ([]interface{}, error) {
func (s service) ListTimeline(ctx context.Context, _ string, id uint64, opt *change.ListTimelineOptions) ([]interface{}, error) {
	// TODO.
	return nil, fmt.Errorf("ListTimeline: not implemented")
}

func gerritUser(user *maintner.GitPerson) users.User {
timeline.go
@@ -1,6 +1,6 @@
package changes
package change

import (
	"time"

	"github.com/shurcooL/issues"