func (db Dashboard) ServeBrowseRedirect(w http.ResponseWriter, r *http.Request) {
	repositoryPtr, revision, fileName, err := db.readRepoRevisionAndPathParams(r)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintf(w, err.Error())
		return
	}
	repository := *repositoryPtr
	lineNumberParam := r.URL.Query().Get("lineNumber")
	if lineNumberParam == "" {
		lineNumberParam = "1"
	}
	lineNumber, err := strconv.Atoi(lineNumberParam)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintf(w, "Invalid format for the lineNumber parameter: %s", err)
		return
	}
	err = repository.ValidateLineNumberInPathAtRevision(revision, fileName, lineNumber)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintf(w, err.Error())
		return
	}
	http.Redirect(w, r, repository.GetBrowseUrl(
		revision, fileName, lineNumber), http.StatusMovedPermanently)
}
Serve the redirect for browsing a file.
The revision, path, and line number are all taken from the URL parameters of the request.
package dashboard
import (
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"github.com/google/todo-tracks/repo"
	"github.com/google/todo-tracks/resources"
)
const (
	fileContentsResource = "file_contents.html"
)
type Dashboard struct {
	Repositories map[string]*repo.Repository
	TodoRegex    string
	ExcludePaths string
}
func (db Dashboard) readRepoParam(r *http.Request) (*repo.Repository, error) {
	repoParam := r.URL.Query().Get("repo")
	if repoParam == "" {
		if len(db.Repositories) != 1 {
			return nil, errors.New("Missing the repo parameter")
		}
		for key := range db.Repositories {
			repoParam = key
			break
		}
	}
	repository := db.Repositories[repoParam]
	if repository == nil {
		return nil, errors.New(fmt.Sprintf("Unknown repo '%s'", repoParam))
	}
	return repository, nil
}
func (db Dashboard) readRepoAndRevisionParams(r *http.Request) (*repo.Repository, repo.Revision, error) {
	repository, err := db.readRepoParam(r)
	if err != nil {
		return nil, repo.Revision(""), err
	}
	revisionParam := r.URL.Query().Get("revision")
	if revisionParam == "" {
		return nil, repo.Revision(""), errors.New("Missing the revision parameter")
	}
	revision, err := (*repository).ValidateRevision(revisionParam)
	if err != nil {
		return nil, repo.Revision(""), err
	}
	return repository, revision, nil
}
func (db Dashboard) readRepoRevisionAndPathParams(r *http.Request) (*repo.Repository, repo.Revision, string, error) {
	repository, revision, err := db.readRepoAndRevisionParams(r)
	if err != nil {
		return nil, repo.Revision(""), "", err
	}
	fileName, err := url.QueryUnescape(r.URL.Query().Get("fileName"))
	if err != nil || fileName == "" {
		return nil, repo.Revision(""), "", errors.New("Missing the fileName parameter")
	}
	err = (*repository).ValidatePathAtRevision(revision, fileName)
	return repository, revision, fileName, err
}
func (db Dashboard) readRepoRevisionPathAndLineNumberParams(r *http.Request) (*repo.Repository, repo.Revision, string, int, error) {
	repository, revision, fileName, err := db.readRepoRevisionAndPathParams(r)
	if err != nil {
		return nil, repo.Revision(""), "", 0, err
	}
	lineNumberParam := r.URL.Query().Get("lineNumber")
	if lineNumberParam == "" {
		return nil, repo.Revision(""), "", 0, errors.New("Missing the lineNumber param")
	}
	lineNumber, err := strconv.Atoi(lineNumberParam)
	if err != nil {
		return nil, repo.Revision(""), "", 0, fmt.Errorf("Invalid format for the lineNumber parameter: %v", err)
	}
	err = (*repository).ValidateLineNumberInPathAtRevision(revision, fileName, lineNumber)
	return repository, revision, fileName, lineNumber, err
}
func (db Dashboard) ServeMainPage(w http.ResponseWriter, r *http.Request) {
	if len(db.Repositories) == 1 {
		for repoId := range db.Repositories {
			http.Redirect(w, r,
				"/ui/list_branches.html#?repo="+repoId,
				http.StatusMovedPermanently)
			return
		}
	} else {
		http.Redirect(w, r, "/ui/list_repos.html", http.StatusMovedPermanently)
	}
}
func (db Dashboard) ServeAliasesJson(w http.ResponseWriter, r *http.Request) {
	repositoryPtr, err := db.readRepoParam(r)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintf(w, "Error loading repo: \"%s\"", err)
		return
	}
	repository := *repositoryPtr
	err = repo.WriteJson(w, repository)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, "Server error \"%s\"", err)
	}
}
func (db Dashboard) ServeRevisionJson(w http.ResponseWriter, r *http.Request) {
	repositoryPtr, revision, err := db.readRepoAndRevisionParams(r)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintf(w, err.Error())
		return
	}
	repository := *repositoryPtr
	err = repo.WriteTodosJson(
		w, repository, revision, db.TodoRegex, db.ExcludePaths)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, "Server error \"%s\"", err)
	}
}
func (db Dashboard) ServeTodoJson(w http.ResponseWriter, r *http.Request) {
	repositoryPtr, revision, fileName, lineNumber, err := db.readRepoRevisionPathAndLineNumberParams(r)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}
	repository := *repositoryPtr
	todoId := repo.TodoId{
		Revision:   revision,
		FileName:   fileName,
		LineNumber: lineNumber,
	}
	repo.WriteTodoDetailsJson(w, repository, todoId)
}
func (db Dashboard) ServeTodoStatusJson(w http.ResponseWriter, r *http.Request) {
	repositoryPtr, revision, fileName, lineNumber, err := db.readRepoRevisionPathAndLineNumberParams(r)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}
	repository := *repositoryPtr
	todoId := repo.TodoId{
		Revision:   revision,
		FileName:   fileName,
		LineNumber: lineNumber,
	}
	repo.WriteTodoStatusDetailsJson(w, repository, todoId)
}