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 }
150 }
151
152 if helper.should_exit() {
153 break;
154 }
155 }
156
157 Ok(())
158}
159
160fn parse_htree_url(url: &str) -> Result<(String, String)> {
162 let url = url
163 .strip_prefix("htree://")
164 .context("URL must start with htree://")?;
165
166 let (identifier, repo) = url
168 .split_once('/')
169 .context("URL must be htree://<identifier>/<repo>")?;
170
171 let repo_name = repo.split('/').next().unwrap_or(repo);
173
174 if identifier.is_empty() {
175 bail!("Identifier cannot be empty");
176 }
177 if repo_name.is_empty() {
178 bail!("Repository name cannot be empty");
179 }
180
181 Ok((identifier.to_string(), repo_name.to_string()))
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_parse_htree_url_pubkey() {
190 let (id, repo) = parse_htree_url(
191 "htree://a9a91ed5f1c405618f63fdd393f9055ab8bac281102cff6b1ac3c74094562dd8/myrepo",
192 )
193 .unwrap();
194 assert_eq!(
195 id,
196 "a9a91ed5f1c405618f63fdd393f9055ab8bac281102cff6b1ac3c74094562dd8"
197 );
198 assert_eq!(repo, "myrepo");
199 }
200
201 #[test]
202 fn test_parse_htree_url_npub() {
203 let (id, repo) =
204 parse_htree_url("htree://npub1qvmu0aru530g6yu3kmlhw33fh68r75wf3wuml3vk4ekg0p4m4t6s7fuhxx/test")
205 .unwrap();
206 assert!(id.starts_with("npub1"));
207 assert_eq!(repo, "test");
208 }
209
210 #[test]
211 fn test_parse_htree_url_petname() {
212 let (id, repo) = parse_htree_url("htree://alice/project").unwrap();
213 assert_eq!(id, "alice");
214 assert_eq!(repo, "project");
215 }
216
217 #[test]
218 fn test_parse_htree_url_self() {
219 let (id, repo) = parse_htree_url("htree://self/myrepo").unwrap();
220 assert_eq!(id, "self");
221 assert_eq!(repo, "myrepo");
222 }
223
224 #[test]
225 fn test_parse_htree_url_with_subpath() {
226 let (id, repo) = parse_htree_url("htree://test/repo/some/path").unwrap();
227 assert_eq!(id, "test");
228 assert_eq!(repo, "repo");
229 }
230
231 #[test]
232 fn test_parse_htree_url_invalid_scheme() {
233 assert!(parse_htree_url("https://example.com/repo").is_err());
234 }
235
236 #[test]
237 fn test_parse_htree_url_no_repo() {
238 assert!(parse_htree_url("htree://pubkey").is_err());
239 }
240
241 #[test]
242 fn test_parse_htree_url_empty_identifier() {
243 assert!(parse_htree_url("htree:///repo").is_err());
244 }
245
246 #[test]
247 fn test_parse_htree_url_colon() {
248 let result = parse_htree_url("htree://test:repo");
250 assert!(result.is_err()); }
252}