dmitri.shuralyov.com/app/changes

Remove code related to write behavior.

Keep only read behavior. This greatly simplifies the code and allows
faster development of changesapp-specific features.

Write behavior will be worked in the future, after read behavior is
fully complete.
dmitshur committed 6 years ago commit 510fbe7977beb4f59ca20598b357a06c4474acd0
Showing partial commit. Full Commit
Collapse all
display.go
@@ -5,51 +5,51 @@ import (
	"time"

	"github.com/shurcooL/issues"
)

// issueItem represents an issue item for display purposes.
type issueItem struct {
	// IssueItem can be one of issues.Comment, issues.Event.
	IssueItem interface{}
// timelineItem represents a timeline item for display purposes.
type timelineItem struct {
	// TimelineItem can be one of issues.Comment, issues.Event.
	TimelineItem interface{}
}

func (i issueItem) TemplateName() string {
	switch i.IssueItem.(type) {
func (i timelineItem) TemplateName() string {
	switch i.TimelineItem.(type) {
	case issues.Comment:
		return "comment"
	case issues.Event:
		return "event"
	default:
		panic(fmt.Errorf("unknown item type %T", i.IssueItem))
		panic(fmt.Errorf("unknown item type %T", i.TimelineItem))
	}
}

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

func (i issueItem) ID() uint64 {
	switch i := i.IssueItem.(type) {
func (i timelineItem) ID() uint64 {
	switch i := i.TimelineItem.(type) {
	case issues.Comment:
		return i.ID
	case issues.Event:
		return i.ID
	default:
		panic(fmt.Errorf("unknown item type %T", i))
	}
}

// byCreatedAtID implements sort.Interface.
type byCreatedAtID []issueItem
type byCreatedAtID []timelineItem

func (s byCreatedAtID) Len() int { return len(s) }
func (s byCreatedAtID) Less(i, j int) bool {
	if s[i].CreatedAt().Equal(s[j].CreatedAt()) {
		// If CreatedAt time is equal, fall back to ID as a tiebreaker.
main.go
@@ -77,23 +77,23 @@ func New(service changes.Service, users users.Service, opt Options) http.Handler
		handler: h.ServeHTTP,
		users:   users,
	}
}

// RepoSpecContextKey is a context key for the request's issues.RepoSpec.
// That value specifies which repo the issues are to be displayed for.
// RepoSpecContextKey is a context key for the request's repo spec.
// That value specifies which repo the changes are to be displayed for.
// The associated value will be of type string.
var RepoSpecContextKey = &contextKey{"RepoSpec"}

// BaseURIContextKey is a context key for the request's base URI.
// That value specifies the base URI prefix to use for all absolute URLs.
// The associated value will be of type string.
var BaseURIContextKey = &contextKey{"BaseURI"}

// Options for configuring issues app.
// Options for configuring changes app.
type Options struct {
	Notifications    notifications.Service // If not nil, issues containing unread notifications are highlighted.
	Notifications    notifications.Service // If not nil, changes containing unread notifications are highlighted.
	DisableReactions bool                  // Disable all support for displaying and toggling reactions.

	HeadPre, HeadPost template.HTML
	BodyPre           string // An html/template definition of "body-pre" template.

@@ -144,18 +144,18 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) error {
		return nil
	}

	// Handle "/".
	if req.URL.Path == "/" {
		return h.IssuesHandler(w, req)
		return h.ChangesHandler(w, req)
	}

	// Handle "/{changeID}" and "/{changeID}/...".
	elems := strings.SplitN(req.URL.Path[1:], "/", 3)
	changeID, err := strconv.ParseUint(elems[0], 10, 64)
	if err != nil {
		return httperror.HTTP{Code: http.StatusNotFound, Err: fmt.Errorf("invalid issue ID %q: %v", elems[0], err)}
		return httperror.HTTP{Code: http.StatusNotFound, Err: fmt.Errorf("invalid change ID %q: %v", elems[0], err)}
	}
	switch {
	// "/{changeID}".
	case len(elems) == 1:
		return h.ChangeHandler(w, req, changeID)
@@ -176,11 +176,11 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) error {
	default:
		return httperror.HTTP{Code: http.StatusNotFound, Err: errors.New("no route")}
	}
}

func (h *handler) IssuesHandler(w http.ResponseWriter, req *http.Request) error {
func (h *handler) ChangesHandler(w http.ResponseWriter, req *http.Request) error {
	if req.Method != http.MethodGet {
		return httperror.Method{Allowed: []string{http.MethodGet}}
	}
	state, err := h.state(req, 0)
	if err != nil {
@@ -194,22 +194,22 @@ func (h *handler) IssuesHandler(w http.ResponseWriter, req *http.Request) error
	if err != nil {
		return err
	}
	openCount, err := h.is.Count(req.Context(), state.RepoSpec, changes.ListOptions{State: changes.StateFilter(changes.OpenState)})
	if err != nil {
		return fmt.Errorf("issues.Count(open): %v", err)
		return fmt.Errorf("changes.Count(open): %v", err)
	}
	closedCount, err := h.is.Count(req.Context(), state.RepoSpec, changes.ListOptions{State: changes.StateFilter(changes.ClosedState)})
	if err != nil {
		return fmt.Errorf("issues.Count(closed): %v", err)
		return fmt.Errorf("changes.Count(closed): %v", err)
	}
	var es []component.ChangeEntry
	for _, i := range is {
		es = append(es, component.ChangeEntry{Change: i, BaseURI: state.BaseURI})
	}
	es = state.augmentUnread(req.Context(), es, h.is, h.Notifications)
	state.Changes = component.Issues{
	state.Changes = component.Changes{
		IssuesNav: component.IssuesNav{
			OpenCount:     openCount,
			ClosedCount:   closedCount,
			Path:          state.BaseURI + state.ReqPath,
			Query:         req.URL.Query(),
@@ -217,23 +217,23 @@ func (h *handler) IssuesHandler(w http.ResponseWriter, req *http.Request) error
		},
		Filter:  filter,
		Entries: es,
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	err = h.static.ExecuteTemplate(w, "issues.html.tmpl", &state)
	err = h.static.ExecuteTemplate(w, "changes.html.tmpl", &state)
	if err != nil {
		return fmt.Errorf("h.static.ExecuteTemplate: %v", err)
	}
	return nil
}

const (
	// stateQueryKey is name of query key for controlling issue state filter.
	// stateQueryKey is name of query key for controlling change state filter.
	stateQueryKey = "state"
)

// stateFilter parses the issue state filter from query,
// stateFilter parses the change state filter from query,
// returning an error if the value is unsupported.
func stateFilter(query url.Values) (changes.StateFilter, error) {
	selectedTabName := query.Get(stateQueryKey)
	switch selectedTabName {
	case "":
@@ -296,38 +296,38 @@ func (h *handler) ChangeHandler(w http.ResponseWriter, req *http.Request, change
	}
	state, err := h.state(req, changeID)
	if err != nil {
		return err
	}
	state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.IssueID)
	state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.ChangeID)
	if err != nil {
		return err
	}
	cs, err := h.is.ListComments(req.Context(), state.RepoSpec, state.IssueID, nil)
	cs, err := h.is.ListComments(req.Context(), state.RepoSpec, state.ChangeID, nil)
	if err != nil {
		return fmt.Errorf("changes.ListComments: %v", err)
	}
	es, err := h.is.ListEvents(req.Context(), state.RepoSpec, state.IssueID, nil)
	es, err := h.is.ListEvents(req.Context(), state.RepoSpec, state.ChangeID, nil)
	if err != nil {
		return fmt.Errorf("changes.ListEvents: %v", err)
	}
	var items []issueItem
	var items []timelineItem
	for _, comment := range cs {
		items = append(items, issueItem{comment})
		items = append(items, timelineItem{comment})
	}
	for _, event := range es {
		items = append(items, issueItem{event})
		items = append(items, timelineItem{event})
	}
	sort.Sort(byCreatedAtID(items))
	state.Items = items
	// Call loadTemplates to set updated reactionsBar, reactableID, etc., template functions.
	t, err := loadTemplates(state.State, h.Options.BodyPre)
	if err != nil {
		return fmt.Errorf("loadTemplates: %v", err)
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	err = t.ExecuteTemplate(w, "issue.html.tmpl", &state)
	err = t.ExecuteTemplate(w, "change.html.tmpl", &state)
	if err != nil {
		return fmt.Errorf("t.ExecuteTemplate: %v", err)
	}
	return nil
}
@@ -338,15 +338,15 @@ func (h *handler) ChangeCommitsHandler(w http.ResponseWriter, req *http.Request,
	}
	state, err := h.state(req, changeID)
	if err != nil {
		return err
	}
	state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.IssueID)
	state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.ChangeID)
	if err != nil {
		return err
	}
	cs, err := h.is.ListCommits(req.Context(), state.RepoSpec, state.IssueID)
	cs, err := h.is.ListCommits(req.Context(), state.RepoSpec, state.ChangeID)
	if err != nil {
		return err
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	err = h.static.ExecuteTemplate(w, "change-commits.html.tmpl", &state)
@@ -373,17 +373,17 @@ func (h *handler) ChangeFilesHandler(w http.ResponseWriter, req *http.Request, c
	}
	state, err := h.state(req, changeID)
	if err != nil {
		return err
	}
	state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.IssueID)
	state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.ChangeID)
	if err != nil {
		return err
	}
	var commit commitMessage
	if commitID != "" {
		cs, err := h.is.ListCommits(req.Context(), state.RepoSpec, state.IssueID)
		cs, err := h.is.ListCommits(req.Context(), state.RepoSpec, state.ChangeID)
		if err != nil {
			return err
		}
		i := commitIndex(cs, commitID)
		if i == -1 {
@@ -406,11 +406,11 @@ func (h *handler) ChangeFilesHandler(w http.ResponseWriter, req *http.Request, c
	}
	var opt *changes.GetDiffOptions
	if commitID != "" {
		opt = &changes.GetDiffOptions{Commit: commitID}
	}
	rawDiff, err := h.is.GetDiff(req.Context(), state.RepoSpec, state.IssueID, opt)
	rawDiff, err := h.is.GetDiff(req.Context(), state.RepoSpec, state.ChangeID, opt)
	if err != nil {
		return err
	}
	fileDiffs, err := diff.ParseMultiFileDiff(rawDiff)
	if err != nil {
@@ -452,18 +452,18 @@ func (h *handler) state(req *http.Request, changeID uint64) (state, error) {
	// TODO: Caller still does a lot of work outside to calculate req.URL.Path by
	//       subtracting BaseURI from full original req.URL.Path. We should be able
	//       to compute it here internally by using req.RequestURI and BaseURI.
	reqPath := req.URL.Path
	if reqPath == "/" {
		reqPath = "" // This is needed so that absolute URL for root view, i.e., /issues, is "/issues" and not "/issues/" because of "/issues" + "/".
		reqPath = "" // This is needed so that absolute URL for root view, i.e., /changes, is "/changes" and not "/changes/" because of "/changes" + "/".
	}
	b := state{
		State: common.State{
			BaseURI:  req.Context().Value(BaseURIContextKey).(string),
			ReqPath:  reqPath,
			RepoSpec: req.Context().Value(RepoSpecContextKey).(string),
			IssueID:  changeID,
			ChangeID: changeID,
		},
	}
	b.HeadPre = h.HeadPre
	b.HeadPost = h.HeadPost
	if h.BodyTop != nil {
@@ -498,35 +498,35 @@ type state struct {
	HeadPre, HeadPost template.HTML
	BodyTop           template.HTML

	common.State

	Changes component.Issues
	Changes component.Changes
	Change  changes.Change
	Items   []issueItem
	Items   []timelineItem
}

func (s state) Tabnav(selected string) template.HTML {
	// Render the tabnav.
	return template.HTML(htmlg.RenderComponentsString(tabnav{
		Tabs: []tab{
			{
				Content:  iconText{Icon: octiconssvg.CommentDiscussion, Text: "Discussion"},
				URL:      fmt.Sprintf("%s/%d", s.BaseURI, s.IssueID),
				URL:      fmt.Sprintf("%s/%d", s.BaseURI, s.ChangeID),
				Selected: selected == "Discussion",
			},
			{
				Content: contentCounter{
					Content: iconText{Icon: octiconssvg.GitCommit, Text: "Commits"},
					Count:   s.Change.Commits,
				},
				URL:      fmt.Sprintf("%s/%d/commits", s.BaseURI, s.IssueID),
				URL:      fmt.Sprintf("%s/%d/commits", s.BaseURI, s.ChangeID),
				Selected: selected == "Commits",
			},
			{
				Content:  iconText{Icon: octiconssvg.Diff, Text: "Files"},
				URL:      fmt.Sprintf("%s/%d/files", s.BaseURI, s.IssueID),
				URL:      fmt.Sprintf("%s/%d/files", s.BaseURI, s.ChangeID),
				Selected: selected == "Files",
			},
		},
	}))
}
@@ -546,11 +546,11 @@ func loadTemplates(state common.State, bodyPre string) (*template.Template, erro
		"reactionPosition": func(emojiID reactions.EmojiID) string { return reactions.Position(":" + string(emojiID) + ":") },
		"equalUsers": func(a, b users.User) bool {
			return a.UserSpec == b.UserSpec
		},
		"reactableID": func(commentID uint64) string {
			return fmt.Sprintf("%d/%d", state.IssueID, commentID)
			return fmt.Sprintf("%d/%d", state.ChangeID, commentID)
		},
		"reactionsBar": func(reactions []reactions.Reaction, reactableID string) htmlg.Component {
			return reactionscomponent.ReactionsBar{
				Reactions:   reactions,
				CurrentUser: state.CurrentUser,