diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-21 15:35:19 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-21 15:35:19 +0000 |
| commit | ee7e115b2d0e6a6eee42eb875199c965696017d5 (patch) | |
| tree | 634ab7f960d56dc9073ebc85baf9fa6c193a32c8 /src | |
| parent | 97e21b62eab89bab1456db7df27df8f1c85399f0 (diff) | |
fixed http clone
but do we really nedd to create a blank commit?
I dont think ngit-relay does that.
Do we need to se the default branch or is this automatic?
Diffstat (limited to 'src')
| -rw-r--r-- | src/git/handlers.rs | 47 | ||||
| -rw-r--r-- | src/git/protocol.rs | 8 | ||||
| -rw-r--r-- | src/git/subprocess.rs | 2 | ||||
| -rw-r--r-- | src/http/mod.rs | 30 | ||||
| -rw-r--r-- | src/nostr/builder.rs | 34 |
5 files changed, 96 insertions, 25 deletions
diff --git a/src/git/handlers.rs b/src/git/handlers.rs index 0efc9d0..ac35d14 100644 --- a/src/git/handlers.rs +++ b/src/git/handlers.rs | |||
| @@ -4,6 +4,7 @@ | |||
| 4 | 4 | ||
| 5 | use std::path::PathBuf; | 5 | use std::path::PathBuf; |
| 6 | use hyper::{body::Bytes, Response, StatusCode}; | 6 | use hyper::{body::Bytes, Response, StatusCode}; |
| 7 | use http_body_util::Full; | ||
| 7 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; | 8 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; |
| 8 | use tracing::{debug, error, warn}; | 9 | use tracing::{debug, error, warn}; |
| 9 | 10 | ||
| @@ -16,7 +17,7 @@ use super::subprocess::GitSubprocess; | |||
| 16 | pub async fn handle_info_refs( | 17 | pub async fn handle_info_refs( |
| 17 | repo_path: PathBuf, | 18 | repo_path: PathBuf, |
| 18 | service: GitService, | 19 | service: GitService, |
| 19 | ) -> Result<Response<String>, GitError> { | 20 | ) -> Result<Response<Full<Bytes>>, GitError> { |
| 20 | debug!("Handling info/refs for {:?} with service {:?}", repo_path, service); | 21 | debug!("Handling info/refs for {:?} with service {:?}", repo_path, service); |
| 21 | 22 | ||
| 22 | // Check if repository exists | 23 | // Check if repository exists |
| @@ -34,6 +35,8 @@ pub async fn handle_info_refs( | |||
| 34 | 35 | ||
| 35 | // Read the output from git | 36 | // Read the output from git |
| 36 | let mut output = Vec::new(); | 37 | let mut output = Vec::new(); |
| 38 | let mut stderr_output = Vec::new(); | ||
| 39 | |||
| 37 | if let Some(stdout) = git.take_stdout() { | 40 | if let Some(stdout) = git.take_stdout() { |
| 38 | let mut stdout = stdout; | 41 | let mut stdout = stdout; |
| 39 | stdout.read_to_end(&mut output).await | 42 | stdout.read_to_end(&mut output).await |
| @@ -42,6 +45,15 @@ pub async fn handle_info_refs( | |||
| 42 | GitError::IoError(e) | 45 | GitError::IoError(e) |
| 43 | })?; | 46 | })?; |
| 44 | } | 47 | } |
| 48 | |||
| 49 | if let Some(stderr) = git.take_stderr() { | ||
| 50 | let mut stderr = stderr; | ||
| 51 | stderr.read_to_end(&mut stderr_output).await | ||
| 52 | .map_err(|e| { | ||
| 53 | error!("Failed to read git stderr: {}", e); | ||
| 54 | GitError::IoError(e) | ||
| 55 | })?; | ||
| 56 | } | ||
| 45 | 57 | ||
| 46 | // Wait for process to complete | 58 | // Wait for process to complete |
| 47 | let status = git.wait().await | 59 | let status = git.wait().await |
| @@ -51,7 +63,8 @@ pub async fn handle_info_refs( | |||
| 51 | })?; | 63 | })?; |
| 52 | 64 | ||
| 53 | if !status.success() { | 65 | if !status.success() { |
| 54 | error!("Git process failed with status: {:?}", status); | 66 | let stderr_str = String::from_utf8_lossy(&stderr_output); |
| 67 | error!("Git process failed with status: {:?}, stderr: {}", status, stderr_str); | ||
| 55 | return Err(GitError::GitFailed(status.code())); | 68 | return Err(GitError::GitFailed(status.code())); |
| 56 | } | 69 | } |
| 57 | 70 | ||
| @@ -70,7 +83,7 @@ pub async fn handle_info_refs( | |||
| 70 | .status(StatusCode::OK) | 83 | .status(StatusCode::OK) |
| 71 | .header("content-type", service.advertisement_content_type()) | 84 | .header("content-type", service.advertisement_content_type()) |
| 72 | .header("cache-control", "no-cache") | 85 | .header("cache-control", "no-cache") |
| 73 | .body(String::from_utf8_lossy(&response_body).to_string()) | 86 | .body(Full::new(Bytes::from(response_body))) |
| 74 | .unwrap()) | 87 | .unwrap()) |
| 75 | } | 88 | } |
| 76 | 89 | ||
| @@ -78,7 +91,7 @@ pub async fn handle_info_refs( | |||
| 78 | pub async fn handle_upload_pack( | 91 | pub async fn handle_upload_pack( |
| 79 | repo_path: PathBuf, | 92 | repo_path: PathBuf, |
| 80 | request_body: Bytes, | 93 | request_body: Bytes, |
| 81 | ) -> Result<Response<String>, GitError> { | 94 | ) -> Result<Response<Full<Bytes>>, GitError> { |
| 82 | debug!("Handling upload-pack for {:?}", repo_path); | 95 | debug!("Handling upload-pack for {:?}", repo_path); |
| 83 | 96 | ||
| 84 | if !repo_path.exists() { | 97 | if !repo_path.exists() { |
| @@ -99,17 +112,27 @@ pub async fn handle_upload_pack( | |||
| 99 | 112 | ||
| 100 | // Read response from git's stdout | 113 | // Read response from git's stdout |
| 101 | let mut output = Vec::new(); | 114 | let mut output = Vec::new(); |
| 115 | let mut stderr_output = Vec::new(); | ||
| 116 | |||
| 102 | if let Some(stdout) = git.take_stdout() { | 117 | if let Some(stdout) = git.take_stdout() { |
| 103 | let mut stdout = stdout; | 118 | let mut stdout = stdout; |
| 104 | stdout.read_to_end(&mut output).await | 119 | stdout.read_to_end(&mut output).await |
| 105 | .map_err(GitError::IoError)?; | 120 | .map_err(GitError::IoError)?; |
| 106 | } | 121 | } |
| 122 | |||
| 123 | if let Some(stderr) = git.take_stderr() { | ||
| 124 | let mut stderr = stderr; | ||
| 125 | stderr.read_to_end(&mut stderr_output).await | ||
| 126 | .map_err(GitError::IoError)?; | ||
| 127 | } | ||
| 107 | 128 | ||
| 108 | // Wait for process | 129 | // Wait for process |
| 109 | let status = git.wait().await | 130 | let status = git.wait().await |
| 110 | .map_err(GitError::IoError)?; | 131 | .map_err(GitError::IoError)?; |
| 111 | 132 | ||
| 112 | if !status.success() { | 133 | if !status.success() { |
| 134 | let stderr_str = String::from_utf8_lossy(&stderr_output); | ||
| 135 | error!("Git upload-pack failed: {}", stderr_str); | ||
| 113 | return Err(GitError::GitFailed(status.code())); | 136 | return Err(GitError::GitFailed(status.code())); |
| 114 | } | 137 | } |
| 115 | 138 | ||
| @@ -117,7 +140,7 @@ pub async fn handle_upload_pack( | |||
| 117 | .status(StatusCode::OK) | 140 | .status(StatusCode::OK) |
| 118 | .header("content-type", GitService::UploadPack.result_content_type()) | 141 | .header("content-type", GitService::UploadPack.result_content_type()) |
| 119 | .header("cache-control", "no-cache") | 142 | .header("cache-control", "no-cache") |
| 120 | .body(String::from_utf8_lossy(&output).to_string()) | 143 | .body(Full::new(Bytes::from(output))) |
| 121 | .unwrap()) | 144 | .unwrap()) |
| 122 | } | 145 | } |
| 123 | 146 | ||
| @@ -127,7 +150,7 @@ pub async fn handle_upload_pack( | |||
| 127 | pub async fn handle_receive_pack( | 150 | pub async fn handle_receive_pack( |
| 128 | repo_path: PathBuf, | 151 | repo_path: PathBuf, |
| 129 | request_body: Bytes, | 152 | request_body: Bytes, |
| 130 | ) -> Result<Response<String>, GitError> { | 153 | ) -> Result<Response<Full<Bytes>>, GitError> { |
| 131 | debug!("Handling receive-pack for {:?}", repo_path); | 154 | debug!("Handling receive-pack for {:?}", repo_path); |
| 132 | 155 | ||
| 133 | if !repo_path.exists() { | 156 | if !repo_path.exists() { |
| @@ -151,17 +174,27 @@ pub async fn handle_receive_pack( | |||
| 151 | 174 | ||
| 152 | // Read response from git's stdout | 175 | // Read response from git's stdout |
| 153 | let mut output = Vec::new(); | 176 | let mut output = Vec::new(); |
| 177 | let mut stderr_output = Vec::new(); | ||
| 178 | |||
| 154 | if let Some(stdout) = git.take_stdout() { | 179 | if let Some(stdout) = git.take_stdout() { |
| 155 | let mut stdout = stdout; | 180 | let mut stdout = stdout; |
| 156 | stdout.read_to_end(&mut output).await | 181 | stdout.read_to_end(&mut output).await |
| 157 | .map_err(GitError::IoError)?; | 182 | .map_err(GitError::IoError)?; |
| 158 | } | 183 | } |
| 184 | |||
| 185 | if let Some(stderr) = git.take_stderr() { | ||
| 186 | let mut stderr = stderr; | ||
| 187 | stderr.read_to_end(&mut stderr_output).await | ||
| 188 | .map_err(GitError::IoError)?; | ||
| 189 | } | ||
| 159 | 190 | ||
| 160 | // Wait for process | 191 | // Wait for process |
| 161 | let status = git.wait().await | 192 | let status = git.wait().await |
| 162 | .map_err(GitError::IoError)?; | 193 | .map_err(GitError::IoError)?; |
| 163 | 194 | ||
| 164 | if !status.success() { | 195 | if !status.success() { |
| 196 | let stderr_str = String::from_utf8_lossy(&stderr_output); | ||
| 197 | error!("Git receive-pack failed: {}", stderr_str); | ||
| 165 | return Err(GitError::GitFailed(status.code())); | 198 | return Err(GitError::GitFailed(status.code())); |
| 166 | } | 199 | } |
| 167 | 200 | ||
| @@ -169,7 +202,7 @@ pub async fn handle_receive_pack( | |||
| 169 | .status(StatusCode::OK) | 202 | .status(StatusCode::OK) |
| 170 | .header("content-type", GitService::ReceivePack.result_content_type()) | 203 | .header("content-type", GitService::ReceivePack.result_content_type()) |
| 171 | .header("cache-control", "no-cache") | 204 | .header("cache-control", "no-cache") |
| 172 | .body(String::from_utf8_lossy(&output).to_string()) | 205 | .body(Full::new(Bytes::from(output))) |
| 173 | .unwrap()) | 206 | .unwrap()) |
| 174 | } | 207 | } |
| 175 | 208 | ||
diff --git a/src/git/protocol.rs b/src/git/protocol.rs index 84da131..93177de 100644 --- a/src/git/protocol.rs +++ b/src/git/protocol.rs | |||
| @@ -157,6 +157,14 @@ impl GitService { | |||
| 157 | } | 157 | } |
| 158 | } | 158 | } |
| 159 | 159 | ||
| 160 | /// Get the git command name (without "git-" prefix) for subprocess invocation | ||
| 161 | pub fn command_name(&self) -> &'static str { | ||
| 162 | match self { | ||
| 163 | Self::UploadPack => "upload-pack", | ||
| 164 | Self::ReceivePack => "receive-pack", | ||
| 165 | } | ||
| 166 | } | ||
| 167 | |||
| 160 | /// Get the content type for the service advertisement | 168 | /// Get the content type for the service advertisement |
| 161 | pub fn advertisement_content_type(&self) -> &'static str { | 169 | pub fn advertisement_content_type(&self) -> &'static str { |
| 162 | match self { | 170 | match self { |
diff --git a/src/git/subprocess.rs b/src/git/subprocess.rs index 96eed26..dac3ace 100644 --- a/src/git/subprocess.rs +++ b/src/git/subprocess.rs | |||
| @@ -30,7 +30,7 @@ impl GitSubprocess { | |||
| 30 | let repo_path = repo_path.as_ref(); | 30 | let repo_path = repo_path.as_ref(); |
| 31 | 31 | ||
| 32 | let mut cmd = Command::new("git"); | 32 | let mut cmd = Command::new("git"); |
| 33 | cmd.arg(service.as_str()); | 33 | cmd.arg(service.command_name()); |
| 34 | 34 | ||
| 35 | if advertise { | 35 | if advertise { |
| 36 | cmd.arg("--advertise-refs"); | 36 | cmd.arg("--advertise-refs"); |
diff --git a/src/http/mod.rs b/src/http/mod.rs index 28ccd7b..c676bda 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs | |||
| @@ -14,7 +14,7 @@ use hyper::server::conn::http1; | |||
| 14 | use hyper::service::Service; | 14 | use hyper::service::Service; |
| 15 | use hyper::{Method, Request, Response}; | 15 | use hyper::{Method, Request, Response}; |
| 16 | use hyper_util::rt::TokioIo; | 16 | use hyper_util::rt::TokioIo; |
| 17 | use http_body_util::BodyExt; | 17 | use http_body_util::{BodyExt, Full}; |
| 18 | use nostr_sdk::hashes::sha1::Hash as Sha1Hash; | 18 | use nostr_sdk::hashes::sha1::Hash as Sha1Hash; |
| 19 | use nostr_sdk::hashes::{Hash, HashEngine}; | 19 | use nostr_sdk::hashes::{Hash, HashEngine}; |
| 20 | use nostr_relay_builder::LocalRelay; | 20 | use nostr_relay_builder::LocalRelay; |
| @@ -42,7 +42,7 @@ impl HttpService { | |||
| 42 | } | 42 | } |
| 43 | 43 | ||
| 44 | impl Service<Request<Incoming>> for HttpService { | 44 | impl Service<Request<Incoming>> for HttpService { |
| 45 | type Response = Response<String>; | 45 | type Response = Response<Full<Bytes>>; |
| 46 | type Error = String; | 46 | type Error = String; |
| 47 | type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; | 47 | type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; |
| 48 | 48 | ||
| @@ -65,6 +65,11 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 65 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); | 65 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); |
| 66 | 66 | ||
| 67 | return Box::pin(async move { | 67 | return Box::pin(async move { |
| 68 | // Collect request body once before the match statement | ||
| 69 | let body_bytes = req.collect().await | ||
| 70 | .map(|collected| collected.to_bytes()) | ||
| 71 | .unwrap_or_else(|_| Bytes::new()); | ||
| 72 | |||
| 68 | let result = match (method.as_ref(), subpath.as_str()) { | 73 | let result = match (method.as_ref(), subpath.as_str()) { |
| 69 | // GET /info/refs?service=git-upload-pack or git-receive-pack | 74 | // GET /info/refs?service=git-upload-pack or git-receive-pack |
| 70 | (m, sp) if m == Method::GET && sp.starts_with("info/refs") => { | 75 | (m, sp) if m == Method::GET && sp.starts_with("info/refs") => { |
| @@ -85,22 +90,12 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 85 | 90 | ||
| 86 | // POST /git-upload-pack (clone/fetch) | 91 | // POST /git-upload-pack (clone/fetch) |
| 87 | (m, "git-upload-pack") if m == Method::POST => { | 92 | (m, "git-upload-pack") if m == Method::POST => { |
| 88 | // Read request body | ||
| 89 | let body_bytes = req.collect().await | ||
| 90 | .map(|collected| collected.to_bytes()) | ||
| 91 | .unwrap_or_else(|_| Bytes::new()); | ||
| 92 | |||
| 93 | git::handlers::handle_upload_pack(repo_path, body_bytes).await | 93 | git::handlers::handle_upload_pack(repo_path, body_bytes).await |
| 94 | } | 94 | } |
| 95 | 95 | ||
| 96 | // POST /git-receive-pack (push) | 96 | // POST /git-receive-pack (push) |
| 97 | (m, "git-receive-pack") if m == Method::POST => { | 97 | (m, "git-receive-pack") if m == Method::POST => { |
| 98 | // Read request body | 98 | git::handlers::handle_receive_pack(repo_path, body_bytes.clone()).await |
| 99 | let body_bytes = req.collect().await | ||
| 100 | .map(|collected| collected.to_bytes()) | ||
| 101 | .unwrap_or_else(|_| Bytes::new()); | ||
| 102 | |||
| 103 | git::handlers::handle_receive_pack(repo_path, body_bytes).await | ||
| 104 | } | 99 | } |
| 105 | 100 | ||
| 106 | _ => { | 101 | _ => { |
| @@ -112,9 +107,10 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 112 | Ok(response) => Ok(response), | 107 | Ok(response) => Ok(response), |
| 113 | Err(e) => { | 108 | Err(e) => { |
| 114 | tracing::error!("Git handler error: {}", e); | 109 | tracing::error!("Git handler error: {}", e); |
| 110 | let error_msg = format!("Git error: {}", e); | ||
| 115 | Ok(Response::builder() | 111 | Ok(Response::builder() |
| 116 | .status(e.status_code()) | 112 | .status(e.status_code()) |
| 117 | .body(format!("Git error: {}", e)) | 113 | .body(Full::new(Bytes::from(error_msg))) |
| 118 | .unwrap()) | 114 | .unwrap()) |
| 119 | } | 115 | } |
| 120 | } | 116 | } |
| @@ -141,7 +137,7 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 141 | .status(200) | 137 | .status(200) |
| 142 | .header("content-type", "application/nostr+json") | 138 | .header("content-type", "application/nostr+json") |
| 143 | .header("access-control-allow-origin", "*") | 139 | .header("access-control-allow-origin", "*") |
| 144 | .body(json) | 140 | .body(Full::new(Bytes::from(json))) |
| 145 | .unwrap()) | 141 | .unwrap()) |
| 146 | }); | 142 | }); |
| 147 | } | 143 | } |
| @@ -185,7 +181,7 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 185 | .header(CONNECTION, "upgrade") | 181 | .header(CONNECTION, "upgrade") |
| 186 | .header(UPGRADE, "websocket") | 182 | .header(UPGRADE, "websocket") |
| 187 | .header(SEC_WEBSOCKET_ACCEPT, derived.unwrap()) | 183 | .header(SEC_WEBSOCKET_ACCEPT, derived.unwrap()) |
| 188 | .body("".to_string()) | 184 | .body(Full::new(Bytes::new())) |
| 189 | .unwrap()) | 185 | .unwrap()) |
| 190 | }); | 186 | }); |
| 191 | } | 187 | } |
| @@ -197,7 +193,7 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 197 | Ok(base | 193 | Ok(base |
| 198 | .status(200) | 194 | .status(200) |
| 199 | .header("content-type", "text/html; charset=utf-8") | 195 | .header("content-type", "text/html; charset=utf-8") |
| 200 | .body(html) | 196 | .body(Full::new(Bytes::from(html))) |
| 201 | .unwrap()) | 197 | .unwrap()) |
| 202 | }) | 198 | }) |
| 203 | } | 199 | } |
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index 259c380..a0b82f4 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs | |||
| @@ -73,6 +73,40 @@ impl Nip34WritePolicy { | |||
| 73 | return Err(format!("git init failed: {}", stderr)); | 73 | return Err(format!("git init failed: {}", stderr)); |
| 74 | } | 74 | } |
| 75 | 75 | ||
| 76 | // Create an initial empty commit so the repository can be cloned | ||
| 77 | // This is required because git clone fails on completely empty repositories | ||
| 78 | let output = std::process::Command::new("git") | ||
| 79 | .args(&[ | ||
| 80 | "--git-dir", repo_path.to_str().unwrap(), | ||
| 81 | "commit-tree", "-m", "Initial empty commit", | ||
| 82 | "4b825dc642cb6eb9a060e54bf8d69288fbee4904" // Empty tree hash | ||
| 83 | ]) | ||
| 84 | .output() | ||
| 85 | .map_err(|e| format!("Failed to create initial commit: {}", e))?; | ||
| 86 | |||
| 87 | if !output.status.success() { | ||
| 88 | let stderr = String::from_utf8_lossy(&output.stderr); | ||
| 89 | tracing::warn!("Failed to create initial commit (repository may not be cloneable): {}", stderr); | ||
| 90 | // Don't fail here - the repository was created successfully | ||
| 91 | } else { | ||
| 92 | // Extract commit hash from stdout | ||
| 93 | let commit_hash = String::from_utf8_lossy(&output.stdout).trim().to_string(); | ||
| 94 | |||
| 95 | // Create master branch pointing to this commit | ||
| 96 | let output = std::process::Command::new("git") | ||
| 97 | .args(&[ | ||
| 98 | "--git-dir", repo_path.to_str().unwrap(), | ||
| 99 | "update-ref", "refs/heads/master", &commit_hash | ||
| 100 | ]) | ||
| 101 | .output() | ||
| 102 | .map_err(|e| format!("Failed to create master branch: {}", e))?; | ||
| 103 | |||
| 104 | if !output.status.success() { | ||
| 105 | let stderr = String::from_utf8_lossy(&output.stderr); | ||
| 106 | tracing::warn!("Failed to create master branch: {}", stderr); | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 76 | tracing::info!("Created bare repository at {}", repo_path.display()); | 110 | tracing::info!("Created bare repository at {}", repo_path.display()); |
| 77 | Ok(()) | 111 | Ok(()) |
| 78 | } | 112 | } |