package web
import (
"errors"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
"sourcecraft.dev/bigbes/sr-ht-dolt/core"
)
const (
// logPageSize is the number of commits per /log page.
logPageSize = 20
// rowsPageSize is the number of rows per /table page.
rowsPageSize = 50
)
// openBrowse opens a browse session for repo, writing a 500 and returning
// ok=false on failure. The caller must Close the returned session.
func (a *app) openBrowse(w http.ResponseWriter, r *http.Request, repo *core.Repo) (BrowseSession, bool) {
sess, err := a.cfg.Browse.Open(r.Context(), repo.Path)
if err != nil {
http.Error(w, "failed to open database", http.StatusInternalServerError)
return nil, false
}
return sess, true
}
// handleLog renders a paginated commit log for a branch (or ref). Pages after
// the first are reached via ?from=<hash> (the nextHash from the prior page); the
// active branch is chosen with ?branch=.
func (a *app) handleLog(w http.ResponseWriter, r *http.Request) {
repo, _, _, ok := a.loadRepoForBrowse(w, r)
if !ok {
return
}
sess, ok := a.openBrowse(w, r, repo)
if !ok {
return
}
defer sess.Close()
branches, err := sess.Branches(r.Context())
if err != nil {
http.Error(w, "failed to list branches", http.StatusInternalServerError)
return
}
branch := r.URL.Query().Get("branch")
if branch == "" {
branch = browse.DefaultBranch(branches)
}
fromHash := r.URL.Query().Get("from")
commits, nextHash, err := sess.Log(r.Context(), branch, fromHash, logPageSize)
if err != nil {
if errors.Is(err, browse.ErrRefNotFound) {
a.notFound(w, r)
return
}
http.Error(w, "failed to read log", http.StatusInternalServerError)
return
}
view := struct {
basePage
Repo *core.Repo
Branches []browse.Branch
Branch string
Commits []browse.CommitInfo
NextHash string
}{
basePage: a.newBasePage(r, "Log — "+repo.OwnerName+"/"+repo.Name),
Repo: repo,
Branches: branches,
Branch: branch,
Commits: commits,
NextHash: nextHash,
}
a.render(w, http.StatusOK, "log.html", view)
}
// handleCommit renders a single commit's per-table diff summary.
func (a *app) handleCommit(w http.ResponseWriter, r *http.Request) {
repo, _, _, ok := a.loadRepoForBrowse(w, r)
if !ok {
return
}
sess, ok := a.openBrowse(w, r, repo)
if !ok {
return
}
defer sess.Close()
hash := chi.URLParam(r, "hash")
summary, err := sess.CommitSummary(r.Context(), hash)
if err != nil {
if errors.Is(err, browse.ErrRefNotFound) {
a.notFound(w, r)
return
}
http.Error(w, "failed to read commit", http.StatusInternalServerError)
return
}
view := struct {
basePage
Repo *core.Repo
Summary *browse.CommitDiff
}{
basePage: a.newBasePage(r, "Commit "+shortHash(hash)+" — "+repo.OwnerName+"/"+repo.Name),
Repo: repo,
Summary: summary,
}
a.render(w, http.StatusOK, "commit.html", view)
}
// handleTree renders the tables (with schemas) present at a ref.
func (a *app) handleTree(w http.ResponseWriter, r *http.Request) {
repo, _, _, ok := a.loadRepoForBrowse(w, r)
if !ok {
return
}
sess, ok := a.openBrowse(w, r, repo)
if !ok {
return
}
defer sess.Close()
ref := chi.URLParam(r, "ref")
tables, err := sess.Tables(r.Context(), ref)
if err != nil {
if errors.Is(err, browse.ErrRefNotFound) {
a.notFound(w, r)
return
}
http.Error(w, "failed to read tables", http.StatusInternalServerError)
return
}
view := struct {
basePage
Repo *core.Repo
Ref string
Tables []browse.TableInfo
Views []View
}{
basePage: a.newBasePage(r, "Tree "+ref+" — "+repo.OwnerName+"/"+repo.Name),
Repo: repo,
Ref: ref,
Tables: tables,
Views: applicableViews(a.views, tables),
}
a.render(w, http.StatusOK, "tree.html", view)
}
// handleTable renders a table's schema and a paginated page of its rows. Pages
// are selected with ?page=N (1-based).
func (a *app) handleTable(w http.ResponseWriter, r *http.Request) {
repo, _, _, ok := a.loadRepoForBrowse(w, r)
if !ok {
return
}
sess, ok := a.openBrowse(w, r, repo)
if !ok {
return
}
defer sess.Close()
ref := chi.URLParam(r, "ref")
table := chi.URLParam(r, "table")
page := 1
if p, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && p > 1 {
page = p
}
offset := (page - 1) * rowsPageSize
rows, err := sess.Rows(r.Context(), ref, table, offset, rowsPageSize)
if err != nil {
if errors.Is(err, browse.ErrRefNotFound) || errors.Is(err, browse.ErrTableNotFound) {
a.notFound(w, r)
return
}
http.Error(w, "failed to read rows", http.StatusInternalServerError)
return
}
totalPages := (rows.Total + rowsPageSize - 1) / rowsPageSize
if totalPages < 1 {
totalPages = 1
}
// Fingerprint the tables at this ref for the tab bar; a failure here must not
// break the row view, so it degrades to no alternative-view tabs.
var views []View
if tables, err := sess.Tables(r.Context(), ref); err == nil {
views = applicableViews(a.views, tables)
}
view := struct {
basePage
Repo *core.Repo
Ref string
Table string
Rows *browse.RowPage
Views []View
Page int
TotalPages int
HasPrev bool
HasNext bool
}{
basePage: a.newBasePage(r, table+" — "+repo.OwnerName+"/"+repo.Name),
Repo: repo,
Ref: ref,
Table: table,
Rows: rows,
Views: views,
Page: page,
TotalPages: totalPages,
HasPrev: page > 1,
HasNext: page < totalPages,
}
a.render(w, http.StatusOK, "table.html", view)
}