josie / simplegit

package web

import (
	"html/template"
	"net/http"
	"regexp"
	"strings"

	"git.josie-c.com/josie/simplegit/internal/git"
	"git.josie-c.com/josie/simplegit/internal/render"
)

const commitsPerPage = 50

var shaPattern = regexp.MustCompile(`^[0-9a-fA-F]{4,40}$`)

type commitRow struct {
	SHA     string // short, 7 chars
	Href    string
	Subject string
	Author  string
	Date    string // trimmed ISO timestamp
}

type commitsData struct {
	navData
	Ref     string
	Commits []commitRow
}

type commitData struct {
	navData
	SHA      string
	Subject  string
	Body     string
	Author   string
	Email    string
	Date     string
	Parents  []commitRow
	TreeHref string
	DiffHTML template.HTML
	Notice   string
}

func (s *Server) handleCommits(w http.ResponseWriter, r *http.Request) {
	repo, repoPath, user, ok := s.resolveRepo(w, r)
	if !ok {
		return
	}
	owner := r.PathValue("user")
	ref := strings.Trim(r.PathValue("ref"), "/")
	if ref == "" {
		ref = repo.DefaultBranch
	}
	if !validRef(ref) {
		http.NotFound(w, r)
		return
	}
	exists, err := git.RefExists(repoPath, ref)
	if err != nil {
		s.internalError(w, r, err)
		return
	}
	if !exists {
		http.NotFound(w, r)
		return
	}
	base := "/" + owner + "/" + repo.Name

	log, err := git.Log(repoPath, ref, commitsPerPage)
	if err != nil {
		s.internalError(w, r, err)
		return
	}
	data := commitsData{navData: nav(r, repo, user, "code"), Ref: ref}
	for _, c := range log {
		data.Commits = append(data.Commits, commitRow{
			SHA:     shortSHA(c.SHA),
			Href:    base + "/commit/" + c.SHA,
			Subject: c.Subject,
			Author:  c.Author,
			Date:    trimDate(c.Date),
		})
	}
	s.render(w, "commits.html", http.StatusOK, data)
}

func (s *Server) handleCommit(w http.ResponseWriter, r *http.Request) {
	repo, repoPath, user, ok := s.resolveRepo(w, r)
	if !ok {
		return
	}
	owner := r.PathValue("user")
	sha := r.PathValue("sha")
	if !shaPattern.MatchString(sha) {
		http.NotFound(w, r)
		return
	}
	base := "/" + owner + "/" + repo.Name

	c, err := git.CommitAt(repoPath, sha)
	if err != nil {
		http.NotFound(w, r)
		return
	}
	data := commitData{
		navData: nav(r, repo, user, "code"),
		SHA:     c.SHA, Subject: c.Subject,
		Body: c.Body, Author: c.Author, Email: c.Email, Date: trimDate(c.Date),
		TreeHref: base + "/tree/" + c.SHA + "/",
	}
	for _, p := range c.Parents {
		if pc, err := git.CommitAt(repoPath, p); err == nil {
			data.Parents = append(data.Parents, commitRow{
				SHA: shortSHA(p), Href: base + "/commit/" + p, Subject: pc.Subject,
			})
		} else {
			data.Parents = append(data.Parents, commitRow{SHA: shortSHA(p), Href: base + "/commit/" + p})
		}
	}

	patch, err := git.ShowPatch(repoPath, c.SHA, maxDiffBytes)
	if err != nil {
		s.internalError(w, r, err)
		return
	}
	if len(patch) > 0 {
		data.DiffHTML, data.Notice = renderDiffHTML(patch)
	}
	s.render(w, "commit.html", http.StatusOK, data)
}

// renderDiffHTML caps a patch at maxDiffBytes and renders it highlighted,
// falling back to an escaped <pre>; the notice is set when truncated.
// Shared by the commit view and the PR diff.
func renderDiffHTML(patch []byte) (template.HTML, string) {
	notice := ""
	if len(patch) > maxDiffBytes {
		patch = patch[:maxDiffBytes]
		notice = diffTruncatedNotice
	}
	if html, handled, err := render.CodeHTML(diffLexeme, string(patch)); err == nil && handled {
		return template.HTML(html), notice
	}
	return template.HTML("<pre>" + template.HTMLEscapeString(string(patch)) + "</pre>"), notice
}

func shortSHA(sha string) string {
	return sha[:min(len(sha), 7)]
}

func trimDate(iso string) string {
	if len(iso) >= 16 {
		return iso[:16]
	}
	return iso
}