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/accept.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-20 21:54:00 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-20 21:54:00 +0000
commit8f1a1743bd4e85e922ec0cc1f050911a28af4cf0 (patch)
tree2d9eb4954395b62c66d1cc937c6e78a060129f3f /src/bin/ngit/sub_commands/repo/accept.rs
parent67f09343c15e6a0e3622811d3eb5e513a8205eda (diff)
add `ngit repo` subcommand group
- `ngit repo` (no subcommand): show repository info including maintainer tree, per-maintainer infrastructure attribution, and a note explaining the union-vs-personal field model and recursive maintainer sets - `ngit repo init`: alias for `ngit init` - `ngit repo edit`: same as init but signals intent to update an existing repository announcement - `ngit repo accept`: scoped command for co-maintainers to publish their announcement; errors with clear messages for all other states (trusted maintainer, already accepted, not invited, no repo found)
Diffstat (limited to 'src/bin/ngit/sub_commands/repo/accept.rs')
-rw-r--r--src/bin/ngit/sub_commands/repo/accept.rs263
1 files changed, 263 insertions, 0 deletions
diff --git a/src/bin/ngit/sub_commands/repo/accept.rs b/src/bin/ngit/sub_commands/repo/accept.rs
new file mode 100644
index 0000000..5564b77
--- /dev/null
+++ b/src/bin/ngit/sub_commands/repo/accept.rs
@@ -0,0 +1,263 @@
1use std::sync::Arc;
2
3use anyhow::{Context, Result};
4use ngit::{
5 accept_maintainership::{accept_maintainership_with_defaults, wait_for_grasp_servers},
6 cli_interactor::cli_error,
7 client::{Params, fetching_with_report, get_repo_ref_from_cache, send_events},
8 repo_ref::{RepoRef, apply_grasp_infrastructure, latest_event_repo_ref},
9};
10use nostr::{
11 ToBech32,
12 nips::{nip01::Coordinate, nip19::Nip19Coordinate},
13};
14use nostr_sdk::{Kind, NostrSigner, RelayUrl};
15
16use crate::{
17 cli::{Cli, extract_signer_cli_arguments},
18 client::{Client, Connect},
19 git::{Repo, RepoActions},
20 login,
21 repo_ref::try_and_get_repo_coordinates_when_remote_unknown,
22};
23
24#[derive(Debug, clap::Args)]
25pub struct SubCommandArgs {
26 #[clap(short, long, value_parser, num_args = 1..)]
27 /// where your git+nostr data is hosted (optional; uses your saved grasp
28 /// server list or the trusted maintainer's servers if not specified)
29 grasp_server: Vec<String>,
30}
31
32pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
33 let git_repo = Repo::discover().context("failed to find a git repository")?;
34 let git_repo_path = git_repo.get_path()?;
35 let mut client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));
36
37 let (signer, user_ref, _) = login::login_or_signup(
38 &Some(&git_repo),
39 &extract_signer_cli_arguments(cli_args).unwrap_or(None),
40 &cli_args.password,
41 Some(&client),
42 false,
43 )
44 .await?;
45
46 let my_pubkey = user_ref.public_key;
47
48 let repo_coordinate = (try_and_get_repo_coordinates_when_remote_unknown(&git_repo).await).ok();
49
50 let Some(repo_coordinate) = repo_coordinate else {
51 return Err(cli_error(
52 "no nostr repository found",
53 &[],
54 &["use `ngit repo init` to publish this repository to nostr"],
55 ));
56 };
57
58 // Fetch latest data from relays
59 fetching_with_report(git_repo_path, &client, &repo_coordinate).await?;
60
61 let Some(repo_ref) =
62 (get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinate).await).ok()
63 else {
64 return Err(cli_error(
65 "no announcement found on relays for this repository",
66 &[],
67 &[
68 "if you created this repository, use `ngit repo init` to publish an announcement",
69 "if this is a relay or network issue, try again later",
70 ],
71 ));
72 };
73
74 // Validate state
75 let trusted = repo_ref.trusted_maintainer;
76
77 if trusted == my_pubkey {
78 return Err(cli_error(
79 "you are already the trusted maintainer of this repository",
80 &[],
81 &["use `ngit repo edit` to update your announcement"],
82 ));
83 }
84
85 let has_announcement = repo_ref
86 .events
87 .keys()
88 .any(|c| c.coordinate.public_key == my_pubkey);
89
90 if has_announcement {
91 return Err(cli_error(
92 "you have already published a co-maintainer announcement for this repository",
93 &[],
94 &["use `ngit repo edit` to update your announcement"],
95 ));
96 }
97
98 if !repo_ref.maintainers.contains(&my_pubkey) {
99 let trusted_npub = trusted.to_bech32().unwrap_or_else(|_| trusted.to_hex());
100 return Err(cli_error(
101 "you have not been invited as a maintainer of this repository",
102 &[("trusted maintainer", trusted_npub.as_str())],
103 &["the trusted maintainer must add your npub to their announcement first"],
104 ));
105 }
106
107 // Happy path: CoMaintainer state without an existing announcement
108 let repo_name = &repo_ref.name;
109 let trusted_npub = trusted.to_bech32().unwrap_or_else(|_| trusted.to_hex());
110 println!("accepting co-maintainership of '{repo_name}' (offered by {trusted_npub})");
111 println!("publishing your repository announcement to nostr...");
112
113 if args.grasp_server.is_empty() {
114 // Use the existing defaults logic from the library
115 accept_maintainership_with_defaults(&git_repo, &repo_ref, &user_ref, &mut client, &signer)
116 .await?;
117 } else {
118 // User specified grasp servers explicitly — use them
119 accept_with_grasp_servers(
120 &git_repo,
121 &repo_ref,
122 &signer,
123 &user_ref,
124 &mut client,
125 &args.grasp_server,
126 )
127 .await?;
128 }
129
130 println!("co-maintainership accepted.");
131 println!("your announcement has been published to nostr. you can now push updates.");
132 println!("run `ngit repo edit` at any time to update your announcement.");
133
134 Ok(())
135}
136
137/// Accept co-maintainership with explicitly specified grasp servers.
138#[allow(clippy::too_many_lines)]
139async fn accept_with_grasp_servers(
140 git_repo: &Repo,
141 repo_ref: &RepoRef,
142 signer: &Arc<dyn NostrSigner>,
143 user_ref: &ngit::login::user::UserRef,
144 client: &mut Client,
145 grasp_servers: &[String],
146) -> Result<()> {
147 let my_pubkey = &user_ref.public_key;
148 let identifier = &repo_ref.identifier;
149
150 let mut git_servers: Vec<String> = vec![];
151 let mut relay_strings: Vec<String> = vec![];
152
153 apply_grasp_infrastructure(
154 grasp_servers,
155 &mut git_servers,
156 &mut relay_strings,
157 my_pubkey,
158 identifier,
159 )?;
160
161 let relays: Vec<RelayUrl> = relay_strings
162 .iter()
163 .filter_map(|r| RelayUrl::parse(r).ok())
164 .collect();
165
166 let latest = latest_event_repo_ref(repo_ref);
167 let name = latest
168 .as_ref()
169 .map_or_else(|| identifier.clone(), |lr| lr.name.clone());
170 let description = latest
171 .as_ref()
172 .map(|lr| lr.description.clone())
173 .unwrap_or_default();
174 let web = latest.as_ref().map(|lr| lr.web.clone()).unwrap_or_default();
175 let hashtags = latest
176 .as_ref()
177 .map(|lr| lr.hashtags.clone())
178 .unwrap_or_default();
179 let blossoms = latest
180 .as_ref()
181 .map(|lr| lr.blossoms.clone())
182 .unwrap_or_default();
183 let root_commit = latest
184 .as_ref()
185 .map(|lr| lr.root_commit.clone())
186 .filter(|c| !c.is_empty())
187 .unwrap_or_else(|| repo_ref.root_commit.clone());
188
189 let mut maintainers = vec![*my_pubkey];
190 if repo_ref.trusted_maintainer != *my_pubkey {
191 maintainers.push(repo_ref.trusted_maintainer);
192 }
193
194 let my_repo_ref = RepoRef {
195 identifier: identifier.clone(),
196 name,
197 description,
198 root_commit,
199 git_server: git_servers,
200 web,
201 relays: relays.clone(),
202 blossoms,
203 hashtags,
204 trusted_maintainer: *my_pubkey,
205 maintainers_without_annoucnement: None,
206 maintainers,
207 events: std::collections::HashMap::new(),
208 nostr_git_url: None,
209 };
210
211 let repo_event = my_repo_ref.to_event(signer).await?;
212
213 client.set_signer(signer.clone()).await;
214
215 send_events(
216 client,
217 Some(git_repo.get_path()?),
218 vec![repo_event],
219 user_ref.relays.write(),
220 relays.clone(),
221 true,
222 false,
223 )
224 .await
225 .context("failed to publish co-maintainer announcement")?;
226
227 if !grasp_servers.is_empty() {
228 wait_for_grasp_servers(git_repo, grasp_servers, my_pubkey, identifier).await?;
229 }
230
231 // Update nostr.repo git config
232 git_repo
233 .save_git_config_item(
234 "nostr.repo",
235 &Nip19Coordinate {
236 coordinate: Coordinate {
237 kind: Kind::GitRepoAnnouncement,
238 public_key: *my_pubkey,
239 identifier: identifier.clone(),
240 },
241 relays: vec![],
242 }
243 .to_bech32()?,
244 false,
245 )
246 .context("failed to update nostr.repo git config")?;
247
248 // Update origin remote
249 let nostr_url = my_repo_ref.to_nostr_git_url(&Some(git_repo)).to_string();
250 if git_repo.git_repo.find_remote("origin").is_ok() {
251 git_repo
252 .git_repo
253 .remote_set_url("origin", &nostr_url)
254 .context("failed to update origin remote")?;
255 } else {
256 git_repo
257 .git_repo
258 .remote("origin", &nostr_url)
259 .context("failed to set origin remote")?;
260 }
261
262 Ok(())
263}