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

Rename package changes to change.
dmitshur committed 2 years ago commit 852ae6208008be4cc1eb4ec6c688ab250e31cd3d
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"