func (*Service) ListCommits(ctx context.Context, repo string, id uint64) ([]change.Commit, error) {
	if !hasChange(repo, id) {
		return nil, os.ErrNotExist
	}
	return s.changes[repo][id-1].Commits, nil
}
ListCommits lists change commits.
package fs
import (
	"context"
	"fmt"
	"os"
	"time"
	"dmitri.shuralyov.com/state"
	"github.com/shurcooL/home/internal/exp/service/change"
	"github.com/shurcooL/reactions"
	"github.com/shurcooL/users"
)
type Service struct {
	Reactions reactions.Service
}
var s = struct {
	changes map[string][]struct {
		change.Change
		Timeline []interface{}
		Commits  []change.Commit
		Diffs    map[string][]byte  
	}
}{
	changes: map[string][]struct {
		change.Change
		Timeline []interface{}
		Commits  []change.Commit
		Diffs    map[string][]byte
	}{
		"dmitri.shuralyov.com/font/woff2": {{
			Change: change.Change{
				ID:           1,
				State:        state.ChangeMerged,
				Title:        "Initial implementation of woff2.",
				Labels:       nil,
				Author:       dmitshur,
				CreatedAt:    time.Date(2018, 2, 12, 0, 9, 19, 621031866, time.UTC),
				Replies:      1,
				Commits:      3,
				ChangedFiles: 5,
			},
			Timeline: []interface{}{
				change.Comment{
					ID:        "0",
					User:      dmitshur,
					CreatedAt: time.Date(2018, 2, 12, 0, 9, 19, 621031866, time.UTC),
					Body: `Add initial parser implementation.
This is an initial implementation of a parser for the WOFF2 font
packaging format.
It is incomplete; further work will come later. The scope for this
milestone was to be able to parse .woff2 files for the needs of the
github.com/ConradIrwin/font/sfnt package.
At this time, the API is very low level and maps directly to the binary
format of the file, as described in [its specification](https: 
early development and is expected to change as further progress is made.
It successfully parses some Go font family .woff2 files that were
generated using the https: 
from the Go font source .ttf files located at
https: 
Add basic test coverage.
Helps https: 
For convenience, a ` + "`" + `godoc` + "`" + ` view of this change can be seen [here](https: 
				},
				change.Review{
					ID:        "1",
					User:      dmitshur,
					CreatedAt: time.Date(2018, 2, 20, 21, 49, 35, 536092503, time.UTC),
					State:     state.ReviewPlus2,
					Body:      "There have been some out-of-band review comments that I've addressed. This will do for the initial version.\n\nLGTM.",
				},
				change.TimelineItem{
					Actor:     dmitshur,
					CreatedAt: time.Date(2018, 2, 20, 21, 57, 47, 537746502, time.UTC),
					Payload: change.MergedEvent{
						CommitID:      "957792cbbdabb084d484a7dcfd1e5b1a739a0ced",
						CommitHTMLURL: "https://dmitri.shuralyov.com/font/woff2/...$commit/957792cbbdabb084d484a7dcfd1e5b1a739a0ced",
						RefName:       "master",
					},
				},
			},
			Commits: []change.Commit{{
				SHA: "d2568fb6f10921b2d0c84d58bad14b2fadb88aa7",
				Message: `Add initial parser implementation.
This is an initial implementation of a parser for the WOFF2 font
packaging format.
It is incomplete; further work will come later. The scope for this
milestone was to be able to parse .woff2 files for the needs of the
github.com/ConradIrwin/font/sfnt package.
At this time, the API is very low level and maps directly to the binary
format of the file, as described in its specification. This API is in
early development and is expected to change as further progress is made.
It successfully parses some Go font family .woff2 files that were
generated using the https: 
from the Go font source .ttf files located at
https: 
Add basic test coverage.
Helps https: 
				Author:     dmitshur,
				AuthorTime: time.Date(2018, 2, 11, 20, 10, 28, 0, time.UTC),
			}, {
				SHA: "61339d441b319cd6ca35d952522f86cc42ad4b6e",
				Message: `Update test for new API.
The API had changed earlier, the test wasn't updated for it. This
change fixes that, allowing tests to pass.`,
				Author:     dmitshur,
				AuthorTime: time.Date(2018, 2, 12, 20, 22, 29, 674711268, time.UTC),
			}, {
				SHA: "3b528c98b05508322be465a207f5ffd8258b8a96",
				Message: `Add comment describing null-padding check.
Also add a TODO comment to improve this check. It's not compliant with
spec as is. Addressing this will be a part of future changes.`,
				Author:     dmitshur,
				AuthorTime: time.Date(2018, 2, 20, 21, 39, 17, 660912242, time.UTC),
			}},
			Diffs: map[string][]byte{
				"all": []byte(diffAll),
				"d2568fb6f10921b2d0c84d58bad14b2fadb88aa7": []byte(diffCommit1),
				"61339d441b319cd6ca35d952522f86cc42ad4b6e": []byte(diffCommit2),
				"3b528c98b05508322be465a207f5ffd8258b8a96": []byte(diffCommit3),
			},
		}},
		"dmitri.shuralyov.com/gpu/mtl": {{
			Change: change.Change{
				ID:           1,
				State:        state.ChangeMerged,
				Title:        "Add minimal API to support interactive rendering in a window.",
				Labels:       nil,
				Author:       dmitshur,
				CreatedAt:    time.Date(2018, 10, 17, 2, 9, 9, 583606000, time.UTC),
				Replies:      1,
				Commits:      4,
				ChangedFiles: 10,
			},
			Timeline: []interface{}{
				change.Comment{
					ID:        "0",
					User:      dmitshur,
					CreatedAt: time.Date(2018, 10, 17, 2, 9, 9, 583606000, time.UTC),
					Edited: &change.Edited{
						By: dmitshur,
						At: time.Date(2018, 10, 21, 17, 25, 56, 868164000, time.UTC),
					},
					Body: `The goal of this change is to make it possible to use package mtl
to render to a window at interactive framerates (e.g., at 60 FPS,
assuming a 60 Hz display with vsync on). It adds the minimal API
that is needed.
A new movingtriangle example is added as a demonstration of this
functionality. It opens a window and renders a triangle that follows
the mouse cursor.
Much of the needed API comes from Core Animation, AppKit frameworks,
rather than Metal. Avoid adding that to mtl package; instead create
separate packages. For now, they are hidden in internal to avoid
committing to a public API and import path. After gaining more
confidence in the approach, they can be factored out and made public.`,
				},
				change.TimelineItem{
					Actor:     dmitshur,
					CreatedAt: time.Date(2018, 10, 18, 0, 54, 37, 790845000, time.UTC),
					Payload: change.RenamedEvent{
						From: "WIP: Add minimal API to support rendering to a window at 60 FPS.",
						To:   "WIP: Add minimal API to support interactive rendering in a window.",
					},
				},
				change.TimelineItem{
					Actor:     dmitshur,
					CreatedAt: time.Date(2018, 10, 21, 17, 25, 56, 868164000, time.UTC),
					Payload: change.RenamedEvent{
						From: "WIP: Add minimal API to support interactive rendering in a window.",
						To:   "Add minimal API to support interactive rendering in a window.",
					},
				},
				change.Review{
					ID:        "1",
					User:      hajimehoshi,
					CreatedAt: time.Date(2018, 10, 23, 3, 22, 57, 249312000, time.UTC),
					State:     state.ReviewPlus2,
					Body:      "I did a rough review and could not find a critical issue. 🙂",
				},
				change.TimelineItem{
					Actor:     dmitshur,
					CreatedAt: time.Date(2018, 10, 23, 3, 32, 2, 951463000, time.UTC),
					Payload: change.MergedEvent{
						CommitID:      "c4eb07ba2d711bc78bcd2606dd587d9267a61aa5",
						CommitHTMLURL: "https://dmitri.shuralyov.com/gpu/mtl/...$commit/c4eb07ba2d711bc78bcd2606dd587d9267a61aa5",
						RefName:       "master",
					},
				},
			},
			Commits: []change.Commit{{
				SHA: "fc76fa8984fb4a28ff383895e55e635e06bd32f0",
				Message: `WIP: Add minimal API to support rendering to a window at 60 FPS.
The goal of this change is to make it possible to use package mtl
to render to a window at 60 FPS. It tries to add the minimum viable
API that is needed.
A new movingtriangle example is added as a demonstration of this
functionality. It renders a triangle that follows the mouse cursor.
TODO: A lot of the newly added API comes from Core Animation, AppKit
frameworks, rather than Metal. As a result, they likely do not belong
in this package and should be factored out.`,
				Author:     dmitshur,
				AuthorTime: time.Date(2018, 10, 17, 2, 9, 9, 583606000, time.UTC),
			}, {
				SHA:        "da15ef360afe80e10274aecb1b3e1390144fde3c",
				Message:    "Add missing reference links.",
				Author:     dmitshur,
				AuthorTime: time.Date(2018, 10, 21, 3, 16, 43, 311370000, time.UTC),
			}, {
				SHA:        "d146c0ceb29d388d838337b3951f16dca31602e1",
				Message:    "Factor out Core Animation, Cocoa APIs into separate internal packages.",
				Author:     dmitshur,
				AuthorTime: time.Date(2018, 10, 21, 17, 22, 10, 566116000, time.UTC),
			}, {
				SHA: "c4eb07ba2d711bc78bcd2606dd587d9267a61aa5",
				Message: `Move internal/{ca,ns} into example/movingtriangle.
Also make minor tweaks to the documentation to make it more accurate.`,
				Author:     dmitshur,
				AuthorTime: time.Date(2018, 10, 23, 3, 20, 25, 631677000, time.UTC),
			}},
			Diffs: map[string][]byte{
				"all": []byte(diffMtlAll),
				"fc76fa8984fb4a28ff383895e55e635e06bd32f0": []byte(diffMtlCommit1),
				"da15ef360afe80e10274aecb1b3e1390144fde3c": []byte(diffMtlCommit2),
				"d146c0ceb29d388d838337b3951f16dca31602e1": []byte(diffMtlCommit3),
				"c4eb07ba2d711bc78bcd2606dd587d9267a61aa5": []byte(diffMtlCommit4),
			},
		}},
	},
}
func (*Service) List(ctx context.Context, repo string, opt change.ListOptions) ([]change.Change, error) {
	var counts func(s state.Change) bool
	switch opt.Filter {
	case change.FilterOpen:
		counts = func(s state.Change) bool { return s == state.ChangeOpen }
	case change.FilterClosedMerged:
		counts = func(s state.Change) bool { return s == state.ChangeClosed || s == state.ChangeMerged }
	case change.FilterAll:
		counts = func(s state.Change) bool { return true }
	default:
		return nil, fmt.Errorf("invalid change.ListOptions.Filter value: %q", opt.Filter)
	}
	var cs []change.Change
	for _, c := range s.changes[repo] {
		if !counts(c.State) {
			continue
		}
		cs = append(cs, c.Change)
	}
	return cs, nil
}
func (*Service) Count(ctx context.Context, repo string, opt change.ListOptions) (uint64, error) {
	var counts func(s state.Change) bool
	switch opt.Filter {
	case change.FilterOpen:
		counts = func(s state.Change) bool { return s == state.ChangeOpen }
	case change.FilterClosedMerged:
		counts = func(s state.Change) bool { return s == state.ChangeClosed || s == state.ChangeMerged }
	case change.FilterAll:
		counts = func(s state.Change) bool { return true }
	default:
		return 0, fmt.Errorf("invalid change.ListOptions.Filter value: %q", opt.Filter)
	}
	var count uint64
	for _, c := range s.changes[repo] {
		if !counts(c.State) {
			continue
		}
		count++
	}
	return count, nil
}
func (*Service) Get(ctx context.Context, repo string, id uint64) (change.Change, error) {
	if !hasChange(repo, id) {
		return change.Change{}, os.ErrNotExist
	}
	return s.changes[repo][id-1].Change, nil
}
func (svc *Service) ListTimeline(ctx context.Context, repo string, id uint64, opt *change.ListTimelineOptions) ([]interface{}, error) {
	if !hasChange(repo, id) {
		return nil, os.ErrNotExist
	}
	if svc.Reactions == nil {
		return s.changes[repo][id-1].Timeline, nil
	}
	reactions, err := svc.Reactions.List(ctx, repo)
	if err != nil {
		return nil, fmt.Errorf("ListTimeline: Reactions.List: %v", err)
	}
	timeline := make([]interface{}, len(s.changes[repo][id-1].Timeline))
	copy(timeline, s.changes[repo][id-1].Timeline)
	switch {
	case repo == "dmitri.shuralyov.com/font/woff2" && id == 1:
		{
			t := timeline[0].(change.Comment)
			t.Reactions = reactions[t.ID]
			timeline[0] = t
		}
		{
			t := timeline[1].(change.Review)
			t.Reactions = reactions[t.ID]
			timeline[1] = t
		}
	case repo == "dmitri.shuralyov.com/gpu/mtl" && id == 1:
		{
			t := timeline[0].(change.Comment)
			t.Reactions = reactions[t.ID]
			timeline[0] = t
		}
		{
			t := timeline[3].(change.Review)
			t.Reactions = reactions[t.ID]
			timeline[3] = t
		}
	}
	return timeline, nil
}