package main import ( "encoding/json" "log" "net/http" "strconv" ) // Read-only JSON API for external frontends. Same access model as the rest // of the server: everything here is world-readable, nothing writes. func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") if err := json.NewEncoder(w).Encode(v); err != nil { log.Printf("encode json: %v", err) } } func apiError(w http.ResponseWriter, code int, msg string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) json.NewEncoder(w).Encode(map[string]string{"error": msg}) } func (srv *Server) apiRepo(w http.ResponseWriter, r *http.Request) *RepoInfo { info, err := srv.Store.open(repoParam(r)) if err != nil { apiError(w, http.StatusNotFound, "no such repository") return nil } return info } // refParam returns the requested ref, falling back to the repo default, // or writes a 400 and returns "" when the ref is malformed. func refParam(w http.ResponseWriter, r *http.Request, repo *RepoInfo) string { ref := r.URL.Query().Get("ref") if ref == "" { ref = repo.Default } if !validRef(ref) { apiError(w, http.StatusBadRequest, "invalid ref") return "" } return ref } func (srv *Server) apiRepos(w http.ResponseWriter, r *http.Request) { repos := srv.Store.list() if repos == nil { repos = []*RepoInfo{} } writeJSON(w, repos) } func (srv *Server) apiRepoInfo(w http.ResponseWriter, r *http.Request) { repo := srv.apiRepo(w, r) if repo == nil { return } writeJSON(w, repo) } func (srv *Server) apiTree(w http.ResponseWriter, r *http.Request) { repo := srv.apiRepo(w, r) if repo == nil { return } ref := refParam(w, r, repo) if ref == "" { return } path := r.URL.Query().Get("path") if !validPath(path) { apiError(w, http.StatusBadRequest, "invalid path") return } entries, err := srv.Store.lsTree(r.Context(), repo.Name, ref, path) if err != nil { apiError(w, http.StatusNotFound, "no such tree") return } writeJSON(w, entries) } type blobResponse struct { Path string `json:"path"` Size int64 `json:"size"` Binary bool `json:"binary"` Truncated bool `json:"truncated"` Content string `json:"content"` } func (srv *Server) apiBlob(w http.ResponseWriter, r *http.Request) { repo := srv.apiRepo(w, r) if repo == nil { return } ref := refParam(w, r, repo) if ref == "" { return } path := r.URL.Query().Get("path") if !validPath(path) || path == "" { apiError(w, http.StatusBadRequest, "invalid path") return } blob, err := srv.Store.catBlob(r.Context(), repo.Name, ref, path) if err != nil { apiError(w, http.StatusNotFound, "no such blob") return } resp := blobResponse{Path: path, Size: int64(len(blob))} switch { case isBinary(blob): resp.Binary = true case len(blob) > maxBlobBytes: resp.Truncated = true default: resp.Content = string(blob) } writeJSON(w, resp) } type readmeResponse struct { Name string `json:"name"` Content string `json:"content"` } func (srv *Server) apiReadme(w http.ResponseWriter, r *http.Request) { repo := srv.apiRepo(w, r) if repo == nil { return } ref := refParam(w, r, repo) if ref == "" { return } name, body := srv.Store.readme(r.Context(), repo.Name, ref) if name == "" || isBinary(body) { apiError(w, http.StatusNotFound, "no readme") return } writeJSON(w, readmeResponse{Name: name, Content: string(body)}) } func (srv *Server) apiCommits(w http.ResponseWriter, r *http.Request) { repo := srv.apiRepo(w, r) if repo == nil { return } if repo.Empty { writeJSON(w, []Commit{}) return } ref := refParam(w, r, repo) if ref == "" { return } limit := 100 if n, err := strconv.Atoi(r.URL.Query().Get("n")); err == nil && n > 0 && n <= 500 { limit = n } commits, err := srv.Store.log(r.Context(), repo.Name, ref, limit) if err != nil { apiError(w, http.StatusNotFound, "no such ref") return } if commits == nil { commits = []Commit{} } writeJSON(w, commits) } type commitResponse struct { Commit Patch string `json:"patch"` Truncated bool `json:"truncated"` } func (srv *Server) apiCommit(w http.ResponseWriter, r *http.Request) { repo := srv.apiRepo(w, r) if repo == nil { return } hash := r.PathValue("hash") if !validRef(hash) { apiError(w, http.StatusBadRequest, "invalid hash") return } commit, err := srv.Store.commit(r.Context(), repo.Name, hash) if err != nil { apiError(w, http.StatusNotFound, "no such commit") return } patch, truncated, err := srv.Store.patch(r.Context(), repo.Name, commit.Hash) if err != nil { apiError(w, http.StatusInternalServerError, "patch failed") return } writeJSON(w, commitResponse{Commit: *commit, Patch: patch, Truncated: truncated}) } func (srv *Server) apiRefs(w http.ResponseWriter, r *http.Request) { repo := srv.apiRepo(w, r) if repo == nil { return } branches, _ := srv.Store.refs(r.Context(), repo.Name, "heads") tags, _ := srv.Store.refs(r.Context(), repo.Name, "tags") if branches == nil { branches = []Ref{} } if tags == nil { tags = []Ref{} } writeJSON(w, map[string][]Ref{"branches": branches, "tags": tags}) }