--- a/internal/archesrv/handlers_repo.go
+++ b/internal/archesrv/handlers_repo.go
@@ -1,779 +1,894 @@
package archesrv
import (
"bytes"
"encoding/hex"
"fmt"
"html/template"
"net/http"
"sort"
"strings"
"arche/internal/diff"
"arche/internal/
+markdown"
+ "arche/internal/
object"
"arche/internal/repo"
"arche/internal/revset"
"arche/internal/syncpkg"
"github.com/alecthomas/chroma/v2"
chrhtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"golang.org/x/crypto/ssh"
)
func (s *forgeServer) requireRepoAccess(w http.ResponseWriter, r *http.Request) (*repo.Repo, *RepoRecord, bool) {
repoName := r.PathValue("repo")
rec, err := s.db.GetRepo(repoName)
if err != nil || rec == nil {
http.NotFound(w, r)
return nil, nil, false
}
user := s.db.currentUser(r)
if !s.db.CanRead(rec, user) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return nil, nil, false
}
repoObj, err := openRepo(s.dataDir(), repoName)
if err != nil {
http.Error(w, "open repo: "+err.Error(), http.StatusInternalServerError)
return nil, nil, false
}
return repoObj, rec, true
}
func (s *forgeServer) handleSyncProxy(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("repo")
rec, err := s.db.GetRepo(repoName)
if err != nil || rec == nil {
http.Error(w, "repo not found", http.StatusNotFound)
return
}
user := s.db.currentUser(r)
if r.Method != http.MethodGet && !s.db.CanWrite(rec, user) {
user := s.db.currentUser(r)
username := "anonymous"
if user != nil {
username = user.Username
}
s.log.Warn("sync write denied", "repo", repoName, "user", username)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if r.Method == http.MethodGet && !s.db.CanRead(rec, user) {
s.log.Warn("sync read denied", "repo", repoName)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
repoObj, err := openRepo(s.dataDir(), repoName)
if err != nil {
http.Error(w, "open repo: "+err.Error(), http.StatusInternalServerError)
return
}
defer repoObj.Close()
action := strings.TrimPrefix(r.URL.Path, "/"+repoName)
r2 := r.Clone(r.Context())
r2.URL.Path = action
user = s.db.currentUser(r)
pusher := "anonymous"
if user != nil {
pusher = user.Username
}
srv := syncpkg.NewServer(repoObj, "")
repoKey := repoName
repoCfg := s.cfg.Repo[repoKey]
srv.PreUpdateHook = func(bm, oldHex, newHex string) error {
if s.cfg.Hooks.PreReceive != "" || s.cfg.Hooks.Update != "" {
if err := runPreReceiveHook(s.cfg.Hooks.PreReceive, bm, oldHex, newHex, s.cfg.Hooks.TimeoutSec); err != nil {
return err
}
if err := runPreReceiveHook(s.cfg.Hooks.Update, bm, oldHex, newHex, s.cfg.Hooks.TimeoutSec); err != nil {
return err
}
}
if repoCfg.RequireSignedCommits && user != nil {
for _, id := range collectNewCommitIDs(repoObj, oldHex, newHex) {
c, err := repoObj.ReadCommit(id)
if err != nil {
continue
}
if len(c.CommitSig) == 0 {
return fmt.Errorf("commit %s (ch:%s) is unsigned; this repository requires signed commits",
hex.EncodeToString(id[:8]), c.ChangeID)
}
body := object.CommitBodyForSigning(c)
keys, _ := s.db.ListSSHKeys(user.ID)
verified := false
for _, k := range keys {
pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k.PublicKey))
if err != nil {
continue
}
if object.VerifyCommitSig(body, c.CommitSig, pub) == nil {
verified = true
break
}
}
if !verified {
return fmt.Errorf("commit %s (ch:%s) has an unverifiable signature; this repository requires commits signed by a registered key",
hex.EncodeToString(id[:8]), c.ChangeID)
}
}
}
return nil
}
srv.OnBookmarkUpdated = func(bm, oldHex, newHex string) {
s.db.FirePushWebhooks(repoName, pusher, bm, oldHex, newHex, collectPushCommits(repoObj, oldHex, newHex))
runPostReceiveHook(s.cfg.Hooks.PostReceive, bm, oldHex, newHex, s.cfg.Hooks.TimeoutSec)
if user != nil {
for _, id := range collectNewCommitIDs(repoObj, oldHex, newHex) {
c, err := repoObj.ReadCommit(id)
if err != nil {
continue
}
_ = s.db.RecordCommitSignature(repoObj, id, c, user.ID)
}
}
if allowed, script, _ := s.db.GetRepoHookConfig(rec.ID); allowed && script != "" {
if !s.db.hasWriteCollaborator(rec.ID) {
runPostReceiveHook(script, bm, oldHex, newHex, s.cfg.Hooks.TimeoutSec)
}
}
}
srv.Handler().ServeHTTP(w, r2)
}
func collectPushCommits(r *repo.Repo, oldHex, newHex string) []CommitRef {
if len(newHex) != 64 {
return []CommitRef{}
}
newBytes, err := hex.DecodeString(newHex)
if err != nil || len(newBytes) != 32 {
return []CommitRef{}
}
var newID [32]byte
copy(newID[:], newBytes)
var oldID [32]byte
if len(oldHex) == 64 {
if oldBytes, err2 := hex.DecodeString(oldHex); err2 == nil && len(oldBytes) == 32 {
copy(oldID[:], oldBytes)
}
}
seen := make(map[[32]byte]bool)
queue := [][32]byte{newID}
var results []CommitRef
const maxCommits = 50
for len(queue) > 0 && len(results) < maxCommits {
id := queue[0]
queue = queue[1:]
if seen[id] || id == oldID {
continue
}
seen[id] = true
c, err := r.ReadCommit(id)
if err != nil {
break
}
author := c.Author.Name
if c.Author.Email != "" {
author += " <" + c.Author.Email + ">"
}
results = append(results, CommitRef{
ID: hex.EncodeToString(id[:]),
ChangeID: "ch:" + c.ChangeID,
Message: c.Message,
Author: author,
})
for _, p := range c.Parents {
if !seen[p] && p != oldID {
queue = append(queue, p)
}
}
}
return results
}
+type srvHomeData struct {
+ Repo string
+ User *User
+ CommitHex string
+ ShortHex string
+ ReadmeName string
+ ReadmeHTML template.HTML
+ Entries []srvTreeEntry
+}
+
-func (s *forgeServer) handleRepoHome(w http.ResponseWriter, r *http.Request) {
-
+func (s *forgeServer) handleRepoHome(w http.ResponseWriter, r *http.Request) {
+
+repoObj, rec, ok := s.requireRepoAccess(w, r)
+ if !ok {
+ return
+ }
+ defer repoObj.Close()
+
+ commitID := resolveDefaultCommit(repoObj)
+ if commitID == ([32]byte{}) {
+ _, id, err := repoObj.HeadCommit()
+ if err != nil {
+
-http.Redirect(w, r, "/"+r
+http.Redirect(w, r, "/"+r
+ec
-.
+.
-P
+N
-a
+a
+me+"/log", h
-t
+t
+tp.StatusFound)
+ return
+ }
+ commitID = id
+ }
+
+ c, err := repoObj.ReadCommit(commitID)
+ if err != nil {
+
-h
+h
-V
+ttp.Redirect(w, r, "/"+rec.N
-a
+a
+me+"/
-l
+l
+og", http.Stat
-u
+u
+sFound)
+ return
+ }
+ tree, err := repoObj.ReadTre
-e(
+e(
+c.TreeID)
+ if err != nil {
+ http.Redirect(w, r, "/
-"
+"
++
-re
+re
+c.Name+"/log", htt
-p
+p
+.StatusF
-o
+o
+und)
+ return
+ }
+
+ commitHex := fullHex(commitID)
+
+ var entries []srvTreeEntry
+ for _, e := range tree.Entries {
+ isDir := e.Mode == object.ModeDir
+ var link string
+ if isDir {
+ link = fmt.Sprintf(
-"
+"
+/%s/tree?id=%s&path=%s", rec.Name, commitHex, e.Name
-)
+)
+
+ } else {
+ link = fmt.Sprintf("/%s/file?id=%s&path=%s", rec.Name, commitHex, e.Name)
+ }
+ entries = append(entries, srvTreeEntry{
+ Name: e.Name,
+ IsDir: isDir,
+ Mode: modeStr(e.Mode),
+ Link: link,
+ })
+ }
+
+ var readmeName string
+ var readmeHTML template.HTML
+ for _, candidate := range []string{"README.md", "readme.md", "README", "readme"} {
+ for _, e := range tree.Entries {
+ if e.Name == candidate && e.Mode != object.ModeDir {
+ content, err := repoObj.ReadBlob(e.ObjectID)
+ if err != nil || isBinaryContent(content) {
+ break
+ }
+ readmeName = e.Name
+ if strings.HasSuffix(strings.ToLower(e.Name), ".md") {
+ readmeHTML = markdown.Render(string(content))
+ } else {
+ readmeHTML = template.HTML("<pre>" + template.HTMLEscapeString(string(content)) + "</pre>")
+ }
+ break
+ }
+ }
+ if readmeName != "" {
+ break
+ }
+ }
+
+ if readmeName == "" && len(entries) == 0 {
+ http.Redirect(w, r, "/"+rec.Name
-+"/log", http.StatusFound
++"/log", http.StatusFound
+)
+ return
+ }
+
+ s.render(w, "srv_repo_home.html", srvHomeData{
+ Repo: rec.Name,
+ User: s.db.currentUser(r),
+ CommitHex: commitHex,
+ ShortHex: shortHex(commitID),
+ ReadmeName: readmeName,
+ ReadmeHTML: readmeHTML,
+ Entries: entries,
+ }
)
}
type srvCommitRow struct {
HexID string
ShortHex string
ChangeID string
Author string
Date string
Phase string
PhaseClass string
Message string
Bookmarks []string
IsHead bool
}
type srvLogData struct {
Repo string
User *User
Commits []srvCommitRow
WhereExpr string
WhereErr string
BookmarkFilter string
AllBookmarks []string
}
func (s *forgeServer) handleRepoLog(w http.ResponseWriter, r *http.Request) {
repoObj, rec, ok := s.requireRepoAccess(w, r)
if !ok {
return
}
defer repoObj.Close()
const maxCommits = 200
where := r.URL.Query().Get("where")
bookmarkFilter := r.URL.Query().Get("bookmark")
var whereFilter revset.Func
var whereErr string
if where != "" {
var err error
whereFilter, err = revset.Parse(where)
if err != nil {
whereErr = err.Error()
}
}
headCID, _ := repoObj.HeadChangeID()
bmMap := bookmarkMap(repoObj)
allBms, _ := repoObj.Store.ListBookmarks()
allBmNames := make([]string, 0, len(allBms))
for _, bm := range allBms {
allBmNames = append(allBmNames, bm.Name)
}
var candidateIDs [][32]byte
if bookmarkFilter != "" {
bm, err := repoObj.Store.GetBookmark(bookmarkFilter)
if err == nil && bm != nil {
visited := map[[32]byte]bool{}
queue := [][32]byte{bm.CommitID}
for len(queue) > 0 && len(candidateIDs) < maxCommits*2 {
id := queue[0]
queue = queue[1:]
if visited[id] {
continue
}
visited[id] = true
candidateIDs = append(candidateIDs, id)
c, err := repoObj.ReadCommit(id)
if err != nil {
continue
}
for _, p := range c.Parents {
if !visited[p] {
queue = append(queue, p)
}
}
}
}
} else {
v
+isited := map[[32]byte]bool{}
+ v
-ar
+ar
+qu
-e
+e
-rr
+ue
-
+
+[][32]byt
-e
+e
-rror
-
-
+
+
-c
-a
+a
-ndidateID
+llBm
-s
+s
+2
-,
+,
-err
+_
-
+
+:
-= repoObj.Store.List
+= repoObj.Store.List
-Pu
+Bookmarks()
+ for _,
-b
+b
+m := range a
l
+lBms2 {
+
i
-c
+f !visited[bm.
CommitID
-s
+] {
+ queue = append
(
+queue, bm.CommitID
)
+ visited[bm.CommitID] = true
+ }
+ }
+
if
+_, headID,
err
-!
+:= repoObj.HeadCommit(); err =
= nil
+&& !visited[headID]
{
+queue = append(queue,
h
+eadID)
+ visi
t
+ed[headID] =
t
-p.E
r
-r
+ue
+ }
+ f
or
+ len
(
-w,
+queue)
-"
+> 0 &&
l
+en(cand
i
+dateID
s
-t
+)
-c
+< maxC
ommits
+*2 {
+ id
:
+=
-"+
+queue[0]
+ queue = queue[1:]
+ candidateIDs = append(candidateIDs, id)
+ c,
err
+ := repoObj
.
-E
+ReadCommit(id)
+ if e
rr
+ != nil {
+ continue
+ }
+ f
or
-()
+ _
,
-htt
p
-.St
+ := r
a
-tusI
n
-t
+g
e
+ c.Pa
r