1use anyhow::{bail, Context, Result};
13use nostr_sdk::ToBech32;
14use std::io::{BufRead, Write};
15use tracing::{debug, info, warn};
16
17mod git;
18mod helper;
19mod nostr_client;
20
21use hashtree_config::Config;
22use helper::RemoteHelper;
23use nostr_client::resolve_identity;
24
25pub fn main_entry() {
28 let _ = rustls::crypto::ring::default_provider().install_default();
30
31 if let Err(e) = run() {
32 eprintln!("Error: {:#}", e);
33 std::process::exit(1);
34 }
35}
36
37fn run() -> Result<()> {
38 #[cfg(unix)]
40 {
41 unsafe {
42 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
43 }
44 }
45
46 tracing_subscriber::fmt()
48 .with_env_filter(
49 tracing_subscriber::EnvFilter::from_default_env()
50 .add_directive("git_remote_htree=warn".parse().unwrap())
51 .add_directive("nostr_relay_pool=off".parse().unwrap()),
52 )
53 .with_writer(std::io::stderr)
54 .init();
55
56 let args: Vec<String> = std::env::args().collect();
57 debug!("git-remote-htree called with args: {:?}", args);
58
59 if args.len() < 3 {
61 bail!("Usage: git-remote-htree <remote-name> <url>");
62 }
63
64 let remote_name = &args[1];
65 let url = &args[2];
66
67 info!("Remote: {}, URL: {}", remote_name, url);
68
69 let (identifier, repo_name) = parse_htree_url(url)?;
71
72 let (pubkey, secret_key) = match resolve_identity(&identifier) {
75 Ok(result) => result,
76 Err(e) => {
77 warn!("Failed to resolve identity '{}': {}", identifier, e);
79 info!("Tip: Use htree://self/<repo> to auto-generate identity on first use");
80 return Err(e);
81 }
82 };
83
84 if secret_key.is_some() {
85 debug!("Found signing key for {}", identifier);
86 } else {
87 debug!("No signing key for {} (read-only)", identifier);
88 }
89
90 if let Ok(pk_bytes) = hex::decode(&pubkey) {
92 if pk_bytes.len() == 32 {
93 if let Ok(pk) = nostr_sdk::PublicKey::from_slice(&pk_bytes) {
94 if let Ok(npub) = pk.to_bech32() {
95 info!("Using identity: {}", npub);
96 }
97 }
98 }
99 }
100
101 let config = Config::load_or_default();
103 debug!("Loaded config with {} read servers, {} write servers",
104 config.blossom.read_servers.len(),
105 config.blossom.write_servers.len());
106
107 let mut helper = RemoteHelper::new(&pubkey, &repo_name, secret_key, config)?;
109
110 let stdin = std::io::stdin();
112 let stdout = std::io::stdout();
113 let mut stdout = stdout.lock();
114
115 for line in stdin.lock().lines() {
116 let line = match line {
117 Ok(l) => l,
118 Err(e) => {
119 if e.kind() == std::io::ErrorKind::BrokenPipe {
120 break;
121 }
122 return Err(e.into());
123 }
124 };
125
126 debug!("< {}", line);
127
128 match helper.handle_command(&line) {
129 Ok(Some(responses)) => {
130 for response in responses {
131 debug!("> {}", response);
132 if let Err(e) = writeln!(stdout, "{}", response) {
133 if e.kind() == std::io::ErrorKind::BrokenPipe {
134 break;
135 }
136 return Err(e.into());
137 }
138 }
139 if let Err(e) = stdout.flush() {
140 if e.kind() == std::io::ErrorKind::BrokenPipe {
141 break;
142 }
143 return Err(e.into());
144 }
145 }
146 Ok(None) => {}
147 Err(e) => {
148 warn!("Command error: {}", e);
149 return Err(e);
151 }
152 }
153
154 if helper.should_exit() {
155 break;
156 }
157 }
158
159 Ok(())
160}
161
162fn parse_htree_url(url: &str) -> Result<(String, String)> {
164 let url = url
165 .strip_prefix("htree://")
166 .context("URL must start with htree://")?;
167
168 let (identifier, repo) = url
170 .split_once('/')
171 .context("URL must be htree://<identifier>/<repo>")?;
172
173 let repo_name = repo.split('/').next().unwrap_or(repo);
175
176 if identifier.is_empty() {
177 bail!("Identifier cannot be empty");
178 }
179 if repo_name.is_empty() {
180 bail!("Repository name cannot be empty");
181 }
182
183 Ok((identifier.to_string(), repo_name.to_string()))
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_parse_htree_url_pubkey() {
192 let (id, repo) = parse_htree_url(
193 "htree://a9a91ed5f1c405618f63fdd393f9055ab8bac281102cff6b1ac3c74094562dd8/myrepo",
194 )
195 .unwrap();
196 assert_eq!(
197 id,
198 "a9a91ed5f1c405618f63fdd393f9055ab8bac281102cff6b1ac3c74094562dd8"
199 );
200 assert_eq!(repo, "myrepo");
201 }
202
203 #[test]
204 fn test_parse_htree_url_npub() {
205 let (id, repo) =
206 parse_htree_url("htree://npub1qvmu0aru530g6yu3kmlhw33fh68r75wf3wuml3vk4ekg0p4m4t6s7fuhxx/test")
207 .unwrap();
208 assert!(id.starts_with("npub1"));
209 assert_eq!(repo, "test");
210 }
211
212 #[test]
213 fn test_parse_htree_url_petname() {
214 let (id, repo) = parse_htree_url("htree://alice/project").unwrap();
215 assert_eq!(id, "alice");
216 assert_eq!(repo, "project");
217 }
218
219 #[test]
220 fn test_parse_htree_url_self() {
221 let (id, repo) = parse_htree_url("htree://self/myrepo").unwrap();
222 assert_eq!(id, "self");
223 assert_eq!(repo, "myrepo");
224 }
225
226 #[test]
227 fn test_parse_htree_url_with_subpath() {
228 let (id, repo) = parse_htree_url("htree://test/repo/some/path").unwrap();
229 assert_eq!(id, "test");
230 assert_eq!(repo, "repo");
231 }
232
233 #[test]
234 fn test_parse_htree_url_invalid_scheme() {
235 assert!(parse_htree_url("https://example.com/repo").is_err());
236 }
237
238 #[test]
239 fn test_parse_htree_url_no_repo() {
240 assert!(parse_htree_url("htree://pubkey").is_err());
241 }
242
243 #[test]
244 fn test_parse_htree_url_empty_identifier() {
245 assert!(parse_htree_url("htree:///repo").is_err());
246 }
247
248 #[test]
249 fn test_parse_htree_url_colon() {
250 let result = parse_htree_url("htree://test:repo");
252 assert!(result.is_err()); }
254}