feat(web): add .patch and .diff for commit URLs

Amolith and Crush created

Add route handlers to serve raw git patches and diffs when accessing
commit URLs with .patch or .diff extensions. This allows users to
download patches directly using curl and pipe them to git apply.

Implements: bug-d552262
Co-Authored-By: Crush <crush@charm.land>

Change summary

git/repo.go             | 14 +++++++++++
pkg/web/webui.go        |  6 ++++
pkg/web/webui_commit.go | 54 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 74 insertions(+)

Detailed changes

git/repo.go 🔗

@@ -1,6 +1,7 @@
 package git
 
 import (
+	"io"
 	"path/filepath"
 	"strings"
 
@@ -160,6 +161,19 @@ func (r *Repository) Patch(commit *Commit) (string, error) {
 	return diff.Patch(), err
 }
 
+// RawDiffFormat represents the format type for raw diffs.
+type RawDiffFormat = git.RawDiffFormat
+
+const (
+	RawDiffNormal RawDiffFormat = git.RawDiffNormal
+	RawDiffPatch  RawDiffFormat = git.RawDiffPatch
+)
+
+// RawDiff writes raw diff output to the provided writer.
+func (r *Repository) RawDiff(rev string, diffType RawDiffFormat, w io.Writer, opts ...git.RawDiffOptions) error {
+	return r.Repository.RawDiff(rev, diffType, w, opts...)
+}
+
 // CountCommits returns the number of commits in the repository.
 func (r *Repository) CountCommits(ref *Reference) (int64, error) {
 	return r.RevListCount([]string{ref.Name().String()})

pkg/web/webui.go 🔗

@@ -278,6 +278,12 @@ func WebUIController(ctx context.Context, r *mux.Router) {
 	r.Handle(basePrefix+"/commit/{hash:[0-9a-f]+}", withRepoVars(withWebUIAccess(http.HandlerFunc(repoCommit)))).
 		Methods(http.MethodGet)
 
+	// Commit patch and diff routes
+	r.Handle(basePrefix+"/commit/{hash:[0-9a-f]+}.patch", withRepoVars(withWebUIAccess(http.HandlerFunc(repoCommitPatch)))).
+		Methods(http.MethodGet)
+	r.Handle(basePrefix+"/commit/{hash:[0-9a-f]+}.diff", withRepoVars(withWebUIAccess(http.HandlerFunc(repoCommitDiff)))).
+		Methods(http.MethodGet)
+
 	// Branches route
 	r.Handle(basePrefix+"/branches", withRepoVars(withWebUIAccess(http.HandlerFunc(repoBranches)))).
 		Methods(http.MethodGet)

pkg/web/webui_commit.go 🔗

@@ -73,3 +73,57 @@ func repoCommit(w http.ResponseWriter, r *http.Request) {
 
 	renderHTML(w, "commit.html", data)
 }
+
+func repoCommitPatch(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	logger := log.FromContext(ctx)
+	repo := proto.RepositoryFromContext(ctx)
+	vars := mux.Vars(r)
+	hash := vars["hash"]
+
+	gr, err := openRepository(repo)
+	if err != nil {
+		logger.Debug("failed to open repository", "repo", repo.Name(), "err", err)
+		renderInternalServerError(w, r)
+		return
+	}
+
+	_, err = gr.CatFileCommit(hash)
+	if err != nil {
+		logger.Debug("failed to get commit", "repo", repo.Name(), "hash", hash, "err", err)
+		renderNotFound(w, r)
+		return
+	}
+
+	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	if err := gr.RawDiff(hash, git.RawDiffPatch, w); err != nil {
+		logger.Debug("failed to generate patch", "repo", repo.Name(), "hash", hash, "err", err)
+	}
+}
+
+func repoCommitDiff(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	logger := log.FromContext(ctx)
+	repo := proto.RepositoryFromContext(ctx)
+	vars := mux.Vars(r)
+	hash := vars["hash"]
+
+	gr, err := openRepository(repo)
+	if err != nil {
+		logger.Debug("failed to open repository", "repo", repo.Name(), "err", err)
+		renderInternalServerError(w, r)
+		return
+	}
+
+	_, err = gr.CatFileCommit(hash)
+	if err != nil {
+		logger.Debug("failed to get commit", "repo", repo.Name(), "hash", hash, "err", err)
+		renderNotFound(w, r)
+		return
+	}
+
+	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	if err := gr.RawDiff(hash, git.RawDiffNormal, w); err != nil {
+		logger.Debug("failed to generate diff", "repo", repo.Name(), "hash", hash, "err", err)
+	}
+}