back interdiff of round #2 and #1

appview,knotserver: immutable nix flakeref link header #741

open
opened by boltless.me targeting master from push-ptrrwwvnkmxq
REVERTED
api/tangled/reporesolveRef.go
··· 1 - // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 - 3 - package tangled 4 - 5 - // schema: sh.tangled.repo.resolveRef 6 - 7 - import ( 8 - "bytes" 9 - "context" 10 - 11 - "github.com/bluesky-social/indigo/lex/util" 12 - ) 13 - 14 - const ( 15 - RepoResolveRefNSID = "sh.tangled.repo.resolveRef" 16 - ) 17 - 18 - // RepoResolveRef calls the XRPC method "sh.tangled.repo.resolveRef". 19 - // 20 - // ref: Reference name (branch, tag or other references) 21 - // repo: Repository identifier in format 'did:plc:.../repoName' 22 - func RepoResolveRef(ctx context.Context, c util.LexClient, ref string, repo string) ([]byte, error) { 23 - buf := new(bytes.Buffer) 24 - 25 - params := map[string]interface{}{} 26 - params["ref"] = ref 27 - params["repo"] = repo 28 - if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.resolveRef", params, nil, buf); err != nil { 29 - return nil, err 30 - } 31 - 32 - return buf.Bytes(), nil 33 - }
REVERTED
appview/repo/repo.go
··· 110 110 } 111 111 112 112 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 113 + archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo) 113 - // TODO: we are requesting the knot twice here to get permanent commit-hash. 114 - // This should purely handled from knot instead. 115 - rawHash, err := tangled.RepoResolveRef(r.Context(), xrpcc, ref, repo) 116 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 117 - l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 118 - rp.pages.Error503(w) 119 - return 120 - } 121 - hash := string(rawHash) 122 - immutableLink := fmt.Sprintf( 123 - "%s/%s/archive/%s", 124 - rp.config.Core.AppviewHost, 125 - repo, 126 - hash, 127 - ) 128 - 129 - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", hash, repo) 130 114 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 131 115 l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 132 116 rp.pages.Error503(w) ··· 139 123 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 140 124 w.Header().Set("Content-Type", "application/gzip") 141 125 w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 142 - w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) 143 126 144 127 // Write the archive data directly 145 128 w.Write(archiveBytes)
ERROR
knotserver/git/git.go

Failed to calculate interdiff for this file.

REVERTED
knotserver/xrpc/repo_resolve_ref.go
··· 1 - package xrpc 2 - 3 - import ( 4 - "fmt" 5 - "net/http" 6 - 7 - "tangled.org/core/knotserver/git" 8 - xrpcerr "tangled.org/core/xrpc/errors" 9 - ) 10 - 11 - func (x *Xrpc) RepoResolveRef(w http.ResponseWriter, r *http.Request) { 12 - repo := r.URL.Query().Get("repo") 13 - repoPath, err := x.parseRepoParam(repo) 14 - if err != nil { 15 - writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 16 - return 17 - } 18 - 19 - ref := r.URL.Query().Get("ref") 20 - // ref can be empty (git.Open handles this) 21 - 22 - gr, err := git.Open(repoPath, ref) 23 - if err != nil { 24 - x.Logger.Error("failed to open", "error", err) 25 - writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 26 - return 27 - } 28 - 29 - w.Header().Set("Content-Type", "text/plain") 30 - fmt.Fprint(w, gr.Hash().String()) 31 - }
ERROR
knotserver/xrpc/xrpc.go

Failed to calculate interdiff for this file.

REVERTED
lexicons/repo/resolveRef.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.repo.resolveRef", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Resolve a ref to its corresponding commit hash", 8 - "parameters": { 9 - "type": "params", 10 - "required": ["repo", "ref"], 11 - "properties": { 12 - "repo": { 13 - "type": "string", 14 - "description": "Repository identifier in format 'did:plc:.../repoName'" 15 - }, 16 - "ref": { 17 - "type": "string", 18 - "description": "Reference name (branch, tag or other references)" 19 - } 20 - } 21 - }, 22 - "output": { 23 - "encoding": "*/*", 24 - "description": "Resolved hash" 25 - }, 26 - "errors": [ 27 - { 28 - "name": "RepoNotFound", 29 - "description": "Repository not found or access denied" 30 - }, 31 - { 32 - "name": "RefNotFound", 33 - "description": "Ref not found" 34 - }, 35 - { 36 - "name": "InvalidRequest", 37 - "description": "Invalid request parameters" 38 - } 39 - ] 40 - } 41 - } 42 - }
NEW
api/tangled/repoarchive.go
··· 1 - // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 - 3 - package tangled 4 - 5 - // schema: sh.tangled.repo.archive 6 - 7 - import ( 8 - "bytes" 9 - "context" 10 - 11 - "github.com/bluesky-social/indigo/lex/util" 12 - ) 13 - 14 - const ( 15 - RepoArchiveNSID = "sh.tangled.repo.archive" 16 - ) 17 - 18 - // RepoArchive calls the XRPC method "sh.tangled.repo.archive". 19 - // 20 - // format: Archive format 21 - // prefix: Prefix for files in the archive 22 - // ref: Git reference (branch, tag, or commit SHA) 23 - // repo: Repository identifier in format 'did:plc:.../repoName' 24 - func RepoArchive(ctx context.Context, c util.LexClient, format string, prefix string, ref string, repo string) ([]byte, error) { 25 - buf := new(bytes.Buffer) 26 - 27 - params := map[string]interface{}{} 28 - if format != "" { 29 - params["format"] = format 30 - } 31 - if prefix != "" { 32 - params["prefix"] = prefix 33 - } 34 - params["ref"] = ref 35 - params["repo"] = repo 36 - if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.archive", params, nil, buf); err != nil { 37 - return nil, err 38 - } 39 - 40 - return buf.Bytes(), nil 41 - }
NEW
appview/repo/archive.go
··· 1 - package repo 2 - 3 - import ( 4 - "fmt" 5 - "net/http" 6 - "net/url" 7 - "strings" 8 - 9 - "tangled.org/core/api/tangled" 10 - xrpcclient "tangled.org/core/appview/xrpcclient" 11 - 12 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 13 - "github.com/go-chi/chi/v5" 14 - "github.com/go-git/go-git/v5/plumbing" 15 - ) 16 - 17 - func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 18 - l := rp.logger.With("handler", "DownloadArchive") 19 - ref := chi.URLParam(r, "ref") 20 - ref, _ = url.PathUnescape(ref) 21 - f, err := rp.repoResolver.Resolve(r) 22 - if err != nil { 23 - l.Error("failed to get repo and knot", "err", err) 24 - return 25 - } 26 - scheme := "http" 27 - if !rp.config.Core.Dev { 28 - scheme = "https" 29 - } 30 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 31 - xrpcc := &indigoxrpc.Client{ 32 - Host: host, 33 - } 34 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 35 - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo) 36 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 37 - l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 38 - rp.pages.Error503(w) 39 - return 40 - } 41 - // Set headers for file download, just pass along whatever the knot specifies 42 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 43 - filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename) 44 - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 45 - w.Header().Set("Content-Type", "application/gzip") 46 - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 47 - // Write the archive data directly 48 - w.Write(archiveBytes) 49 - }
NEW
appview/repo/router.go
··· 40 40 r.Get("/blob/{ref}/*", rp.Blob) 41 41 r.Get("/raw/{ref}/*", rp.RepoBlobRaw) 42 42 43 - // intentionally doesn't use /* as this isn't 44 - // a file path 45 - r.Get("/archive/{ref}", rp.DownloadArchive) 46 - 47 43 r.Route("/fork", func(r chi.Router) { 48 44 r.Use(middleware.AuthMiddleware(rp.oauth)) 49 45 r.Get("/", rp.ForkRepo)
NEW
appview/state/git_http.go
··· 1 - package state 2 - 3 - import ( 4 - "fmt" 5 - "io" 6 - "maps" 7 - "net/http" 8 - 9 - "github.com/bluesky-social/indigo/atproto/identity" 10 - "github.com/go-chi/chi/v5" 11 - "tangled.org/core/appview/models" 12 - ) 13 - 14 - func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 15 - user := r.Context().Value("resolvedId").(identity.Identity) 16 - repo := r.Context().Value("repo").(*models.Repo) 17 - 18 - scheme := "https" 19 - if s.config.Core.Dev { 20 - scheme = "http" 21 - } 22 - 23 - targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 24 - s.proxyRequest(w, r, targetURL) 25 - 26 - } 27 - 28 - func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 29 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 - if !ok { 31 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 32 - return 33 - } 34 - repo := r.Context().Value("repo").(*models.Repo) 35 - 36 - scheme := "https" 37 - if s.config.Core.Dev { 38 - scheme = "http" 39 - } 40 - 41 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 - s.proxyRequest(w, r, targetURL) 43 - } 44 - 45 - func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { 46 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 47 - if !ok { 48 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 49 - return 50 - } 51 - repo := r.Context().Value("repo").(*models.Repo) 52 - 53 - scheme := "https" 54 - if s.config.Core.Dev { 55 - scheme = "http" 56 - } 57 - 58 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 59 - s.proxyRequest(w, r, targetURL) 60 - } 61 - 62 - func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) { 63 - client := &http.Client{} 64 - 65 - // Create new request 66 - proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body) 67 - if err != nil { 68 - http.Error(w, err.Error(), http.StatusInternalServerError) 69 - return 70 - } 71 - 72 - // Copy original headers 73 - proxyReq.Header = r.Header 74 - 75 - repoOwnerHandle := chi.URLParam(r, "user") 76 - proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 77 - 78 - // Execute request 79 - resp, err := client.Do(proxyReq) 80 - if err != nil { 81 - http.Error(w, err.Error(), http.StatusInternalServerError) 82 - return 83 - } 84 - defer resp.Body.Close() 85 - 86 - // Copy response headers 87 - maps.Copy(w.Header(), resp.Header) 88 - 89 - // Set response status code 90 - w.WriteHeader(resp.StatusCode) 91 - 92 - // Copy response body 93 - if _, err := io.Copy(w, resp.Body); err != nil { 94 - http.Error(w, err.Error(), http.StatusInternalServerError) 95 - return 96 - } 97 - }
NEW
appview/state/proxy_knot.go
··· 1 + package state 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "maps" 7 + "net/http" 8 + 9 + "github.com/bluesky-social/indigo/atproto/identity" 10 + "github.com/go-chi/chi/v5" 11 + "tangled.org/core/appview/models" 12 + ) 13 + 14 + func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 15 + user := r.Context().Value("resolvedId").(identity.Identity) 16 + repo := r.Context().Value("repo").(*models.Repo) 17 + 18 + scheme := "https" 19 + if s.config.Core.Dev { 20 + scheme = "http" 21 + } 22 + 23 + targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 24 + s.proxyRequest(w, r, targetURL) 25 + 26 + } 27 + 28 + func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 29 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 + if !ok { 31 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 32 + return 33 + } 34 + repo := r.Context().Value("repo").(*models.Repo) 35 + 36 + scheme := "https" 37 + if s.config.Core.Dev { 38 + scheme = "http" 39 + } 40 + 41 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 + s.proxyRequest(w, r, targetURL) 43 + } 44 + 45 + func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { 46 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 47 + if !ok { 48 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 49 + return 50 + } 51 + repo := r.Context().Value("repo").(*models.Repo) 52 + 53 + scheme := "https" 54 + if s.config.Core.Dev { 55 + scheme = "http" 56 + } 57 + 58 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 59 + s.proxyRequest(w, r, targetURL) 60 + } 61 + 62 + func (s *State) DownloadArchive(w http.ResponseWriter, r *http.Request) { 63 + ref := chi.URLParam(r, "ref") 64 + 65 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 66 + if !ok { 67 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 68 + return 69 + } 70 + repo := r.Context().Value("repo").(*models.Repo) 71 + 72 + scheme := "https" 73 + if s.config.Core.Dev { 74 + scheme = "http" 75 + } 76 + 77 + targetURL := fmt.Sprintf("%s://%s/%s/%s/archive/%s", scheme, repo.Knot, user.DID, repo.Name, ref) 78 + s.proxyRequest(w, r, targetURL) 79 + } 80 + 81 + func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) { 82 + client := &http.Client{} 83 + 84 + // Create new request 85 + proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body) 86 + if err != nil { 87 + http.Error(w, err.Error(), http.StatusInternalServerError) 88 + return 89 + } 90 + 91 + // Copy original headers 92 + proxyReq.Header = r.Header 93 + 94 + repoOwnerHandle := chi.URLParam(r, "user") 95 + proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 96 + 97 + // Execute request 98 + resp, err := client.Do(proxyReq) 99 + if err != nil { 100 + http.Error(w, err.Error(), http.StatusInternalServerError) 101 + return 102 + } 103 + defer resp.Body.Close() 104 + 105 + // Copy response headers 106 + maps.Copy(w.Header(), resp.Header) 107 + 108 + // Set response status code 109 + w.WriteHeader(resp.StatusCode) 110 + 111 + // Copy response body 112 + if _, err := io.Copy(w, resp.Body); err != nil { 113 + http.Error(w, err.Error(), http.StatusInternalServerError) 114 + return 115 + } 116 + }
NEW
appview/state/router.go
··· 100 100 r.Get("/info/refs", s.InfoRefs) 101 101 r.Post("/git-upload-pack", s.UploadPack) 102 102 r.Post("/git-receive-pack", s.ReceivePack) 103 - 103 + // intentionally doesn't use /* as this isn't 104 + // a file path 105 + r.Get("/archive/{ref}", s.DownloadArchive) 104 106 }) 105 107 }) 106 108
NEW
knotserver/archive.go
··· 1 + package knotserver 2 + 3 + import ( 4 + "compress/gzip" 5 + "fmt" 6 + "net/http" 7 + "strings" 8 + 9 + securejoin "github.com/cyphar/filepath-securejoin" 10 + "github.com/go-chi/chi/v5" 11 + "github.com/go-git/go-git/v5/plumbing" 12 + "tangled.org/core/knotserver/git" 13 + ) 14 + 15 + func (h *Knot) Archive(w http.ResponseWriter, r *http.Request) { 16 + var ( 17 + did = chi.URLParam(r, "did") 18 + name = chi.URLParam(r, "name") 19 + ref = chi.URLParam(r, "ref") 20 + ) 21 + repo, err := securejoin.SecureJoin(did, name) 22 + if err != nil { 23 + gitError(w, "repository not found", http.StatusNotFound) 24 + h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 25 + return 26 + } 27 + 28 + repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repo) 29 + if err != nil { 30 + gitError(w, "repository not found", http.StatusNotFound) 31 + h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 32 + return 33 + } 34 + 35 + gr, err := git.Open(repoPath, ref) 36 + 37 + immutableLink := fmt.Sprintf( 38 + "https://%s/%s/%s/archive/%s", 39 + h.c.Server.Hostname, 40 + did, 41 + name, 42 + gr.Hash(), 43 + ) 44 + 45 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 46 + filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename) 47 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 48 + w.Header().Set("Content-Type", "application/gzip") 49 + w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) 50 + 51 + gw := gzip.NewWriter(w) 52 + defer gw.Close() 53 + 54 + err = gr.WriteTar(gw, "") 55 + if err != nil { 56 + // once we start writing to the body we can't report error anymore 57 + // so we are only left with logging the error 58 + h.l.Error("writing tar file", "error", err) 59 + return 60 + } 61 + 62 + err = gw.Flush() 63 + if err != nil { 64 + // once we start writing to the body we can't report error anymore 65 + // so we are only left with logging the error 66 + h.l.Error("flushing", "error", err.Error()) 67 + return 68 + } 69 + }
NEW
knotserver/router.go
··· 84 84 r.Get("/info/refs", h.InfoRefs) 85 85 r.Post("/git-upload-pack", h.UploadPack) 86 86 r.Post("/git-receive-pack", h.ReceivePack) 87 + // convenience routes 88 + r.Get("/archive/{ref}", h.Archive) 87 89 }) 88 90 }) 89 91
NEW
knotserver/xrpc/repo_archive.go
··· 1 - package xrpc 2 - 3 - import ( 4 - "compress/gzip" 5 - "fmt" 6 - "net/http" 7 - "strings" 8 - 9 - "github.com/go-git/go-git/v5/plumbing" 10 - 11 - "tangled.org/core/knotserver/git" 12 - xrpcerr "tangled.org/core/xrpc/errors" 13 - ) 14 - 15 - func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) { 16 - repo := r.URL.Query().Get("repo") 17 - repoPath, err := x.parseRepoParam(repo) 18 - if err != nil { 19 - writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 20 - return 21 - } 22 - 23 - ref := r.URL.Query().Get("ref") 24 - // ref can be empty (git.Open handles this) 25 - 26 - format := r.URL.Query().Get("format") 27 - if format == "" { 28 - format = "tar.gz" // default 29 - } 30 - 31 - prefix := r.URL.Query().Get("prefix") 32 - 33 - if format != "tar.gz" { 34 - writeError(w, xrpcerr.NewXrpcError( 35 - xrpcerr.WithTag("InvalidRequest"), 36 - xrpcerr.WithMessage("only tar.gz format is supported"), 37 - ), http.StatusBadRequest) 38 - return 39 - } 40 - 41 - gr, err := git.Open(repoPath, ref) 42 - if err != nil { 43 - writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 44 - return 45 - } 46 - 47 - repoParts := strings.Split(repo, "/") 48 - repoName := repoParts[len(repoParts)-1] 49 - 50 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 51 - 52 - var archivePrefix string 53 - if prefix != "" { 54 - archivePrefix = prefix 55 - } else { 56 - archivePrefix = fmt.Sprintf("%s-%s", repoName, safeRefFilename) 57 - } 58 - 59 - filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) 60 - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 61 - w.Header().Set("Content-Type", "application/gzip") 62 - 63 - gw := gzip.NewWriter(w) 64 - defer gw.Close() 65 - 66 - err = gr.WriteTar(gw, archivePrefix) 67 - if err != nil { 68 - // once we start writing to the body we can't report error anymore 69 - // so we are only left with logging the error 70 - x.Logger.Error("writing tar file", "error", err.Error()) 71 - return 72 - } 73 - 74 - err = gw.Flush() 75 - if err != nil { 76 - // once we start writing to the body we can't report error anymore 77 - // so we are only left with logging the error 78 - x.Logger.Error("flushing", "error", err.Error()) 79 - return 80 - } 81 - }
NEW
lexicons/repo/archive.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.repo.archive", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "parameters": { 8 - "type": "params", 9 - "required": ["repo", "ref"], 10 - "properties": { 11 - "repo": { 12 - "type": "string", 13 - "description": "Repository identifier in format 'did:plc:.../repoName'" 14 - }, 15 - "ref": { 16 - "type": "string", 17 - "description": "Git reference (branch, tag, or commit SHA)" 18 - }, 19 - "format": { 20 - "type": "string", 21 - "description": "Archive format", 22 - "enum": ["tar", "zip", "tar.gz", "tar.bz2", "tar.xz"], 23 - "default": "tar.gz" 24 - }, 25 - "prefix": { 26 - "type": "string", 27 - "description": "Prefix for files in the archive" 28 - } 29 - } 30 - }, 31 - "output": { 32 - "encoding": "*/*", 33 - "description": "Binary archive data" 34 - }, 35 - "errors": [ 36 - { 37 - "name": "RepoNotFound", 38 - "description": "Repository not found or access denied" 39 - }, 40 - { 41 - "name": "RefNotFound", 42 - "description": "Git reference not found" 43 - }, 44 - { 45 - "name": "InvalidRequest", 46 - "description": "Invalid request parameters" 47 - }, 48 - { 49 - "name": "ArchiveError", 50 - "description": "Failed to create archive" 51 - } 52 - ] 53 - } 54 - } 55 - }