dmitri.shuralyov.com/app/changes/...

Update import paths.
dmitshur committed 6 years ago commit 30f9fab1751529ac3d77ad7bab604a70079e3b6d
Collapse all
_data/style.css
@@ -83,11 +83,11 @@ div.commit-message.list-entry-border {
code {
	font-family: "Go Mono";
	font-size: 12px;
}

/* Needed in changesapp only because of something in parent containers... */
/* Needed in changes only because of something in parent containers... */
.markdown-body {
	word-break: break-word;
}

/* GFM style overrides. */
assets/assets.go
@@ -9,14 +9,14 @@ import (

	"github.com/shurcooL/go/gopherjs_http"
	"github.com/shurcooL/httpfs/union"
)

// Assets contains assets for changesapp.
// Assets contains assets for changes.
var Assets = union.New(map[string]http.FileSystem{
	"/script.js": gopherjs_http.Package("dmitri.shuralyov.com/changes/app/frontend"),
	"/assets":    http.Dir(importPathToDir("dmitri.shuralyov.com/changes/app/_data")),
	"/script.js": gopherjs_http.Package("dmitri.shuralyov.com/app/changes/frontend"),
	"/assets":    http.Dir(importPathToDir("dmitri.shuralyov.com/app/changes/_data")),
})

func importPathToDir(importPath string) string {
	p, err := build.Import(importPath, "", build.FindOnly)
	if err != nil {
assets/doc.go
@@ -1,4 +1,4 @@
//go:generate vfsgendev -source="dmitri.shuralyov.com/changes/app/assets".Assets
//go:generate vfsgendev -source="dmitri.shuralyov.com/app/changes/assets".Assets

// Package assets contains assets for changesapp.
// Package assets contains assets for changes.
package assets
cmd/changesdev/main.go
@@ -30,15 +30,15 @@ import (
	"log"
	"net/http"
	"os"
	"strings"

	"dmitri.shuralyov.com/changes"
	"dmitri.shuralyov.com/changes/app"
	"dmitri.shuralyov.com/changes/fs"
	"dmitri.shuralyov.com/changes/gerritapi"
	"dmitri.shuralyov.com/changes/githubapi"
	"dmitri.shuralyov.com/app/changes"
	"dmitri.shuralyov.com/service/change"
	"dmitri.shuralyov.com/service/change/fs"
	"dmitri.shuralyov.com/service/change/gerritapi"
	"dmitri.shuralyov.com/service/change/githubapi"
	"github.com/andygrunwald/go-gerrit"
	"github.com/google/go-github/github"
	"github.com/gregjones/httpcache"
	"github.com/shurcooL/githubql"
	"github.com/shurcooL/httpgzip"
@@ -49,12 +49,12 @@ import (
var httpFlag = flag.String("http", ":8080", "Listen for HTTP connections on this address.")

func main() {
	flag.Parse()

	var service changes.Service
	switch 3 {
	var service change.Service
	switch 0 {
	case 0:
		// Perform GitHub API authentication with provided token.
		token := os.Getenv("CHANGES_GITHUB_TOKEN")
		if token == "" {
			log.Fatalln("CHANGES_GITHUB_TOKEN env var is empty")
@@ -96,11 +96,11 @@ func main() {

	case 3:
		service = &fs.Service{}
	}

	changesOpt := changesapp.Options{
	changesOpt := changes.Options{
		HeadPre: `<style type="text/css">
	body {
		margin: 20px;
		font-family: Go;
		font-size: 14px;
@@ -114,13 +114,12 @@ func main() {
		border: solid #d2d2d2 1px;
		background-color: #fff;
		box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
	}
</style>`,
		DisableReactions: true,
	}
	changesApp := changesapp.New(service, nil, changesOpt)
	changesApp := changes.New(service, nil, changesOpt)

	issuesHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		prefixLen := len("/changes")
		if prefix := req.URL.Path[:prefixLen]; req.URL.Path == prefix+"/" {
			baseURL := prefix
@@ -132,20 +131,21 @@ func main() {
		}
		req.URL.Path = req.URL.Path[prefixLen:]
		if req.URL.Path == "" {
			req.URL.Path = "/"
		}
		//req = req.WithContext(context.WithValue(req.Context(), changesapp.RepoSpecContextKey, "github.com/google/go-github"))
		//req = req.WithContext(context.WithValue(req.Context(), changesapp.RepoSpecContextKey, "github.com/dustin/go-humanize"))
		//req = req.WithContext(context.WithValue(req.Context(), changesapp.RepoSpecContextKey, "github.com/neugram/ng"))
		//req = req.WithContext(context.WithValue(req.Context(), changesapp.RepoSpecContextKey, "github.com/golang/scratch"))
		//req = req.WithContext(context.WithValue(req.Context(), changesapp.RepoSpecContextKey, "go.googlesource.com/go"))
		//req = req.WithContext(context.WithValue(req.Context(), changesapp.RepoSpecContextKey, "go.googlesource.com/tools"))
		//req = req.WithContext(context.WithValue(req.Context(), changesapp.RepoSpecContextKey, "go.googlesource.com/build"))
		//req = req.WithContext(context.WithValue(req.Context(), changesapp.RepoSpecContextKey, "upspin.googlesource.com/upspin"))
		req = req.WithContext(context.WithValue(req.Context(), changesapp.RepoSpecContextKey, "dmitri.shuralyov.com/font/woff2"))
		req = req.WithContext(context.WithValue(req.Context(), changesapp.BaseURIContextKey, "/changes"))
		//req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, "github.com/google/go-github"))
		//req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, "github.com/dustin/go-humanize"))
		//req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, "github.com/neugram/ng"))
		//req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, "github.com/golang/scratch"))
		req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, "github.com/bradleyfalzon/ghinstallation"))
		//req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, "go.googlesource.com/go"))
		//req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, "go.googlesource.com/tools"))
		//req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, "go.googlesource.com/build"))
		//req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, "upspin.googlesource.com/upspin"))
		//req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, "dmitri.shuralyov.com/font/woff2"))
		req = req.WithContext(context.WithValue(req.Context(), changes.BaseURIContextKey, "/changes"))
		changesApp.ServeHTTP(w, req)
	})
	http.Handle("/changes", issuesHandler)
	http.Handle("/changes/", issuesHandler)

commits.go
@@ -1,12 +1,12 @@
package changesapp
package changes

import (
	"strings"

	"dmitri.shuralyov.com/changes"
	"dmitri.shuralyov.com/changes/app/component"
	"dmitri.shuralyov.com/app/changes/component"
	"dmitri.shuralyov.com/service/change"
	homecomponent "github.com/shurcooL/home/component"
	"github.com/shurcooL/htmlg"
	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
)
@@ -32,11 +32,11 @@ func (cs Commits) Render() []*html.Node {
	}
	return []*html.Node{htmlg.DivClass("list-entry-border", nodes...)}
}

type Commit struct {
	changes.Commit
	change.Commit
}

func (c Commit) Render() []*html.Node {
	div := &html.Node{
		Type: html.ElementNode, Data: atom.Div.String(),
component/changes.go
@@ -1,11 +1,11 @@
package component

import (
	"fmt"

	"dmitri.shuralyov.com/changes"
	"dmitri.shuralyov.com/service/change"
	"github.com/shurcooL/htmlg"
	issuescomponent "github.com/shurcooL/issuesapp/component"
	"github.com/shurcooL/octiconssvg"
	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
@@ -13,11 +13,11 @@ import (

// Changes is a component that displays a page of changes,
// with a navigation bar on top.
type Changes struct {
	IssuesNav IssuesNav
	Filter    changes.StateFilter
	Filter    change.StateFilter
	Entries   []ChangeEntry
}

func (i Changes) Render() []*html.Node {
	// TODO: Make this much nicer.
@@ -40,15 +40,15 @@ func (i Changes) Render() []*html.Node {
		div := &html.Node{
			Type: html.ElementNode, Data: atom.Div.String(),
			Attr: []html.Attribute{{Key: atom.Style.String(), Val: "text-align: center; margin-top: 80px; margin-bottom: 80px;"}},
		}
		switch i.Filter {
		case changes.FilterOpen:
		case change.FilterOpen:
			div.AppendChild(htmlg.Text("There are no open changes."))
		case changes.FilterClosedMerged:
		case change.FilterClosedMerged:
			div.AppendChild(htmlg.Text("There are no closed/merged changes."))
		case changes.FilterAll:
		case change.FilterAll:
			div.AppendChild(htmlg.Text("There are no changes."))
		}
		ns = append(ns, div)
	}

@@ -56,11 +56,11 @@ func (i Changes) Render() []*html.Node {
	return []*html.Node{div}
}

// ChangeEntry is an entry within the list of changes.
type ChangeEntry struct {
	Change changes.Change
	Change change.Change
	Unread bool // Unread indicates whether the change contains unread notifications for authenticated user.

	// TODO, THINK: This is router details, can it be factored out or cleaned up?
	BaseURI string
}
component/component.go
@@ -3,11 +3,11 @@ package component

import (
	"fmt"
	"time"

	"dmitri.shuralyov.com/changes"
	"dmitri.shuralyov.com/service/change"
	"github.com/dustin/go-humanize"
	"github.com/shurcooL/htmlg"
	issuescomponent "github.com/shurcooL/issuesapp/component"
	"github.com/shurcooL/octiconssvg"
	"github.com/shurcooL/users"
@@ -15,11 +15,11 @@ import (
	"golang.org/x/net/html/atom"
)

// Event is an event component.
type Event struct {
	Event changes.TimelineItem
	Event change.TimelineItem
}

func (e Event) Render() []*html.Node {
	// TODO: Make this much nicer.
	// <div class="list-entry event event-{{.Type}}">
@@ -49,41 +49,41 @@ func (e Event) icon() *html.Node {
		icon            *html.Node
		color           = "#767676"
		backgroundColor = "#f3f3f3"
	)
	switch p := e.Event.Payload.(type) {
	case changes.ClosedEvent:
	case change.ClosedEvent:
		icon = octiconssvg.CircleSlash()
		color, backgroundColor = "#fff", "#bd2c00"
	case changes.ReopenedEvent:
	case change.ReopenedEvent:
		icon = octiconssvg.PrimitiveDot()
		color, backgroundColor = "#fff", "#6cc644"
	case changes.RenamedEvent:
	case change.RenamedEvent:
		icon = octiconssvg.Pencil()
	case changes.LabeledEvent, changes.UnlabeledEvent:
	case change.LabeledEvent, change.UnlabeledEvent:
		icon = octiconssvg.Tag()
	case changes.ReviewRequestedEvent:
	case change.ReviewRequestedEvent:
		icon = octiconssvg.Eye()
	case changes.ReviewRequestRemovedEvent:
	case change.ReviewRequestRemovedEvent:
		icon = octiconssvg.X()
	case changes.MergedEvent:
	case change.MergedEvent:
		icon = octiconssvg.GitMerge()
		color, backgroundColor = "#fff", "#6f42c1"
	case changes.DeletedEvent:
	case change.DeletedEvent:
		switch p.Type {
		case "branch":
			icon = octiconssvg.GitBranch()
			color, backgroundColor = "#fff", "#767676"
		case "comment":
			icon = octiconssvg.X()
		default:
			panic("unreachable")
		}
	case changes.ApprovedEvent:
	case change.ApprovedEvent:
		icon = octiconssvg.Check()
		color, backgroundColor = "#fff", "#6cc644"
	case changes.ChangesRequestedEvent:
	case change.ChangesRequestedEvent:
		icon = octiconssvg.X()
		color, backgroundColor = "#fff", "#bd2c00"
	default:
		icon = octiconssvg.PrimitiveDot()
	}
@@ -97,46 +97,46 @@ func (e Event) icon() *html.Node {
	}
}

func (e Event) text() []*html.Node {
	switch p := e.Event.Payload.(type) {
	case changes.ClosedEvent:
	case change.ClosedEvent:
		return []*html.Node{htmlg.Text("closed this")}
	case changes.ReopenedEvent:
	case change.ReopenedEvent:
		return []*html.Node{htmlg.Text("reopened this")}
	case changes.RenamedEvent:
	case change.RenamedEvent:
		return []*html.Node{htmlg.Text("changed the title from "), htmlg.Strong(p.From), htmlg.Text(" to "), htmlg.Strong(p.To)}
	case changes.LabeledEvent:
	case change.LabeledEvent:
		var ns []*html.Node
		ns = append(ns, htmlg.Text("added the "))
		ns = append(ns, issuescomponent.Label{Label: p.Label}.Render()...)
		ns = append(ns, htmlg.Text(" label"))
		return ns
	case changes.UnlabeledEvent:
	case change.UnlabeledEvent:
		var ns []*html.Node
		ns = append(ns, htmlg.Text("removed the "))
		ns = append(ns, issuescomponent.Label{Label: p.Label}.Render()...)
		ns = append(ns, htmlg.Text(" label"))
		return ns
	case changes.ReviewRequestedEvent:
	case change.ReviewRequestedEvent:
		ns := []*html.Node{htmlg.Text("requested a review from ")}
		ns = append(ns, Avatar{User: p.RequestedReviewer, Size: 16, inline: true}.Render()...)
		ns = append(ns, User{p.RequestedReviewer}.Render()...)
		return ns
	case changes.ReviewRequestRemovedEvent:
	case change.ReviewRequestRemovedEvent:
		ns := []*html.Node{htmlg.Text("removed the review request from ")}
		ns = append(ns, Avatar{User: p.RequestedReviewer, Size: 16, inline: true}.Render()...)
		ns = append(ns, User{p.RequestedReviewer}.Render()...)
		return ns
	case changes.MergedEvent:
	case change.MergedEvent:
		var ns []*html.Node
		ns = append(ns, htmlg.Text("merged commit "))
		ns = append(ns, htmlg.Strong(p.CommitID)) // TODO: Code{}, use CommitHTMLURL.
		ns = append(ns, htmlg.Text(" into "))
		ns = append(ns, htmlg.Strong(p.RefName)) // TODO: Code{}.
		return ns
	case changes.DeletedEvent:
	case change.DeletedEvent:
		switch p.Type {
		case "branch":
			var ns []*html.Node
			ns = append(ns, htmlg.Text("deleted the "))
			ns = append(ns, htmlg.Strong(p.Name)) // TODO: Code{}.
@@ -145,23 +145,23 @@ func (e Event) text() []*html.Node {
		case "comment":
			return []*html.Node{htmlg.Text("deleted a comment")}
		default:
			panic("unreachable")
		}
	case changes.ApprovedEvent:
	case change.ApprovedEvent:
		return []*html.Node{htmlg.Text("approved this change")}
	case changes.ChangesRequestedEvent:
	case change.ChangesRequestedEvent:
		return []*html.Node{htmlg.Text("requested changes")}
	default:
		return []*html.Node{htmlg.Text("unknown event")} // TODO: See if this is optimal.
	}
}

// ChangeStateBadge is a component that displays the state of a change
// with a badge, who opened it, and when it was opened.
type ChangeStateBadge struct {
	Change changes.Change
	Change change.Change
}

func (i ChangeStateBadge) Render() []*html.Node {
	var ns []*html.Node
	ns = append(ns, ChangeBadge{State: i.Change.State}.Render()...)
@@ -178,29 +178,29 @@ func (i ChangeStateBadge) Render() []*html.Node {
	return ns
}

// ChangeBadge is a change badge, displaying the change's state.
type ChangeBadge struct {
	State changes.State
	State change.State
}

func (cb ChangeBadge) Render() []*html.Node {
	var (
		icon  *html.Node
		text  string
		color string
	)
	switch cb.State {
	case changes.OpenState:
	case change.OpenState:
		icon = octiconssvg.GitPullRequest()
		text = "Open"
		color = "#6cc644"
	case changes.ClosedState:
	case change.ClosedState:
		icon = octiconssvg.GitPullRequest()
		text = "Closed"
		color = "#bd2c00"
	case changes.MergedState:
	case change.MergedState:
		icon = octiconssvg.GitMerge()
		text = "Merged"
		color = "#6f42c1"
	default:
		return []*html.Node{htmlg.Text(string(cb.State))}
@@ -225,11 +225,11 @@ background-color: ` + color + `;`,
	return []*html.Node{span}
}

// ChangeIcon is a change icon, displaying the change's state.
type ChangeIcon struct {
	State changes.State
	State change.State
}

func (ii ChangeIcon) Render() []*html.Node {
	// TODO: Make this much nicer.
	// {{if eq . "open"}}
@@ -240,17 +240,17 @@ func (ii ChangeIcon) Render() []*html.Node {
	var (
		icon  *html.Node
		color string
	)
	switch ii.State {
	case changes.OpenState:
	case change.OpenState:
		icon = octiconssvg.GitPullRequest()
		color = "#6cc644"
	case changes.ClosedState:
	case change.ClosedState:
		icon = octiconssvg.GitPullRequest()
		color = "#bd2c00"
	case changes.MergedState:
	case change.MergedState:
		icon = octiconssvg.GitMerge()
		color = "#6f42c1"
	}
	span := &html.Node{
		Type: html.ElementNode, Data: atom.Span.String(),
display.go
@@ -1,17 +1,17 @@
package changesapp
package changes

import (
	"bytes"
	"fmt"
	"html/template"
	"sort"
	"strings"
	"time"

	"dmitri.shuralyov.com/changes"
	"dmitri.shuralyov.com/changes/app/component"
	"dmitri.shuralyov.com/app/changes/component"
	"dmitri.shuralyov.com/service/change"
	"github.com/shurcooL/highlight_diff"
	"github.com/shurcooL/htmlg"
	"github.com/shurcooL/users"
	"github.com/sourcegraph/annotate"
	"golang.org/x/net/html"
@@ -25,41 +25,41 @@ type timelineItem struct {
	TimelineItem interface{}
}

func (i timelineItem) TemplateName() string {
	switch i.TimelineItem.(type) {
	case changes.Comment:
	case change.Comment:
		return "comment"
	case changes.Review:
	case change.Review:
		return "review"
	case changes.TimelineItem:
	case change.TimelineItem:
		return "event"
	default:
		panic(fmt.Errorf("unknown item type %T", i.TimelineItem))
	}
}

func (i timelineItem) CreatedAt() time.Time {
	switch i := i.TimelineItem.(type) {
	case changes.Comment:
	case change.Comment:
		return i.CreatedAt
	case changes.Review:
	case change.Review:
		return i.CreatedAt
	case changes.TimelineItem:
	case change.TimelineItem:
		return i.CreatedAt
	default:
		panic(fmt.Errorf("unknown item type %T", i))
	}
}

func (i timelineItem) ID() uint64 {
	switch i := i.TimelineItem.(type) {
	case changes.Comment:
	case change.Comment:
		return i.ID
	case changes.Review:
	case change.Review:
		return i.ID
	case changes.TimelineItem:
	case change.TimelineItem:
		return i.ID
	default:
		panic(fmt.Errorf("unknown item type %T", i))
	}
}
doc.go
@@ -1,2 +1,2 @@
// Package changesapp is a web frontend for a changes service.
package changesapp
// Package changes is a web frontend for a change tracking service.
package changes
errorhandler.go
@@ -1,6 +1,6 @@
package changesapp
package changes

import (
	"context"
	"errors"
	"fmt"
frontend/main.go
@@ -1,6 +1,6 @@
// frontend script for changesapp.
// frontend script for changes.
//
// It's a Go package meant to be compiled with GOARCH=js
// and executed in a browser, where the DOM is available.
package main

main.go
@@ -1,6 +1,6 @@
package changesapp
package changes

import (
	"bytes"
	"context"
	"encoding/json"
@@ -15,14 +15,14 @@ import (
	"sort"
	"strconv"
	"strings"
	"time"

	"dmitri.shuralyov.com/changes"
	"dmitri.shuralyov.com/changes/app/assets"
	"dmitri.shuralyov.com/changes/app/common"
	"dmitri.shuralyov.com/changes/app/component"
	"dmitri.shuralyov.com/app/changes/assets"
	"dmitri.shuralyov.com/app/changes/common"
	"dmitri.shuralyov.com/app/changes/component"
	"dmitri.shuralyov.com/service/change"
	"github.com/dustin/go-humanize"
	"github.com/shurcooL/github_flavored_markdown"
	"github.com/shurcooL/htmlg"
	"github.com/shurcooL/httperror"
	"github.com/shurcooL/httpfs/html/vfstemplate"
@@ -34,11 +34,11 @@ import (
	"github.com/shurcooL/users"
	"golang.org/x/net/html"
	"sourcegraph.com/sourcegraph/go-diff/diff"
)

// TODO: Find a better way for changesapp to be able to ensure registration of a top-level route:
// TODO: Find a better way for changes to be able to ensure registration of a top-level route:
//
// 	emojisHandler := httpgzip.FileServer(emojis.Assets, httpgzip.FileServerOptions{ServeError: httpgzip.Detailed})
// 	http.Handle("/emojis/", http.StripPrefix("/emojis", emojisHandler))
//
// So that it can depend on it.
@@ -50,18 +50,18 @@ import (
//
// In order to serve HTTP requests, the returned http.Handler expects each incoming
// request to have 2 parameters provided to it via RepoSpecContextKey and BaseURIContextKey
// context keys. For example:
//
// 	changesApp := changesapp.New(...)
// 	changesApp := changes.New(...)
//
// 	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// 		req = req.WithContext(context.WithValue(req.Context(), changesapp.RepoSpecContextKey, string(...)))
// 		req = req.WithContext(context.WithValue(req.Context(), changesapp.BaseURIContextKey, string(...)))
// 		req = req.WithContext(context.WithValue(req.Context(), changes.RepoSpecContextKey, string(...)))
// 		req = req.WithContext(context.WithValue(req.Context(), changes.BaseURIContextKey, string(...)))
// 		changesApp.ServeHTTP(w, req)
// 	})
func New(service changes.Service, users users.Service, opt Options) http.Handler {
func New(service change.Service, users users.Service, opt Options) http.Handler {
	static, err := loadTemplates(common.State{}, opt.BodyPre)
	if err != nil {
		log.Fatalln("loadTemplates failed:", err)
	}
	h := handler{
@@ -98,14 +98,14 @@ type Options struct {

	// BodyTop provides components to include on top of <body> of page rendered for req. It can be nil.
	BodyTop func(*http.Request, common.State) ([]htmlg.Component, error)
}

// handler handles all requests to changesapp. It acts like a request multiplexer,
// handler handles all requests to changes. It acts like a request multiplexer,
// choosing from various endpoints and parsing the repository ID from URL.
type handler struct {
	is changes.Service
	is change.Service
	us users.Service // May be nil if there's no users service.

	assetsFileServer http.Handler
	gfmFileServer    http.Handler

@@ -115,14 +115,14 @@ type handler struct {
	Options
}

func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) error {
	if _, ok := req.Context().Value(RepoSpecContextKey).(string); !ok {
		return fmt.Errorf("request to %v doesn't have changesapp.RepoSpecContextKey context key set", req.URL.Path)
		return fmt.Errorf("request to %v doesn't have changes.RepoSpecContextKey context key set", req.URL.Path)
	}
	if _, ok := req.Context().Value(BaseURIContextKey).(string); !ok {
		return fmt.Errorf("request to %v doesn't have changesapp.BaseURIContextKey context key set", req.URL.Path)
		return fmt.Errorf("request to %v doesn't have changes.BaseURIContextKey context key set", req.URL.Path)
	}

	// Handle "/assets/gfm/...".
	if strings.HasPrefix(req.URL.Path, "/assets/gfm/") {
		req = stripPrefix(req, len("/assets/gfm"))
@@ -192,19 +192,19 @@ func (h *handler) ChangesHandler(w http.ResponseWriter, req *http.Request) error
	}
	filter, err := stateFilter(req.URL.Query())
	if err != nil {
		return httperror.BadRequest{Err: err}
	}
	is, err := h.is.List(req.Context(), state.RepoSpec, changes.ListOptions{Filter: filter})
	is, err := h.is.List(req.Context(), state.RepoSpec, change.ListOptions{Filter: filter})
	if err != nil {
		return err
	}
	openCount, err := h.is.Count(req.Context(), state.RepoSpec, changes.ListOptions{Filter: changes.FilterOpen})
	openCount, err := h.is.Count(req.Context(), state.RepoSpec, change.ListOptions{Filter: change.FilterOpen})
	if err != nil {
		return fmt.Errorf("changes.Count(open): %v", err)
	}
	closedCount, err := h.is.Count(req.Context(), state.RepoSpec, changes.ListOptions{Filter: changes.FilterClosedMerged})
	closedCount, err := h.is.Count(req.Context(), state.RepoSpec, change.ListOptions{Filter: change.FilterClosedMerged})
	if err != nil {
		return fmt.Errorf("changes.Count(closed): %v", err)
	}
	var es []component.ChangeEntry
	for _, i := range is {
@@ -235,25 +235,25 @@ const (
	stateQueryKey = "state"
)

// stateFilter parses the change state filter from query,
// returning an error if the value is unsupported.
func stateFilter(query url.Values) (changes.StateFilter, error) {
func stateFilter(query url.Values) (change.StateFilter, error) {
	selectedTabName := query.Get(stateQueryKey)
	switch selectedTabName {
	case "":
		return changes.FilterOpen, nil
		return change.FilterOpen, nil
	case "closed":
		return changes.FilterClosedMerged, nil
		return change.FilterClosedMerged, nil
	case "all":
		return changes.FilterAll, nil
		return change.FilterAll, nil
	default:
		return "", fmt.Errorf("unsupported state filter value: %q", selectedTabName)
	}
}

func (s state) augmentUnread(ctx context.Context, es []component.ChangeEntry, is changes.Service, notificationsService notifications.Service) []component.ChangeEntry {
func (s state) augmentUnread(ctx context.Context, es []component.ChangeEntry, is change.Service, notificationsService notifications.Service) []component.ChangeEntry {
	if notificationsService == nil {
		return es
	}

	tt, ok := is.(interface {
@@ -307,23 +307,23 @@ func (h *handler) MockHandler(w http.ResponseWriter, req *http.Request) error {
		return fmt.Errorf("loadTemplates: %v", err)
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	err = t.ExecuteTemplate(w, "review-mock", struct {
		state
		Review changes.Review
		Review change.Review
	}{
		state: st,
		Review: changes.Review{
		Review: change.Review{
			ID:        0,
			User:      users.User{Login: "Eric Grosse", AvatarURL: "https://lh6.googleusercontent.com/-_sdEtv2PRxk/AAAAAAAAAAI/AAAAAAAAAAA/aE1Q66Cuvb4/s100-p/photo.jpg"},
			CreatedAt: time.Now().UTC(),
			Edited:    nil,
			State:     changes.Approved,
			State:     change.Approved,
			Body:      "",
			Reactions: []reactions.Reaction{},
			Editable:  true,
			Comments: []changes.InlineComment{
			Comments: []change.InlineComment{
				{
					File: "rpc/keyserver/server.go",
					Line: 26,
					Body: "Ok by me, but how was this chosen?",
				},
@@ -446,13 +446,13 @@ func (h *handler) ChangeFilesHandler(w http.ResponseWriter, req *http.Request, c
		}
		if next := i + 1; next < len(cs) {
			commit.NextSHA = cs[next].SHA
		}
	}
	var opt *changes.GetDiffOptions
	var opt *change.GetDiffOptions
	if commitID != "" {
		opt = &changes.GetDiffOptions{Commit: commitID}
		opt = &change.GetDiffOptions{Commit: commitID}
	}
	rawDiff, err := h.is.GetDiff(req.Context(), state.RepoSpec, state.ChangeID, opt)
	if err != nil {
		return err
	}
@@ -481,11 +481,11 @@ func (h *handler) ChangeFilesHandler(w http.ResponseWriter, req *http.Request, c
	return err
}

// commitIndex returns the index of commit with SHA equal to commitID,
// or -1 if not found.
func commitIndex(cs []changes.Commit, commitID string) int {
func commitIndex(cs []change.Commit, commitID string) int {
	for i := range cs {
		if cs[i].SHA == commitID {
			return i
		}
	}
@@ -543,11 +543,11 @@ type state struct {
	BodyTop           template.HTML

	common.State

	Changes  component.Changes
	Change   changes.Change
	Change   change.Change
	Timeline []timelineItem
}

func (s state) Tabnav(selected string) template.HTML {
	// Render the tabnav.
@@ -622,12 +622,12 @@ func loadTemplates(state common.State, bodyPre string) (*template.Template, erro
		},

		"render": func(c htmlg.Component) template.HTML {
			return template.HTML(htmlg.Render(c.Render()...))
		},
		"event":            func(e changes.TimelineItem) htmlg.Component { return component.Event{Event: e} },
		"changeStateBadge": func(c changes.Change) htmlg.Component { return component.ChangeStateBadge{Change: c} },
		"event":            func(e change.TimelineItem) htmlg.Component { return component.Event{Event: e} },
		"changeStateBadge": func(c change.Change) htmlg.Component { return component.ChangeStateBadge{Change: c} },
		"time":             func(t time.Time) htmlg.Component { return component.Time{Time: t} },
		"user":             func(u users.User) htmlg.Component { return component.User{User: u} },
		"avatar":           func(u users.User) htmlg.Component { return component.Avatar{User: u, Size: 48} },
	})
	t, err := vfstemplate.ParseGlob(assets.Assets, t, "/assets/*.tmpl")
@@ -642,11 +642,11 @@ func loadTemplates(state common.State, bodyPre string) (*template.Template, erro
type contextKey struct {
	name string
}

func (k *contextKey) String() string {
	return "dmitri.shuralyov.com/changes/app context value " + k.name
	return "dmitri.shuralyov.com/app/changes context value " + k.name
}

// stripPrefix returns request r with prefix of length prefixLen stripped from r.URL.Path.
// prefixLen must not be longer than len(r.URL.Path), otherwise stripPrefix panics.
// If r.URL.Path is empty after the prefix is stripped, the path is changed to "/".