mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
feat(grasp06): /prs/ endpoint reachable with empty-repo fetch
Adds the `/prs/<npub>/<identifier>.git/*` URL space behind the existing
`NGIT_GRASP06_ENABLE` flag. Fetches synthesise an empty bare repo per
request (via `git init --bare` in a tempdir) when no real repo exists
on disk; if a real repo exists at `<git_data_path>/prs/<hex>/<id>.git`
it delegates to the standard `handle_info_refs` / `handle_upload_pack`.
The receive-pack endpoint is a deliberate stub that returns HTTP 200
with an ERR pkt-line ("GRASP-06 receive-pack not yet implemented"),
matching `build_git_protocol_error_response`'s shape so git clients
print the message and exit non-zero rather than seeing a 404. The real
receive-pack handler is not yet implemented.
Module layout under `src/grasp06/`:
paths.rs — PRS_URL_PREFIX, PRS_DISK_PREFIX, prs_repo_path,
prs_base_path, is_prs_repo_path (hex on disk).
endpoint.rs — PrsUrl + parse_prs_url (npub-only, percent-decoded id).
fetch.rs — handle_prs_info_refs, handle_prs_upload_pack, and the
receive-pack stub.
Routing is wired into HttpService::call ahead of `parse_git_url` so
`/prs/...` paths can't be misclassified as standard owner repos. When
the flag is off the branch is skipped and `/prs/*` falls through to
the existing 404 path (preserving the discovery-gate contract).
Tests flipped green:
- prs_fetch_unknown_path_serves_empty_repo_with_grasp_06
- prs_push_other_refs_rejected_with_grasp_06 (ERR pkt-line)
- standard_push_does_not_mirror_to_prs_with_grasp_06
Tests still green (no regression):
- prs_namespace_404_when_grasp06_not_advertised_no_grasp_06
- nip11_advertises_grasp_06_when_enabled
- pr_event_rejected_when_clone_tag_does_not_name_prs_endpoint_with_grasp_06
Tests still red (deferred):
- prs_push_refs_nostr_event_id_accepted_with_grasp_06
- pr_event_accepted_when_clone_tag_names_prs_endpoint_with_grasp_06
- prs_push_mirrors_to_announced_repo_with_grasp_06
`tempfile` is promoted from a dev-dependency to a runtime dependency
because the empty-repo synthesis path needs it. No new config options,
so the four-place config-sync contract is unaffected.
This commit is contained in:
@@ -57,6 +57,9 @@ anyhow = "1.0"
|
||||
# Async traits
|
||||
async-trait = "0.1"
|
||||
|
||||
# Temporary directories (used for GRASP-06 empty-repo synthesis)
|
||||
tempfile = "3"
|
||||
|
||||
# Git (for future use)
|
||||
# git-http-backend = "0.3"
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//! URL parsing for the GRASP-06 `/prs/` endpoint.
|
||||
//!
|
||||
//! Per the spec, the path layout is:
|
||||
//!
|
||||
//! ```text
|
||||
//! /prs/<npub>/<identifier>.git/<subpath>
|
||||
//! ```
|
||||
//!
|
||||
//! The submitter is always supplied as a bech32 `npub1...` — hex pubkeys
|
||||
//! are rejected per the spec. The identifier is percent-decoded so URLs
|
||||
//! like `/prs/<npub>/my%20repo.git/info/refs` resolve to the same
|
||||
//! identifier as the standard endpoint stores on disk.
|
||||
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
use crate::git::percent_decode;
|
||||
use crate::grasp06::paths::PRS_URL_PREFIX;
|
||||
|
||||
/// A parsed `/prs/<npub>/<id>.git/<subpath>` URL.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrsUrl {
|
||||
/// The submitter's public key, decoded from the `npub1...` URL segment.
|
||||
pub submitter: PublicKey,
|
||||
/// The percent-decoded NIP-34 `d` identifier (no `.git` suffix).
|
||||
pub identifier: String,
|
||||
/// Everything after `.git/` in the URL path. May be empty.
|
||||
pub subpath: String,
|
||||
}
|
||||
|
||||
/// Parse a `/prs/<npub>/<identifier>.git/<subpath>` URL.
|
||||
///
|
||||
/// Returns `None` if the path is not in the `/prs/` URL space, if the
|
||||
/// `<npub>` segment is not a valid bech32 npub, or if the second segment
|
||||
/// does not end in `.git`. Identifiers are percent-decoded.
|
||||
///
|
||||
/// This intentionally rejects hex pubkeys: GRASP-06 specifies the URL
|
||||
/// uses an `npub`, and accepting hex would silently change the on-disk
|
||||
/// path layout assumed by [`crate::grasp06::paths::prs_repo_path`].
|
||||
pub fn parse_prs_url(path: &str) -> Option<PrsUrl> {
|
||||
let path = path.strip_prefix('/').unwrap_or(path);
|
||||
|
||||
// Require the literal `prs/` prefix.
|
||||
let rest = path.strip_prefix(PRS_URL_PREFIX)?;
|
||||
let rest = rest.strip_prefix('/')?;
|
||||
|
||||
// Split off `<npub>/<id>.git/<subpath>` (subpath may be empty).
|
||||
let mut parts = rest.splitn(3, '/');
|
||||
let npub_segment = parts.next()?;
|
||||
let repo_segment = parts.next()?;
|
||||
let subpath = parts.next().unwrap_or("").to_string();
|
||||
|
||||
if npub_segment.is_empty() || repo_segment.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Spec is npub-only: reject hex (and anything else that isn't a
|
||||
// valid bech32 npub).
|
||||
if !npub_segment.starts_with("npub1") {
|
||||
return None;
|
||||
}
|
||||
let submitter = PublicKey::from_bech32(npub_segment).ok()?;
|
||||
|
||||
// `<identifier>.git` (URL-encoded). Decode then strip the suffix.
|
||||
let decoded = percent_decode(repo_segment);
|
||||
let identifier = decoded.strip_suffix(".git")?.to_string();
|
||||
if identifier.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(PrsUrl {
|
||||
submitter,
|
||||
identifier,
|
||||
subpath,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A valid bech32 npub for use in tests (generated once, deterministic).
|
||||
fn sample_npub() -> String {
|
||||
let keys = Keys::generate();
|
||||
keys.public_key().to_bech32().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_info_refs() {
|
||||
let npub = sample_npub();
|
||||
let path = format!("/prs/{}/my-repo.git/info/refs", npub);
|
||||
let parsed = parse_prs_url(&path).expect("should parse");
|
||||
assert_eq!(parsed.identifier, "my-repo");
|
||||
assert_eq!(parsed.subpath, "info/refs");
|
||||
assert_eq!(parsed.submitter.to_bech32().unwrap(), npub);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_with_empty_subpath() {
|
||||
// Tolerate trailing `.git` with no subpath (e.g. `/prs/<npub>/<id>.git/`).
|
||||
let npub = sample_npub();
|
||||
let path = format!("/prs/{}/my-repo.git/", npub);
|
||||
let parsed = parse_prs_url(&path).expect("should parse");
|
||||
assert_eq!(parsed.subpath, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_trailing_dot_git() {
|
||||
let npub = sample_npub();
|
||||
assert!(parse_prs_url(&format!("/prs/{}/my-repo/info/refs", npub)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_hex_pubkey() {
|
||||
// 64-hex is a valid pubkey form but the spec wants bech32.
|
||||
let hex = "0".repeat(64);
|
||||
assert!(parse_prs_url(&format!("/prs/{}/my-repo.git/info/refs", hex)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_paths_outside_prs() {
|
||||
let npub = sample_npub();
|
||||
assert!(parse_prs_url(&format!("/{}/my-repo.git/info/refs", npub)).is_none());
|
||||
assert!(parse_prs_url("/").is_none());
|
||||
assert!(parse_prs_url("/prs/").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_decodes_identifier() {
|
||||
let npub = sample_npub();
|
||||
let path = format!("/prs/{}/my%20repo.git/info/refs", npub);
|
||||
let parsed = parse_prs_url(&path).expect("should parse");
|
||||
assert_eq!(parsed.identifier, "my repo");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! GRASP-06 `/prs/` fetch handlers and receive-pack stub.
|
||||
//!
|
||||
//! Spec 06.md line 13:
|
||||
//!
|
||||
//! > MUST respond to upload-pack requests for any well-formed path as if
|
||||
//! > serving an empty bare repository.
|
||||
//!
|
||||
//! When a real `/prs/<submitter>/<identifier>.git` repo exists on disk
|
||||
//! we delegate to the standard handlers in [`crate::git::handlers`].
|
||||
//! Otherwise we synthesise a brand-new empty bare repo in a per-request
|
||||
//! temporary directory and run the standard upload-pack against that.
|
||||
//!
|
||||
//! Receive-pack is intentionally a stub for now: it returns an HTTP 200
|
||||
//! response carrying an `ERR` pkt-line. The real handler is not yet
|
||||
//! implemented.
|
||||
|
||||
use http_body_util::Full;
|
||||
use hyper::body::Bytes;
|
||||
use hyper::{Response, StatusCode};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::git::handlers::{handle_info_refs, handle_upload_pack, GitError};
|
||||
use crate::git::protocol::{GitService, PktLine};
|
||||
use crate::grasp06::endpoint::PrsUrl;
|
||||
use crate::grasp06::paths::prs_repo_path;
|
||||
|
||||
/// Handle `GET /prs/<npub>/<id>.git/info/refs?service=...`.
|
||||
///
|
||||
/// Used for both `git-upload-pack` (clone/fetch) discovery and
|
||||
/// `git-receive-pack` discovery: in both cases we advertise the refs of
|
||||
/// an empty bare repo when no real repo exists at the requested path,
|
||||
/// so that `git push` can proceed to its POST step and be rejected by
|
||||
/// the receive-pack stub via an ERR pkt-line.
|
||||
pub async fn handle_prs_info_refs(
|
||||
prs: &PrsUrl,
|
||||
git_data_path: &str,
|
||||
service: GitService,
|
||||
git_protocol: Option<&str>,
|
||||
) -> Result<Response<Full<Bytes>>, GitError> {
|
||||
let real_repo = prs_repo_path(
|
||||
Path::new(git_data_path),
|
||||
&prs.submitter.to_hex(),
|
||||
&prs.identifier,
|
||||
);
|
||||
|
||||
if real_repo.exists() {
|
||||
debug!(
|
||||
"/prs/ info/refs: real repo found at {} — delegating",
|
||||
real_repo.display()
|
||||
);
|
||||
return handle_info_refs(real_repo, service, git_protocol).await;
|
||||
}
|
||||
|
||||
debug!(
|
||||
"/prs/ info/refs: synthesising empty bare repo for prs={}/{}",
|
||||
prs.submitter.to_hex(),
|
||||
prs.identifier
|
||||
);
|
||||
let temp = init_empty_bare_repo()?;
|
||||
let repo_path = temp.path().to_path_buf();
|
||||
let response = handle_info_refs(repo_path, service, git_protocol).await;
|
||||
// `temp` is dropped here, deleting the directory. Git has already
|
||||
// read everything it needs by the time `handle_info_refs` returns.
|
||||
drop(temp);
|
||||
response
|
||||
}
|
||||
|
||||
/// Handle `POST /prs/<npub>/<id>.git/git-upload-pack`.
|
||||
///
|
||||
/// Same synthesise-or-delegate behaviour as [`handle_prs_info_refs`].
|
||||
pub async fn handle_prs_upload_pack(
|
||||
prs: &PrsUrl,
|
||||
git_data_path: &str,
|
||||
body: Bytes,
|
||||
git_protocol: Option<&str>,
|
||||
) -> Result<Response<Full<Bytes>>, GitError> {
|
||||
let real_repo = prs_repo_path(
|
||||
Path::new(git_data_path),
|
||||
&prs.submitter.to_hex(),
|
||||
&prs.identifier,
|
||||
);
|
||||
|
||||
if real_repo.exists() {
|
||||
debug!(
|
||||
"/prs/ upload-pack: real repo found at {} — delegating",
|
||||
real_repo.display()
|
||||
);
|
||||
return handle_upload_pack(real_repo, body, git_protocol).await;
|
||||
}
|
||||
|
||||
debug!(
|
||||
"/prs/ upload-pack: synthesising empty bare repo for prs={}/{}",
|
||||
prs.submitter.to_hex(),
|
||||
prs.identifier
|
||||
);
|
||||
let temp = init_empty_bare_repo()?;
|
||||
let repo_path = temp.path().to_path_buf();
|
||||
let response = handle_upload_pack(repo_path, body, git_protocol).await;
|
||||
drop(temp);
|
||||
response
|
||||
}
|
||||
|
||||
/// Stub `POST /prs/<npub>/<id>.git/git-receive-pack`.
|
||||
///
|
||||
/// Returns HTTP 200 with an `ERR` pkt-line so that git clients display
|
||||
/// the message and exit non-zero. The real receive-pack handler (with
|
||||
/// ref-name validation, init-on-push, and purgatory wiring) is not yet
|
||||
/// implemented.
|
||||
pub fn handle_prs_receive_pack_stub() -> Response<Full<Bytes>> {
|
||||
build_receive_pack_err("GRASP-06 receive-pack not yet implemented")
|
||||
}
|
||||
|
||||
/// Build an HTTP 200 response carrying a `git-receive-pack` ERR pkt-line.
|
||||
///
|
||||
/// Per the git smart-HTTP protocol, application-level rejections are
|
||||
/// communicated as a single `PKT-LINE("ERR " <message>)` packet on a
|
||||
/// 200 response. A 4xx/5xx response would prevent the client from
|
||||
/// parsing and displaying the message. The shape here mirrors
|
||||
/// `build_git_protocol_error_response` in [`crate::git::handlers`].
|
||||
fn build_receive_pack_err(message: &str) -> Response<Full<Bytes>> {
|
||||
let payload = format!("ERR {}\n", message.trim());
|
||||
let pktline = PktLine::data(payload.as_bytes()).encode();
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
"content-type",
|
||||
GitService::ReceivePack.result_content_type(),
|
||||
)
|
||||
.header("cache-control", "no-cache")
|
||||
.body(Full::new(Bytes::from(pktline)))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Create a fresh empty bare repo in a temp directory.
|
||||
///
|
||||
/// Per-request temp dirs are deliberate. They are cheap on
|
||||
/// any reasonable filesystem (one `mkdir`, one `git init --bare`) and
|
||||
/// avoid any concurrency hazards a shared template would introduce. If
|
||||
/// profiling shows this is a bottleneck we can switch to a shared
|
||||
/// `<git_data_path>/prs/.empty-template.git`; do not optimise until
|
||||
/// measurable.
|
||||
fn init_empty_bare_repo() -> Result<TempDir, GitError> {
|
||||
let temp = TempDir::new().map_err(GitError::IoError)?;
|
||||
let path = temp.path();
|
||||
let output = Command::new("git")
|
||||
.args(["init", "--bare", "--quiet"])
|
||||
.arg(path)
|
||||
.output()
|
||||
.map_err(GitError::ProcessSpawnFailed)?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
warn!(
|
||||
"/prs/ empty-repo synthesis: git init --bare failed in {}: {}",
|
||||
path.display(),
|
||||
stderr.trim()
|
||||
);
|
||||
return Err(GitError::GitFailed(output.status.code()));
|
||||
}
|
||||
Ok(temp)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! GRASP-06 — Contributor Pull Request Submission.
|
||||
//!
|
||||
//! Spec: <https://github.com/DanConwayDev/grasp/blob/main/06.md>
|
||||
//! Design: `docs/explanation/grasp-06-contributor-pr-submission.md`
|
||||
//!
|
||||
//! This module is gated behind [`crate::config::Config::grasp06_enable`].
|
||||
//! When disabled, `/prs/*` requests fall through to the standard 404 path.
|
||||
//!
|
||||
//! ## Current scope
|
||||
//!
|
||||
//! - URL parsing for `/prs/<npub>/<identifier>.git/<subpath>`.
|
||||
//! - On-disk path conventions for future phases.
|
||||
//! - Empty-bare-repo synthesis for `info/refs` and `git-upload-pack`.
|
||||
//! - A stub `git-receive-pack` handler that returns an HTTP 200 ERR pkt-line.
|
||||
//!
|
||||
//! Real receive-pack, purgatory scoping, event-acceptance relaxation, and
|
||||
//! cross-service mirroring are not yet implemented.
|
||||
|
||||
pub mod endpoint;
|
||||
pub mod fetch;
|
||||
pub mod paths;
|
||||
@@ -0,0 +1,89 @@
|
||||
//! On-disk path conventions for the GRASP-06 `/prs/` endpoint.
|
||||
//!
|
||||
//! Repositories submitted via `/prs/<npub>/<identifier>.git` are stored
|
||||
//! under a dedicated subtree of the git data directory so existing
|
||||
//! subsystems (cleanup, sync, listings) can exclude them with a single
|
||||
//! path-prefix check. The submitter pubkey is stored as **hex** on disk;
|
||||
//! the identifier is stored verbatim (URL percent-decoding happens at
|
||||
//! parse time in [`crate::grasp06::endpoint`]).
|
||||
//!
|
||||
//! ```text
|
||||
//! <git_data_path>/
|
||||
//! prs/ <-- PRS_DISK_PREFIX
|
||||
//! <submitter-hex>/
|
||||
//! <identifier>.git/
|
||||
//! ```
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// URL prefix used in HTTP paths: `/prs/<npub>/<identifier>.git/...`.
|
||||
pub const PRS_URL_PREFIX: &str = "prs";
|
||||
|
||||
/// Directory name used on disk under `<git_data_path>`. Kept identical to
|
||||
/// `PRS_URL_PREFIX` so an operator scanning the filesystem can match the
|
||||
/// URL space at a glance.
|
||||
pub const PRS_DISK_PREFIX: &str = "prs";
|
||||
|
||||
/// Return the base directory under which all `/prs/` repos live.
|
||||
pub fn prs_base_path(git_data_path: &Path) -> PathBuf {
|
||||
git_data_path.join(PRS_DISK_PREFIX)
|
||||
}
|
||||
|
||||
/// Build the on-disk path for `/prs/<npub>/<identifier>.git`.
|
||||
///
|
||||
/// `submitter_hex` is the 64-character lowercase hex of the submitter's
|
||||
/// public key. `identifier` is the percent-decoded NIP-34 `d` value as
|
||||
/// stored on disk (verbatim).
|
||||
pub fn prs_repo_path(git_data_path: &Path, submitter_hex: &str, identifier: &str) -> PathBuf {
|
||||
let identifier = identifier.strip_suffix(".git").unwrap_or(identifier);
|
||||
prs_base_path(git_data_path)
|
||||
.join(submitter_hex)
|
||||
.join(format!("{}.git", identifier))
|
||||
}
|
||||
|
||||
/// True if `path` is inside the `/prs/` subtree of `git_data_path`.
|
||||
///
|
||||
/// Used by future cleanup and sync subsystems to skip these repos —
|
||||
/// they have their own lifecycle (zero-ref cleanup, no announcement
|
||||
/// promotion) and must not be treated as standard owner repos.
|
||||
pub fn is_prs_repo_path(path: &Path, git_data_path: &Path) -> bool {
|
||||
path.starts_with(prs_base_path(git_data_path))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prs_base_path_appends_prs() {
|
||||
assert_eq!(
|
||||
prs_base_path(Path::new("/data/git")),
|
||||
PathBuf::from("/data/git/prs")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prs_repo_path_uses_hex_pubkey() {
|
||||
let p = prs_repo_path(Path::new("/data/git"), "deadbeef", "my-repo");
|
||||
assert_eq!(p, PathBuf::from("/data/git/prs/deadbeef/my-repo.git"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prs_repo_path_strips_trailing_git() {
|
||||
let p = prs_repo_path(Path::new("/data/git"), "deadbeef", "my-repo.git");
|
||||
assert_eq!(p, PathBuf::from("/data/git/prs/deadbeef/my-repo.git"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_prs_repo_path_detects_subtree() {
|
||||
let root = Path::new("/data/git");
|
||||
assert!(is_prs_repo_path(
|
||||
&PathBuf::from("/data/git/prs/abc/repo.git"),
|
||||
root
|
||||
));
|
||||
assert!(!is_prs_repo_path(
|
||||
&PathBuf::from("/data/git/npub1xyz/repo.git"),
|
||||
root
|
||||
));
|
||||
}
|
||||
}
|
||||
+151
@@ -164,6 +164,157 @@ impl Service<Request<Incoming>> for HttpService {
|
||||
});
|
||||
}
|
||||
|
||||
// GRASP-06: route /prs/<npub>/<id>.git/* before the standard git URL
|
||||
// parser. When disabled, the path falls through to existing 404
|
||||
// handling (preserving the discovery-gate contract).
|
||||
if self.config.grasp06_enable {
|
||||
if let Some(prs) = crate::grasp06::endpoint::parse_prs_url(&path) {
|
||||
let git_protocol = req
|
||||
.headers()
|
||||
.get("git-protocol")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
let content_encoding = req
|
||||
.headers()
|
||||
.get("content-encoding")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_lowercase());
|
||||
|
||||
tracing::debug!(
|
||||
"/prs/ request: {} {} (submitter={}, id={}, subpath={}, protocol={:?})",
|
||||
method,
|
||||
path,
|
||||
prs.submitter.to_hex(),
|
||||
prs.identifier,
|
||||
prs.subpath,
|
||||
git_protocol
|
||||
);
|
||||
|
||||
let subpath = prs.subpath.clone();
|
||||
let method_clone = method.clone();
|
||||
let metrics_clone = self.metrics.clone();
|
||||
|
||||
return Box::pin(async move {
|
||||
// Collect (and gunzip if needed) the request body just like
|
||||
// the standard git branch does.
|
||||
let raw_body = req
|
||||
.collect()
|
||||
.await
|
||||
.map(|collected| collected.to_bytes())
|
||||
.unwrap_or_else(|_| Bytes::new());
|
||||
let body_bytes = if content_encoding.as_deref() == Some("gzip") {
|
||||
use flate2::read::GzDecoder;
|
||||
use std::io::Read;
|
||||
let mut decoder = GzDecoder::new(&raw_body[..]);
|
||||
let mut decompressed = Vec::new();
|
||||
match decoder.read_to_end(&mut decompressed) {
|
||||
Ok(_) => Bytes::from(decompressed),
|
||||
Err(e) => {
|
||||
tracing::warn!("/prs/ gzip decompress failed: {}", e);
|
||||
raw_body
|
||||
}
|
||||
}
|
||||
} else {
|
||||
raw_body
|
||||
};
|
||||
|
||||
let result: Result<Response<Full<Bytes>>, git::handlers::GitError> =
|
||||
match (method_clone.as_ref(), subpath.as_str()) {
|
||||
// GET /info/refs?service=git-{upload,receive}-pack
|
||||
(m, sp) if m == Method::GET && sp.starts_with("info/refs") => {
|
||||
let service = query
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.strip_prefix("service=")
|
||||
.and_then(git::protocol::GitService::from_query_param);
|
||||
match service {
|
||||
Some(svc) => {
|
||||
let r = crate::grasp06::fetch::handle_prs_info_refs(
|
||||
&prs,
|
||||
&git_data_path,
|
||||
svc,
|
||||
git_protocol.as_deref(),
|
||||
)
|
||||
.await;
|
||||
if let Some(ref m) = metrics_clone {
|
||||
let status =
|
||||
if r.is_ok() { "success" } else { "error" };
|
||||
let op = match svc {
|
||||
git::protocol::GitService::UploadPack => "fetch",
|
||||
git::protocol::GitService::ReceivePack => "push",
|
||||
};
|
||||
m.record_git_operation(op, status);
|
||||
}
|
||||
r
|
||||
}
|
||||
None => Err(git::handlers::GitError::RepositoryNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
// POST /git-upload-pack — clone/fetch.
|
||||
(m, "git-upload-pack") if m == Method::POST => {
|
||||
let r = crate::grasp06::fetch::handle_prs_upload_pack(
|
||||
&prs,
|
||||
&git_data_path,
|
||||
body_bytes,
|
||||
git_protocol.as_deref(),
|
||||
)
|
||||
.await;
|
||||
if let Some(ref m) = metrics_clone {
|
||||
let status = if r.is_ok() { "success" } else { "error" };
|
||||
m.record_git_operation("clone", status);
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
// POST /git-receive-pack — stub for now.
|
||||
// Returns HTTP 200 with an ERR pkt-line; the
|
||||
// real handler is not yet implemented.
|
||||
(m, "git-receive-pack") if m == Method::POST => {
|
||||
if let Some(ref m) = metrics_clone {
|
||||
m.record_git_operation("push", "error");
|
||||
}
|
||||
Ok(crate::grasp06::fetch::handle_prs_receive_pack_stub())
|
||||
}
|
||||
|
||||
_ => Err(git::handlers::GitError::RepositoryNotFound),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
let (parts, body) = response.into_parts();
|
||||
Ok(add_cors_headers(Response::builder().status(parts.status))
|
||||
.header(
|
||||
"content-type",
|
||||
parts
|
||||
.headers
|
||||
.get("content-type")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("application/octet-stream"),
|
||||
)
|
||||
.header(
|
||||
"cache-control",
|
||||
parts
|
||||
.headers
|
||||
.get("cache-control")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("no-cache"),
|
||||
)
|
||||
.body(body)
|
||||
.unwrap())
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Git error: {}", e);
|
||||
Ok(add_cors_headers(Response::builder())
|
||||
.status(e.status_code())
|
||||
.body(Full::new(Bytes::from(error_msg)))
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Git HTTP requests first
|
||||
if let Some((npub, identifier, subpath)) = git::parse_git_url(&path) {
|
||||
// Extract Git-Protocol header for protocol v2 support
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod audit_cleanup;
|
||||
pub mod cleanup_empty_repos;
|
||||
pub mod config;
|
||||
pub mod git;
|
||||
pub mod grasp06;
|
||||
pub mod http;
|
||||
pub mod metrics;
|
||||
pub mod nostr;
|
||||
|
||||
Reference in New Issue
Block a user