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