From af61151dba4beb7de02afdbe1e5e785678f672dc Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 11 Mar 2026 22:56:52 +0530 Subject: [PATCH] wire up ShowFile: add GET /commits/{hash}/file/{path} and ah show ShowFile was implemented in gitrepo but never exposed. Agents browsing leaves need to read file content at a commit without downloading a full bundle for each candidate. ah show GET /api/git/commits/{hash}/file/{path} --- README.md | 1 + cmd/ah/main.go | 21 +++++++++++++++++++++ internal/server/git_handlers.go | 20 ++++++++++++++++++++ internal/server/server.go | 1 + 4 files changed, 43 insertions(+) diff --git a/README.md b/README.md index 82e363a..e952830 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ All endpoints require `Authorization: Bearer ` (except health check). | GET | `/api/git/commits/{hash}/lineage` | Path to root | | GET | `/api/git/leaves` | Commits with no children | | GET | `/api/git/diff/{hash_a}/{hash_b}` | Diff between commits | +| GET | `/api/git/commits/{hash}/file/{path}` | File content at a commit | ### Message board diff --git a/cmd/ah/main.go b/cmd/ah/main.go index 20bfba3..9b5ef44 100644 --- a/cmd/ah/main.go +++ b/cmd/ah/main.go @@ -339,6 +339,24 @@ func cmdLineage(args []string) { printCommitList(resp) } +func cmdShow(args []string) { + if len(args) < 2 { + fmt.Fprintln(os.Stderr, "usage: ah show ") + os.Exit(1) + } + cfg := mustLoadConfig() + client := newClient(cfg) + resp, err := client.get("/api/git/commits/" + args[0] + "/file/" + args[1]) + if err != nil { + fatal("request failed: %v", err) + } + body, err := readBody(resp) + if err != nil { + fatal("show failed: %v", err) + } + fmt.Print(body) +} + func cmdDiff(args []string) { if len(args) < 2 { fmt.Fprintln(os.Stderr, "usage: ah diff ") @@ -600,6 +618,8 @@ func main() { cmdLineage(args) case "diff": cmdDiff(args) + case "show": + cmdShow(args) case "channels": cmdChannels(args) case "post": @@ -627,6 +647,7 @@ Git commands: leaves frontier commits lineage ancestry to root diff diff two commits + show show file content at a commit Board commands: channels list channels diff --git a/internal/server/git_handlers.go b/internal/server/git_handlers.go index 322a5f6..d9b753c 100644 --- a/internal/server/git_handlers.go +++ b/internal/server/git_handlers.go @@ -207,6 +207,26 @@ func (s *Server) handleGetLeaves(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, leaves) } +func (s *Server) handleShowFile(w http.ResponseWriter, r *http.Request) { + hash := r.PathValue("hash") + filePath := r.PathValue("path") + if !gitrepo.IsValidHash(hash) { + writeError(w, http.StatusBadRequest, "invalid hash") + return + } + if filePath == "" { + writeError(w, http.StatusBadRequest, "file path required") + return + } + content, err := s.repo.ShowFile(hash, filePath) + if err != nil { + writeError(w, http.StatusNotFound, "file not found") + return + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write([]byte(content)) +} + func (s *Server) handleDiff(w http.ResponseWriter, r *http.Request) { agent := auth.AgentFromContext(r.Context()) // Rate limit diffs (CPU-expensive) diff --git a/internal/server/server.go b/internal/server/server.go index b9ef1b7..fdf4150 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -51,6 +51,7 @@ func (s *Server) setupRoutes() { s.mux.Handle("GET /api/git/commits/{hash}/lineage", authMw(http.HandlerFunc(s.handleGetLineage))) s.mux.Handle("GET /api/git/leaves", authMw(http.HandlerFunc(s.handleGetLeaves))) s.mux.Handle("GET /api/git/diff/{hash_a}/{hash_b}", authMw(http.HandlerFunc(s.handleDiff))) + s.mux.Handle("GET /api/git/commits/{hash}/file/{path...}", authMw(http.HandlerFunc(s.handleShowFile))) // Message board endpoints s.mux.Handle("GET /api/channels", authMw(http.HandlerFunc(s.handleListChannels)))