upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/init.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-09-04 08:04:48 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-09-04 13:30:59 +0100
commit949c6459aa7683453a7160423b689ceadb08954b (patch)
tree230c26ecb11b99916e5570e548673eb09ecf0a36 /src/bin/ngit/sub_commands/init.rs
parenta825311f2c55661aaab3a163bda9109295c96044 (diff)
refactor: organise into lib and bin structure
the make the code more readable this commit just moves the files, the next commit should fix the imports
Diffstat (limited to 'src/bin/ngit/sub_commands/init.rs')
-rw-r--r--src/bin/ngit/sub_commands/init.rs385
1 files changed, 385 insertions, 0 deletions
diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs
new file mode 100644
index 0000000..5b7e03d
--- /dev/null
+++ b/src/bin/ngit/sub_commands/init.rs
@@ -0,0 +1,385 @@
1use std::collections::HashMap;
2
3use anyhow::{Context, Result};
4use nostr::{nips::nip01::Coordinate, FromBech32, PublicKey, ToBech32};
5use nostr_sdk::Kind;
6
7use super::send::send_events;
8#[cfg(not(test))]
9use crate::client::Client;
10#[cfg(test)]
11use crate::client::MockConnect;
12use crate::{
13 cli::Cli,
14 cli_interactor::{Interactor, InteractorPrompt, PromptInputParms},
15 client::{fetching_with_report, get_repo_ref_from_cache, Connect},
16 git::{convert_clone_url_to_https, Repo, RepoActions},
17 login,
18 repo_ref::{
19 extract_pks, get_repo_config_from_yaml, save_repo_config_to_yaml,
20 try_and_get_repo_coordinates, RepoRef,
21 },
22};
23
24#[derive(Debug, clap::Args)]
25pub struct SubCommandArgs {
26 #[clap(short, long)]
27 /// name of repository
28 title: Option<String>,
29 #[clap(short, long)]
30 /// optional description
31 description: Option<String>,
32 #[clap(long)]
33 /// git server url users can clone from
34 clone_url: Vec<String>,
35 #[clap(short, long, value_parser, num_args = 1..)]
36 /// homepage
37 web: Vec<String>,
38 #[clap(short, long, value_parser, num_args = 1..)]
39 /// relays contributors push patches and comments to
40 relays: Vec<String>,
41 #[clap(short, long, value_parser, num_args = 1..)]
42 /// npubs of other maintainers
43 other_maintainers: Vec<String>,
44 #[clap(long)]
45 /// usually root commit but will be more recent commit for forks
46 earliest_unique_commit: Option<String>,
47 #[clap(short, long)]
48 /// shortname with no spaces or special characters
49 identifier: Option<String>,
50}
51
52#[allow(clippy::too_many_lines)]
53pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
54 let git_repo = Repo::discover().context("cannot find a git repository")?;
55 let git_repo_path = git_repo.get_path()?;
56
57 let root_commit = git_repo
58 .get_root_commit()
59 .context("failed to get root commit of the repository")?;
60
61 // TODO: check for empty repo
62 // TODO: check for existing maintaiers file
63
64 #[cfg(not(test))]
65 let mut client = Client::default();
66 #[cfg(test)]
67 let mut client = <MockConnect as std::default::Default>::default();
68
69 let repo_coordinates = if let Ok(repo_coordinates) =
70 try_and_get_repo_coordinates(&git_repo, &client, false).await
71 {
72 Some(repo_coordinates)
73 } else {
74 None
75 };
76
77 let repo_ref = if let Some(repo_coordinates) = repo_coordinates {
78 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
79 Some(get_repo_ref_from_cache(git_repo_path, &repo_coordinates).await?)
80 } else {
81 None
82 };
83
84 let (signer, user_ref) = login::launch(
85 &git_repo,
86 &cli_args.bunker_uri,
87 &cli_args.bunker_app_key,
88 &cli_args.nsec,
89 &cli_args.password,
90 Some(&client),
91 false,
92 false,
93 )
94 .await?;
95
96 let repo_config_result = get_repo_config_from_yaml(&git_repo);
97 // TODO: check for other claims
98
99 let name = match &args.title {
100 Some(t) => t.clone(),
101 None => Interactor::default().input(
102 PromptInputParms::default()
103 .with_prompt("name")
104 .with_default(if let Some(repo_ref) = &repo_ref {
105 repo_ref.name.clone()
106 } else {
107 String::new()
108 }),
109 )?,
110 };
111
112 let identifier = match &args.identifier {
113 Some(t) => t.clone(),
114 None => Interactor::default().input(
115 PromptInputParms::default()
116 .with_prompt("identifier")
117 .with_default(if let Some(repo_ref) = &repo_ref {
118 repo_ref.identifier.clone()
119 } else {
120 let fallback = name
121 .clone()
122 .replace(' ', "-")
123 .chars()
124 .map(|c| {
125 if c.is_ascii_alphanumeric() || c.eq(&'/') {
126 c
127 } else {
128 '-'
129 }
130 })
131 .collect();
132 if let Ok(config) = &repo_config_result {
133 if let Some(identifier) = &config.identifier {
134 identifier.to_string()
135 } else {
136 fallback
137 }
138 } else {
139 fallback
140 }
141 }),
142 )?,
143 };
144
145 let description = match &args.description {
146 Some(t) => t.clone(),
147 None => Interactor::default().input(
148 PromptInputParms::default()
149 .with_prompt("description")
150 .with_default(if let Some(repo_ref) = &repo_ref {
151 repo_ref.description.clone()
152 } else {
153 String::new()
154 }),
155 )?,
156 };
157
158 let git_server = if args.clone_url.is_empty() {
159 Interactor::default()
160 .input(
161 PromptInputParms::default()
162 .with_prompt("clone url (for fetch)")
163 .with_default(if let Some(repo_ref) = &repo_ref {
164 repo_ref.git_server.clone().join(" ")
165 } else if let Ok(url) = git_repo.get_origin_url() {
166 if let Ok(fetch_url) = convert_clone_url_to_https(&url) {
167 fetch_url
168 } else {
169 // local repo or custom protocol
170 url
171 }
172 } else {
173 String::new()
174 }),
175 )?
176 .split(' ')
177 .map(std::string::ToString::to_string)
178 .collect()
179 } else {
180 args.clone_url.clone()
181 };
182
183 let web: Vec<String> = if args.web.is_empty() {
184 Interactor::default()
185 .input(
186 PromptInputParms::default()
187 .with_prompt("web")
188 .optional()
189 .with_default(if let Some(repo_ref) = &repo_ref {
190 repo_ref.web.clone().join(" ")
191 } else {
192 format!("https://gitworkshop.dev/repo/{}", &identifier)
193 }),
194 )?
195 .split(' ')
196 .map(std::string::ToString::to_string)
197 .collect()
198 } else {
199 args.web.clone()
200 };
201
202 let maintainers: Vec<PublicKey> = {
203 let mut dont_ask = !args.other_maintainers.is_empty();
204 let mut maintainers_string = if !args.other_maintainers.is_empty() {
205 [args.other_maintainers.clone()].concat().join(" ")
206 } else if repo_ref.is_none() && repo_config_result.is_err() {
207 signer.public_key().await?.to_bech32()?
208 } else {
209 let maintainers = if let Ok(config) = &repo_config_result {
210 config.maintainers.clone()
211 } else if let Some(repo_ref) = &repo_ref {
212 repo_ref
213 .maintainers
214 .clone()
215 .iter()
216 .map(|k| k.to_bech32().unwrap())
217 .collect()
218 } else {
219 //unreachable
220 vec![signer.public_key().await?.to_bech32()?]
221 };
222 // add current user if not present
223 if maintainers.iter().any(|m| {
224 if let Ok(m_pubkey) = PublicKey::from_bech32(m) {
225 user_ref.public_key.eq(&m_pubkey)
226 } else {
227 false
228 }
229 }) {
230 maintainers.join(" ")
231 } else {
232 [maintainers, vec![signer.public_key().await?.to_bech32()?]]
233 .concat()
234 .join(" ")
235 }
236 };
237 'outer: loop {
238 if !dont_ask {
239 println!("{}", &maintainers_string);
240 maintainers_string = Interactor::default().input(
241 PromptInputParms::default()
242 .with_prompt("maintainers")
243 .with_default(maintainers_string),
244 )?;
245 }
246 let mut maintainers: Vec<PublicKey> = vec![];
247 for m in maintainers_string.split(' ') {
248 if let Ok(m_pubkey) = PublicKey::from_bech32(m) {
249 maintainers.push(m_pubkey);
250 } else {
251 println!("not a valid set of npubs seperated by a space");
252 dont_ask = false;
253 continue 'outer;
254 }
255 }
256 // add current user incase removed
257 if !maintainers.iter().any(|m| user_ref.public_key.eq(m)) {
258 maintainers.push(signer.public_key().await?);
259 }
260 break maintainers;
261 }
262 };
263
264 // TODO: check if relays are free to post to so contributors can submit patches
265 // TODO: recommend some reliable free ones
266 let relays: Vec<String> = if args.relays.is_empty() {
267 Interactor::default()
268 .input(
269 PromptInputParms::default()
270 .with_prompt("relays")
271 .with_default(if let Ok(config) = &repo_config_result {
272 config.relays.clone().join(" ")
273 } else if let Some(repo_ref) = &repo_ref {
274 repo_ref.relays.clone().join(" ")
275 } else {
276 user_ref.relays.write().join(" ")
277 }),
278 )?
279 .split(' ')
280 .map(std::string::ToString::to_string)
281 .collect()
282 } else {
283 args.relays.clone()
284 };
285
286 let earliest_unique_commit = match &args.earliest_unique_commit {
287 Some(t) => t.clone(),
288 None => {
289 let mut earliest_unique_commit = if let Some(repo_ref) = &repo_ref {
290 repo_ref.root_commit.clone()
291 } else {
292 root_commit.to_string()
293 };
294 loop {
295 earliest_unique_commit = Interactor::default().input(
296 PromptInputParms::default()
297 .with_prompt("earliest unique commit")
298 .with_default(earliest_unique_commit.clone()),
299 )?;
300 if let Ok(exists) = git_repo.does_commit_exist(&earliest_unique_commit) {
301 if exists {
302 break earliest_unique_commit;
303 }
304 println!("commit does not exist on current repository");
305 } else {
306 println!("commit id not formatted correctly");
307 }
308 if earliest_unique_commit.len().ne(&40) {
309 println!("commit id must be 40 characters long");
310 }
311 }
312 }
313 };
314
315 println!("publishing repostory reference...");
316
317 let repo_ref = RepoRef {
318 identifier: identifier.clone(),
319 name,
320 description,
321 root_commit: earliest_unique_commit,
322 git_server,
323 web,
324 relays: relays.clone(),
325 maintainers: maintainers.clone(),
326 events: HashMap::new(),
327 };
328 let repo_event = repo_ref.to_event(&signer).await?;
329
330 client.set_signer(signer).await;
331
332 send_events(
333 &client,
334 git_repo_path,
335 vec![repo_event],
336 user_ref.relays.write(),
337 relays.clone(),
338 !cli_args.disable_cli_spinners,
339 false,
340 )
341 .await?;
342
343 git_repo.save_git_config_item(
344 "nostr.repo",
345 &Coordinate {
346 kind: Kind::GitRepoAnnouncement,
347 public_key: user_ref.public_key,
348 identifier: identifier.clone(),
349 relays: vec![],
350 }
351 .to_bech32()?,
352 false,
353 )?;
354
355 // if yaml file doesnt exist or needs updating
356 if match &repo_config_result {
357 Ok(config) => {
358 !<std::option::Option<std::string::String> as Clone>::clone(&config.identifier)
359 .unwrap_or_default()
360 .eq(&identifier)
361 || !extract_pks(config.maintainers.clone())?.eq(&maintainers)
362 || !config.relays.eq(&relays)
363 }
364 Err(_) => true,
365 } {
366 save_repo_config_to_yaml(
367 &git_repo,
368 identifier.clone(),
369 maintainers.clone(),
370 relays.clone(),
371 )?;
372 println!(
373 "maintainers.yaml {}. commit and push.",
374 if repo_config_result.is_err() {
375 "created"
376 } else {
377 "updated"
378 }
379 );
380 println!(
381 "this optional file helps in identifying who the maintainers are over time through the commit history"
382 );
383 }
384 Ok(())
385}