upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/repo/mod.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-03-04 16:59:45 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-03-04 16:59:45 +0000
commitecae4bd13c1e28d7efd9ee9bb106ff27fa0451f6 (patch)
tree96d8208535e8ee672d383dac70446c3cd4865e00 /src/bin/ngit/sub_commands/repo/mod.rs
parentaf016dd23101537ccc8ecd5a992bf3b7c6d3abe9 (diff)
add `ngit repo --json` for machine-readable repo detection
Outputs {"is_nostr_repo": false} when not in a nostr repository, or full structured JSON (name, identifier, description, nostr_url, coordinate, maintainers, grasp_servers, git_servers, relays, hashtags) when it is. Always exits 0. Also adds --title as an alias for --name on `ngit init`.
Diffstat (limited to 'src/bin/ngit/sub_commands/repo/mod.rs')
-rw-r--r--src/bin/ngit/sub_commands/repo/mod.rs219
1 files changed, 197 insertions, 22 deletions
diff --git a/src/bin/ngit/sub_commands/repo/mod.rs b/src/bin/ngit/sub_commands/repo/mod.rs
index 97e2c2b..63d96bd 100644
--- a/src/bin/ngit/sub_commands/repo/mod.rs
+++ b/src/bin/ngit/sub_commands/repo/mod.rs
@@ -14,6 +14,7 @@ use ngit::{
14 utils::get_short_git_server_name, 14 utils::get_short_git_server_name,
15}; 15};
16use nostr::{FromBech32, PublicKey, TagStandard, ToBech32, nips::nip19::Nip19Coordinate}; 16use nostr::{FromBech32, PublicKey, TagStandard, ToBech32, nips::nip19::Nip19Coordinate};
17use serde::Serialize;
17 18
18use crate::{ 19use crate::{
19 cli::{Cli, RepoCommands, extract_signer_cli_arguments}, 20 cli::{Cli, RepoCommands, extract_signer_cli_arguments},
@@ -27,21 +28,53 @@ pub async fn launch(
27 cli_args: &Cli, 28 cli_args: &Cli,
28 repo_command: Option<&RepoCommands>, 29 repo_command: Option<&RepoCommands>,
29 offline: bool, 30 offline: bool,
31 json: bool,
30) -> Result<()> { 32) -> Result<()> {
31 match repo_command { 33 match repo_command {
32 Some(RepoCommands::Init(args) | RepoCommands::Edit(args)) => { 34 Some(RepoCommands::Init(args) | RepoCommands::Edit(args)) => {
33 init::launch(cli_args, args).await 35 init::launch(cli_args, args).await
34 } 36 }
35 Some(RepoCommands::Accept(args)) => accept::launch(cli_args, args).await, 37 Some(RepoCommands::Accept(args)) => accept::launch(cli_args, args).await,
36 None => show_info(cli_args, offline).await, 38 None => show_info(cli_args, offline, json).await,
37 } 39 }
38} 40}
39 41
40// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
43// JSON output types
44// ---------------------------------------------------------------------------
45
46#[derive(Serialize)]
47struct RepoInfoJson {
48 is_nostr_repo: bool,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 name: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 identifier: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 description: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 nostr_url: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 coordinate: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 web: Option<Vec<String>>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 maintainers: Option<Vec<String>>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 grasp_servers: Option<Vec<String>>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 git_servers: Option<Vec<String>>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 relays: Option<Vec<String>>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 hashtags: Option<Vec<String>>,
71}
72
73// ---------------------------------------------------------------------------
41// `ngit repo` (no subcommand) — show repository info 74// `ngit repo` (no subcommand) — show repository info
42// --------------------------------------------------------------------------- 75// ---------------------------------------------------------------------------
43 76
44async fn show_info(cli_args: &Cli, offline: bool) -> Result<()> { 77async fn show_info(cli_args: &Cli, offline: bool, json: bool) -> Result<()> {
45 let git_repo = Repo::discover().context("failed to find a git repository")?; 78 let git_repo = Repo::discover().context("failed to find a git repository")?;
46 let git_repo_path = git_repo.get_path()?; 79 let git_repo_path = git_repo.get_path()?;
47 let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo))); 80 let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));
@@ -61,15 +94,31 @@ async fn show_info(cli_args: &Cli, offline: bool) -> Result<()> {
61 .ok() 94 .ok()
62 .map(|(_, user_ref, _)| user_ref.public_key); 95 .map(|(_, user_ref, _)| user_ref.public_key);
63 96
64 println!("subcommands: init, edit, accept (run `ngit repo --help` for details)");
65 println!();
66
67 let repo_coordinate = (try_and_get_repo_coordinates_when_remote_unknown(&git_repo).await).ok(); 97 let repo_coordinate = (try_and_get_repo_coordinates_when_remote_unknown(&git_repo).await).ok();
68 98
69 let Some(repo_coordinate) = repo_coordinate else { 99 let Some(repo_coordinate) = repo_coordinate else {
70 println!("no nostr repository found"); 100 if json {
71 println!(); 101 println!("{}", serde_json::to_string_pretty(&RepoInfoJson {
72 println!("use `ngit repo init` to publish this repository to nostr"); 102 is_nostr_repo: false,
103 name: None,
104 identifier: None,
105 description: None,
106 nostr_url: None,
107 coordinate: None,
108 web: None,
109 maintainers: None,
110 grasp_servers: None,
111 git_servers: None,
112 relays: None,
113 hashtags: None,
114 })?);
115 } else {
116 println!("subcommands: init, edit, accept (run `ngit repo --help` for details)");
117 println!();
118 println!("no nostr repository found");
119 println!();
120 println!("use `ngit repo init` to publish this repository to nostr");
121 }
73 return Ok(()); 122 return Ok(());
74 }; 123 };
75 124
@@ -83,23 +132,149 @@ async fn show_info(cli_args: &Cli, offline: bool) -> Result<()> {
83 let Some(repo_ref) = 132 let Some(repo_ref) =
84 (get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinate).await).ok() 133 (get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinate).await).ok()
85 else { 134 else {
86 println!( 135 if json {
87 "coordinate found ({}) but no announcement on relays", 136 // Coordinate found but no announcement yet — still a nostr repo
88 repo_coordinate.identifier 137 let nostr_url = git_repo
89 ); 138 .git_repo
90 println!(); 139 .find_remote("origin")
91 println!("if you created this repository, run `ngit repo init` to publish an announcement"); 140 .ok()
92 println!("if you are a co-maintainer, run `ngit repo accept` to publish your announcement"); 141 .and_then(|r| r.url().map(std::string::ToString::to_string))
142 .filter(|u| u.starts_with("nostr://"));
143 println!("{}", serde_json::to_string_pretty(&RepoInfoJson {
144 is_nostr_repo: true,
145 name: None,
146 identifier: Some(repo_coordinate.identifier.clone()),
147 description: None,
148 nostr_url,
149 coordinate: repo_coordinate.to_bech32().ok(),
150 web: None,
151 maintainers: None,
152 grasp_servers: None,
153 git_servers: None,
154 relays: None,
155 hashtags: None,
156 })?);
157 } else {
158 println!("subcommands: init, edit, accept (run `ngit repo --help` for details)");
159 println!();
160 println!(
161 "coordinate found ({}) but no announcement on relays",
162 repo_coordinate.identifier
163 );
164 println!();
165 println!("if you created this repository, run `ngit repo init` to publish an announcement");
166 println!("if you are a co-maintainer, run `ngit repo accept` to publish your announcement");
167 }
93 return Ok(()); 168 return Ok(());
94 }; 169 };
95 170
96 print_repo_info( 171 if json {
97 &repo_ref, 172 print_repo_info_json(&repo_ref, &repo_coordinate, &git_repo)?;
98 my_pubkey.as_ref(), 173 } else {
99 &repo_coordinate, 174 println!("subcommands: init, edit, accept (run `ngit repo --help` for details)");
100 git_repo_path, 175 println!();
101 ) 176 print_repo_info(
102 .await; 177 &repo_ref,
178 my_pubkey.as_ref(),
179 &repo_coordinate,
180 git_repo_path,
181 )
182 .await;
183 }
184 Ok(())
185}
186
187fn print_repo_info_json(
188 repo_ref: &RepoRef,
189 coordinate: &Nip19Coordinate,
190 git_repo: &Repo,
191) -> Result<()> {
192 let nostr_url = git_repo
193 .git_repo
194 .find_remote("origin")
195 .ok()
196 .and_then(|r| r.url().map(std::string::ToString::to_string))
197 .filter(|u| u.starts_with("nostr://"));
198
199 let grasp_servers: Vec<String> = repo_ref
200 .git_server
201 .iter()
202 .filter(|s| is_grasp_server_clone_url(s))
203 .filter_map(|s| normalize_grasp_server_url(s).ok())
204 .collect();
205
206 let git_servers: Vec<String> = repo_ref
207 .git_server
208 .iter()
209 .filter(|s| !is_grasp_server_clone_url(s))
210 .cloned()
211 .collect();
212
213 let grasp_relay_urls: Vec<String> = repo_ref
214 .git_server
215 .iter()
216 .filter(|s| is_grasp_server_clone_url(s))
217 .filter_map(|s| format_grasp_server_url_as_relay_url(s).ok())
218 .collect();
219
220 let relays: Vec<String> = repo_ref
221 .relays
222 .iter()
223 .filter(|r| {
224 let r_str = r.as_str().trim_end_matches('/');
225 !grasp_relay_urls
226 .iter()
227 .any(|g| g.trim_end_matches('/') == r_str)
228 })
229 .map(std::string::ToString::to_string)
230 .collect();
231
232 let maintainers: Vec<String> = repo_ref
233 .maintainers
234 .iter()
235 .filter_map(|pk| pk.to_bech32().ok())
236 .collect();
237
238 let info = RepoInfoJson {
239 is_nostr_repo: true,
240 name: Some(repo_ref.name.clone()),
241 identifier: Some(repo_ref.identifier.clone()),
242 description: if repo_ref.description.is_empty() {
243 None
244 } else {
245 Some(repo_ref.description.clone())
246 },
247 nostr_url,
248 coordinate: coordinate.to_bech32().ok(),
249 web: if repo_ref.web.is_empty() {
250 None
251 } else {
252 Some(repo_ref.web.clone())
253 },
254 maintainers: Some(maintainers),
255 grasp_servers: if grasp_servers.is_empty() {
256 None
257 } else {
258 Some(grasp_servers)
259 },
260 git_servers: if git_servers.is_empty() {
261 None
262 } else {
263 Some(git_servers)
264 },
265 relays: if relays.is_empty() {
266 None
267 } else {
268 Some(relays)
269 },
270 hashtags: if repo_ref.hashtags.is_empty() {
271 None
272 } else {
273 Some(repo_ref.hashtags.clone())
274 },
275 };
276
277 println!("{}", serde_json::to_string_pretty(&info)?);
103 Ok(()) 278 Ok(())
104} 279}
105 280