package web import ( "errors" "net/http" "strconv" "github.com/go-chi/chi/v5" "sourcecraft.dev/bigbes/sr-ht-ecore/chrome" "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= (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 { chrome.Page Repo *core.Repo Branches []browse.Branch Branch string Commits []browse.CommitInfo NextHash string }{ Page: a.page(r, "Log — "+repo.OwnerName+"/"+repo.Name), Repo: repo, Branches: branches, Branch: branch, Commits: commits, NextHash: nextHash, } a.render(w, http.StatusOK, "log", 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 { chrome.Page Repo *core.Repo Summary *browse.CommitDiff }{ Page: a.page(r, "Commit "+chrome.ShortSHA(hash)+" — "+repo.OwnerName+"/"+repo.Name), Repo: repo, Summary: summary, } a.render(w, http.StatusOK, "commit", 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 { chrome.Page Repo *core.Repo Ref string Tables []browse.TableInfo Views []View }{ Page: a.page(r, "Tree "+ref+" — "+repo.OwnerName+"/"+repo.Name), Repo: repo, Ref: ref, Tables: tables, Views: applicableViews(a.views, tables), } a.render(w, http.StatusOK, "tree", 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) } // PageNum and not Page: the chrome's own Page is embedded here, and the // pagination counter is the page's payload, which must not shadow it. view := struct { chrome.Page Repo *core.Repo Ref string Table string Rows *browse.RowPage Views []View PageNum int TotalPages int HasPrev bool HasNext bool }{ Page: a.page(r, table+" — "+repo.OwnerName+"/"+repo.Name), Repo: repo, Ref: ref, Table: table, Rows: rows, Views: views, PageNum: page, TotalPages: totalPages, HasPrev: page > 1, HasNext: page < totalPages, } // A row page is the one full-bleed screen this service has: the column count // is the table's, not ours, so the centered container turns a wide table into // a narrow strip with a scrollbar under it. view.ContainerClass = "container-fluid" a.render(w, http.StatusOK, "table", view) }