1use anyhow::{bail, Context, Result};
39use nostr_sdk::ToBech32;
40use std::io::{BufRead, Write};
41use tracing::{debug, info, warn};
42
43mod git;
44mod helper;
45mod nostr_client;
46
47use hashtree_config::Config;
48use helper::RemoteHelper;
49use nostr_client::resolve_identity;
50
51pub fn main_entry() {
54 let _ = rustls::crypto::ring::default_provider().install_default();
56
57 if let Err(e) = run() {
58 eprintln!("Error: {:#}", e);
59 std::process::exit(1);
60 }
61}
62
63fn run() -> Result<()> {
64 #[cfg(unix)]
66 {
67 unsafe {
68 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
69 }
70 }
71
72 tracing_subscriber::fmt()
75 .with_env_filter(
76 tracing_subscriber::EnvFilter::from_default_env()
77 .add_directive("git_remote_htree=error".parse().unwrap())
78 .add_directive("nostr_relay_pool=off".parse().unwrap()),
79 )
80 .with_writer(std::io::stderr)
81 .init();
82
83 let args: Vec<String> = std::env::args().collect();
84 debug!("git-remote-htree called with args: {:?}", args);
85
86 if args.len() < 3 {
88 bail!("Usage: git-remote-htree <remote-name> <url>");
89 }
90
91 let remote_name = &args[1];
92 let url = &args[2];
93
94 info!("Remote: {}, URL: {}", remote_name, url);
95
96 let parsed = parse_htree_url(url)?;
98 let identifier = parsed.identifier;
99 let repo_name = parsed.repo_name;
100 let is_private = parsed.is_private; let url_secret = if let Some(key) = parsed.secret_key {
104 Some(key)
106 } else if parsed.auto_generate_secret {
107 let key = generate_secret_key();
109 let secret_hex = hex::encode(key);
110
111 let npub = match resolve_identity(&identifier) {
113 Ok((pubkey, _)) => {
114 hex::decode(&pubkey)
115 .ok()
116 .filter(|b| b.len() == 32)
117 .and_then(|pk_bytes| nostr_sdk::PublicKey::from_slice(&pk_bytes).ok())
118 .and_then(|pk| pk.to_bech32().ok())
119 .unwrap_or(pubkey)
120 }
121 Err(_) => identifier.clone(),
122 };
123
124 let local_url = format!("htree://{}/{}#k={}", identifier, repo_name, secret_hex);
125 let share_url = format!("htree://{}/{}#k={}", npub, repo_name, secret_hex);
126
127 eprintln!();
128 eprintln!("=== Link-Visible Repository Setup ===");
129 eprintln!();
130 eprintln!("A secret key has been generated for this link-visible repository.");
131 eprintln!();
132 eprintln!("Step 1: Update your remote URL with the generated key:");
133 eprintln!(" git remote set-url {} {}", remote_name, local_url);
134 eprintln!();
135 eprintln!("Step 2: Push again (same command you just ran)");
136 eprintln!();
137 eprintln!("Shareable URL (for others to clone):");
138 eprintln!(" {}", share_url);
139 eprintln!();
140
141 std::process::exit(0);
143 } else {
144 None
145 };
146
147 if is_private {
148 info!("Private repo mode: only author can decrypt");
149 } else if url_secret.is_some() {
150 info!("Link-visible repo mode: using secret key from URL");
151 }
152
153 let (pubkey, signing_key) = match resolve_identity(&identifier) {
156 Ok(result) => result,
157 Err(e) => {
158 warn!("Failed to resolve identity '{}': {}", identifier, e);
160 info!("Tip: Use htree://self/<repo> to auto-generate identity on first use");
161 return Err(e);
162 }
163 };
164
165 if signing_key.is_some() {
166 debug!("Found signing key for {}", identifier);
167 } else {
168 debug!("No signing key for {} (read-only)", identifier);
169 }
170
171 let npub = hex::decode(&pubkey)
173 .ok()
174 .filter(|b| b.len() == 32)
175 .and_then(|pk_bytes| nostr_sdk::PublicKey::from_slice(&pk_bytes).ok())
176 .and_then(|pk| pk.to_bech32().ok())
177 .unwrap_or_else(|| pubkey.clone());
178
179 info!("Using identity: {}", npub);
180
181 let config = Config::load_or_default();
183 debug!("Loaded config with {} read servers, {} write servers",
184 config.blossom.read_servers.len(),
185 config.blossom.write_servers.len());
186
187 let mut helper = RemoteHelper::new(&pubkey, &repo_name, signing_key, url_secret, is_private, config)?;
189
190 let stdin = std::io::stdin();
192 let stdout = std::io::stdout();
193 let mut stdout = stdout.lock();
194
195 for line in stdin.lock().lines() {
196 let line = match line {
197 Ok(l) => l,
198 Err(e) => {
199 if e.kind() == std::io::ErrorKind::BrokenPipe {
200 break;
201 }
202 return Err(e.into());
203 }
204 };
205
206 debug!("< {}", line);
207
208 match helper.handle_command(&line) {
209 Ok(Some(responses)) => {
210 for response in responses {
211 debug!("> {}", response);
212 if let Err(e) = writeln!(stdout, "{}", response) {
213 if e.kind() == std::io::ErrorKind::BrokenPipe {
214 break;
215 }
216 return Err(e.into());
217 }
218 }
219 if let Err(e) = stdout.flush() {
220 if e.kind() == std::io::ErrorKind::BrokenPipe {
221 break;
222 }
223 return Err(e.into());
224 }
225 }
226 Ok(None) => {}
227 Err(e) => {
228 warn!("Command error: {}", e);
229 return Err(e);
231 }
232 }
233
234 if helper.should_exit() {
235 break;
236 }
237 }
238
239 Ok(())
240}
241
242pub struct ParsedUrl {
244 pub identifier: String,
245 pub repo_name: String,
246 pub secret_key: Option<[u8; 32]>,
248 pub is_private: bool,
250 pub auto_generate_secret: bool,
252}
253
254fn parse_htree_url(url: &str) -> Result<ParsedUrl> {
261 let url = url
262 .strip_prefix("htree://")
263 .context("URL must start with htree://")?;
264
265 let (url_path, secret_key, is_private, auto_generate_secret) = if let Some((path, fragment)) = url.split_once('#') {
267 if fragment == "private" {
268 (path, None, true, false)
270 } else if fragment == "link-visible" {
271 (path, None, false, true)
273 } else if let Some(key_hex) = fragment.strip_prefix("k=") {
274 let bytes = hex::decode(key_hex)
276 .context("Invalid secret key hex in URL fragment")?;
277 if bytes.len() != 32 {
278 bail!("Secret key must be 32 bytes (64 hex chars)");
279 }
280 let mut key = [0u8; 32];
281 key.copy_from_slice(&bytes);
282 (path, Some(key), false, false)
283 } else {
284 bail!(
286 "Unknown URL fragment '#{}'. Valid options:\n\
287 - #k=<64-hex-chars> Link-visible with explicit key\n\
288 - #link-visible Link-visible with auto-generated key\n\
289 - #private Author-only (NIP-44 encrypted)\n\
290 - (no fragment) Public",
291 fragment
292 );
293 }
294 } else {
295 (url, None, false, false)
296 };
297
298 let (identifier, repo) = url_path
300 .split_once('/')
301 .context("URL must be htree://<identifier>/<repo>")?;
302
303 let repo_name = repo.to_string();
305
306 if identifier.is_empty() {
307 bail!("Identifier cannot be empty");
308 }
309 if repo_name.is_empty() {
310 bail!("Repository name cannot be empty");
311 }
312
313 Ok(ParsedUrl {
314 identifier: identifier.to_string(),
315 repo_name,
316 secret_key,
317 is_private,
318 auto_generate_secret,
319 })
320}
321
322pub fn generate_secret_key() -> [u8; 32] {
324 let mut key = [0u8; 32];
325 getrandom::fill(&mut key).expect("Failed to generate random bytes");
326 key
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_parse_htree_url_pubkey() {
335 let parsed = parse_htree_url(
336 "htree://a9a91ed5f1c405618f63fdd393f9055ab8bac281102cff6b1ac3c74094562dd8/myrepo",
337 )
338 .unwrap();
339 assert_eq!(
340 parsed.identifier,
341 "a9a91ed5f1c405618f63fdd393f9055ab8bac281102cff6b1ac3c74094562dd8"
342 );
343 assert_eq!(parsed.repo_name, "myrepo");
344 assert!(parsed.secret_key.is_none());
345 }
346
347 #[test]
348 fn test_parse_htree_url_npub() {
349 let parsed =
350 parse_htree_url("htree://npub1qvmu0aru530g6yu3kmlhw33fh68r75wf3wuml3vk4ekg0p4m4t6s7fuhxx/test")
351 .unwrap();
352 assert!(parsed.identifier.starts_with("npub1"));
353 assert_eq!(parsed.repo_name, "test");
354 assert!(parsed.secret_key.is_none());
355 }
356
357 #[test]
358 fn test_parse_htree_url_petname() {
359 let parsed = parse_htree_url("htree://alice/project").unwrap();
360 assert_eq!(parsed.identifier, "alice");
361 assert_eq!(parsed.repo_name, "project");
362 assert!(parsed.secret_key.is_none());
363 }
364
365 #[test]
366 fn test_parse_htree_url_self() {
367 let parsed = parse_htree_url("htree://self/myrepo").unwrap();
368 assert_eq!(parsed.identifier, "self");
369 assert_eq!(parsed.repo_name, "myrepo");
370 assert!(parsed.secret_key.is_none());
371 }
372
373 #[test]
374 fn test_parse_htree_url_with_subpath() {
375 let parsed = parse_htree_url("htree://test/repo/some/path").unwrap();
376 assert_eq!(parsed.identifier, "test");
377 assert_eq!(parsed.repo_name, "repo/some/path");
378 assert!(parsed.secret_key.is_none());
379 }
380
381 #[test]
382 fn test_parse_htree_url_with_secret() {
383 let secret_hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
384 let url = format!("htree://test/repo#k={}", secret_hex);
385 let parsed = parse_htree_url(&url).unwrap();
386 assert_eq!(parsed.identifier, "test");
387 assert_eq!(parsed.repo_name, "repo");
388 assert!(parsed.secret_key.is_some());
389 let key = parsed.secret_key.unwrap();
390 assert_eq!(hex::encode(key), secret_hex);
391 }
392
393 #[test]
394 fn test_parse_htree_url_invalid_secret_length() {
395 let url = "htree://test/repo#k=0123456789abcdef";
397 assert!(parse_htree_url(url).is_err());
398 }
399
400 #[test]
401 fn test_parse_htree_url_invalid_secret_hex() {
402 let url = "htree://test/repo#k=ghij456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
404 assert!(parse_htree_url(url).is_err());
405 }
406
407 #[test]
408 fn test_parse_htree_url_invalid_scheme() {
409 assert!(parse_htree_url("https://example.com/repo").is_err());
410 }
411
412 #[test]
413 fn test_parse_htree_url_no_repo() {
414 assert!(parse_htree_url("htree://pubkey").is_err());
415 }
416
417 #[test]
418 fn test_parse_htree_url_empty_identifier() {
419 assert!(parse_htree_url("htree:///repo").is_err());
420 }
421
422 #[test]
423 fn test_parse_htree_url_colon() {
424 let result = parse_htree_url("htree://test:repo");
426 assert!(result.is_err()); }
428
429 #[test]
430 fn test_parse_htree_url_private() {
431 let parsed = parse_htree_url("htree://self/myrepo#private").unwrap();
432 assert_eq!(parsed.identifier, "self");
433 assert_eq!(parsed.repo_name, "myrepo");
434 assert!(parsed.is_private);
435 assert!(parsed.secret_key.is_none());
436 }
437
438 #[test]
439 fn test_parse_htree_url_secret_not_private() {
440 let secret_hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
442 let url = format!("htree://test/repo#k={}", secret_hex);
443 let parsed = parse_htree_url(&url).unwrap();
444 assert!(!parsed.is_private);
445 assert!(parsed.secret_key.is_some());
446 }
447
448 #[test]
449 fn test_parse_htree_url_public() {
450 let parsed = parse_htree_url("htree://test/repo").unwrap();
452 assert!(!parsed.is_private);
453 assert!(parsed.secret_key.is_none());
454 assert!(!parsed.auto_generate_secret);
455 }
456
457 #[test]
458 fn test_parse_htree_url_link_visible_auto() {
459 let parsed = parse_htree_url("htree://self/myrepo#link-visible").unwrap();
461 assert_eq!(parsed.identifier, "self");
462 assert_eq!(parsed.repo_name, "myrepo");
463 assert!(!parsed.is_private);
464 assert!(parsed.secret_key.is_none()); assert!(parsed.auto_generate_secret);
466 }
467
468 #[test]
469 fn test_parse_htree_url_link_visible_explicit_key() {
470 let secret_hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
472 let url = format!("htree://test/repo#k={}", secret_hex);
473 let parsed = parse_htree_url(&url).unwrap();
474 assert!(parsed.secret_key.is_some());
475 assert!(!parsed.auto_generate_secret); }
477
478 #[test]
479 fn test_parse_htree_url_private_not_auto_generate() {
480 let parsed = parse_htree_url("htree://self/myrepo#private").unwrap();
482 assert!(parsed.is_private);
483 assert!(!parsed.auto_generate_secret);
484 }
485}