diff options
Diffstat (limited to 'src/sub_commands/init.rs')
| -rw-r--r-- | src/sub_commands/init.rs | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/src/sub_commands/init.rs b/src/sub_commands/init.rs new file mode 100644 index 0000000..e5fa52c --- /dev/null +++ b/src/sub_commands/init.rs | |||
| @@ -0,0 +1,247 @@ | |||
| 1 | use std::{str::FromStr, fs::{OpenOptions, File}, io::Write}; | ||
| 2 | use dialoguer::{theme::ColorfulTheme, Confirm, Input}; | ||
| 3 | use clap::{Args}; | ||
| 4 | use indicatif::ProgressBar; | ||
| 5 | use nostr_sdk::prelude::*; | ||
| 6 | |||
| 7 | use crate::{config::{load_config, save_conifg}, groups::{init::{InitializeGroup}, group::{Group}}, repos::{init::InitializeRepo, repo::Repo}, utils::{save_event, create_client, get_or_generate_keys}, cli_helpers::select_relays, repo_config::RepoConfig}; | ||
| 8 | |||
| 9 | #[derive(Args)] | ||
| 10 | struct InitRepo { | ||
| 11 | // Repo Name | ||
| 12 | #[arg(short, long)] | ||
| 13 | name: String, | ||
| 14 | /// Admin Group ID | ||
| 15 | #[arg(long)] | ||
| 16 | admin_group_id: Option<String>, | ||
| 17 | /// Relays | ||
| 18 | #[arg(short, long)] | ||
| 19 | relays: Option<String>, | ||
| 20 | } | ||
| 21 | |||
| 22 | #[derive(Args)] | ||
| 23 | pub struct InitSubCommand { | ||
| 24 | /// Repo Name | ||
| 25 | #[arg(short, long)] | ||
| 26 | name: Option<String>, | ||
| 27 | } | ||
| 28 | |||
| 29 | pub fn create_and_broadcast_init( | ||
| 30 | relays: Vec<String>, | ||
| 31 | sub_command_args: &InitSubCommand, | ||
| 32 | ) -> Result<()> { | ||
| 33 | |||
| 34 | let mut cfg = load_config(); | ||
| 35 | |||
| 36 | let repo_dir_path = std::env::current_dir().unwrap(); | ||
| 37 | |||
| 38 | // check for potential problems | ||
| 39 | let ngit_path = repo_dir_path.clone().join(".ngit"); | ||
| 40 | if ngit_path.is_dir() && ( | ||
| 41 | !Confirm::with_theme(&ColorfulTheme::default()) | ||
| 42 | .with_prompt("ngit already initialized! Do you want overwrite it with a fresh repoisotry?") | ||
| 43 | .default(false) | ||
| 44 | .interact() | ||
| 45 | .unwrap() | ||
| 46 | || !Confirm::with_theme(&ColorfulTheme::default()) | ||
| 47 | .with_prompt("Are you sure?") | ||
| 48 | .default(false) | ||
| 49 | .interact() | ||
| 50 | .unwrap() | ||
| 51 | ) { panic!("aborted as ngit repository already exists."); }; | ||
| 52 | |||
| 53 | let git_path = repo_dir_path.clone().join(".git"); | ||
| 54 | if git_path.is_dir() && ( | ||
| 55 | !Confirm::with_theme(&ColorfulTheme::default()) | ||
| 56 | .with_prompt("git has already been initialized here. For this alpha ngit prototype its best to start with a fresh repository. Continue anyway?") | ||
| 57 | .default(false) | ||
| 58 | .interact() | ||
| 59 | .unwrap() | ||
| 60 | ) { panic!("aborted as git repository already initialized."); }; | ||
| 61 | |||
| 62 | // collect information from user | ||
| 63 | |||
| 64 | let dir_name: String =String::from(repo_dir_path.file_name().unwrap().to_string_lossy()); | ||
| 65 | let repo_name: String = match &sub_command_args.name { | ||
| 66 | Some(name) => name.clone(), | ||
| 67 | None => { | ||
| 68 | Input::with_theme(&ColorfulTheme::default()) | ||
| 69 | .with_prompt("Repo Name") | ||
| 70 | .default(dir_name) | ||
| 71 | .interact_text() | ||
| 72 | .unwrap() | ||
| 73 | }, | ||
| 74 | }; | ||
| 75 | let repo_relays = select_relays(&mut cfg, &relays)?; | ||
| 76 | |||
| 77 | let mut repo_group_members: Vec<String> = vec![]; | ||
| 78 | loop { | ||
| 79 | if Confirm::with_theme(&ColorfulTheme::default()) | ||
| 80 | .with_prompt("Would you like add other maintainers now?") | ||
| 81 | .default(false) | ||
| 82 | .interact() | ||
| 83 | .unwrap() | ||
| 84 | { | ||
| 85 | let member_key_input: String = Input::with_theme(&ColorfulTheme::default()) | ||
| 86 | .with_prompt("npub or hex (n to abort)") | ||
| 87 | .interact_text() | ||
| 88 | .unwrap() | ||
| 89 | ; | ||
| 90 | if member_key_input.starts_with("npub") { | ||
| 91 | match XOnlyPublicKey::from_bech32(member_key_input) { | ||
| 92 | Ok(k) => { repo_group_members.push(k.to_string()); }, | ||
| 93 | Err(e) => { println!("{}",e) }, | ||
| 94 | }} | ||
| 95 | else { | ||
| 96 | match XOnlyPublicKey::from_str(member_key_input.as_str()) { | ||
| 97 | Ok(k) => { repo_group_members.push(k.to_string()); }, | ||
| 98 | Err(e) => { println!("{}",e) }, | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } | ||
| 102 | else { break; } | ||
| 103 | } | ||
| 104 | |||
| 105 | let keys = get_or_generate_keys(&mut cfg); | ||
| 106 | let mut events_to_broadcast: Vec<Event> = vec![]; | ||
| 107 | |||
| 108 | // delay adding user as group member so keys are the last thing asked for | ||
| 109 | repo_group_members.push(keys.public_key().to_string()); | ||
| 110 | |||
| 111 | let admin_group_event = match cfg.default_admin_group_event_serialized { | ||
| 112 | None => { | ||
| 113 | let new_admin_group = Group::new( | ||
| 114 | &InitializeGroup::new() | ||
| 115 | .members( | ||
| 116 | vec![ | ||
| 117 | keys.public_key().to_string(), | ||
| 118 | ], | ||
| 119 | vec![], | ||
| 120 | ) | ||
| 121 | .relays(&repo_relays), | ||
| 122 | &keys, | ||
| 123 | ).unwrap(); | ||
| 124 | cfg.default_admin_group_event_serialized = Some(new_admin_group.events[0].as_json()); | ||
| 125 | save_conifg(&cfg); | ||
| 126 | events_to_broadcast.push(new_admin_group.events[0].clone()); | ||
| 127 | new_admin_group.events[0].clone() | ||
| 128 | }, | ||
| 129 | Some(admin) => Group::new_from_json_event(admin.clone()) | ||
| 130 | .expect("default_admin_group_event_serialized in MyConfig to load into Group") | ||
| 131 | .events[0].clone(), | ||
| 132 | }; | ||
| 133 | |||
| 134 | let new_repo_group = Group::new( | ||
| 135 | &InitializeGroup::new() | ||
| 136 | .name(format!("{repo_name} maintainers (ngit)")) | ||
| 137 | .members( | ||
| 138 | repo_group_members, | ||
| 139 | vec![], | ||
| 140 | ) | ||
| 141 | .relays(&repo_relays) | ||
| 142 | , | ||
| 143 | &keys, | ||
| 144 | ).unwrap(); | ||
| 145 | events_to_broadcast.push(new_repo_group.events[0].clone()); | ||
| 146 | |||
| 147 | let new_repo = Repo::new( | ||
| 148 | &InitializeRepo::new() | ||
| 149 | .name(&repo_name) | ||
| 150 | .relays(&repo_relays) | ||
| 151 | .maintainers_group(new_repo_group.get_ref()) | ||
| 152 | , | ||
| 153 | &keys, | ||
| 154 | ).unwrap(); | ||
| 155 | events_to_broadcast.push(new_repo.events[0].clone()); | ||
| 156 | |||
| 157 | // crate .ngit folder and store the repo and group reference and associated events (?) | ||
| 158 | for p in [ | ||
| 159 | "groups", | ||
| 160 | "branches", | ||
| 161 | "patches", | ||
| 162 | "merges", | ||
| 163 | "prs", | ||
| 164 | "issues", | ||
| 165 | "comments", | ||
| 166 | ] { std::fs::create_dir_all(ngit_path.join(p)).unwrap(); } | ||
| 167 | |||
| 168 | // save repo event locally | ||
| 169 | save_event( | ||
| 170 | ngit_path.join(format!("groups/{}.json",admin_group_event.id.to_string())), | ||
| 171 | &admin_group_event, | ||
| 172 | ).unwrap(); | ||
| 173 | |||
| 174 | save_event( | ||
| 175 | ngit_path.join(format!("groups/{}.json",new_repo_group.id.to_string())), | ||
| 176 | &new_repo_group.events[0], | ||
| 177 | ).unwrap(); | ||
| 178 | save_event( | ||
| 179 | ngit_path.join("repo.json"), | ||
| 180 | &new_repo.events[0], | ||
| 181 | ).unwrap(); | ||
| 182 | |||
| 183 | // set repo config | ||
| 184 | let mut repo_config = RepoConfig::open(&repo_dir_path); | ||
| 185 | for b in ["main", "master"] { | ||
| 186 | repo_config.set_mapping(&b.to_string(), &new_repo.events[0].id.to_string()); | ||
| 187 | |||
| 188 | } | ||
| 189 | repo_config.set_last_branch_ref_update_time(new_repo.events[0].created_at.clone()); | ||
| 190 | |||
| 191 | // initialise git | ||
| 192 | git2::Repository::init(repo_dir_path.clone()).unwrap(); | ||
| 193 | |||
| 194 | // add .gitignore | ||
| 195 | let gitignore_path = repo_dir_path.join(".gitignore"); | ||
| 196 | let mut gitignore_file = if gitignore_path.is_file() { | ||
| 197 | OpenOptions::new() | ||
| 198 | .write(true) | ||
| 199 | .append(true) | ||
| 200 | .open(&gitignore_path) | ||
| 201 | .expect(".gitignore to open") | ||
| 202 | } else { | ||
| 203 | File::create(gitignore_path) | ||
| 204 | .expect("create and open .gitignore file") | ||
| 205 | }; | ||
| 206 | writeln!(gitignore_file, ".ngit") | ||
| 207 | .expect(".ngit added to gitignore"); | ||
| 208 | |||
| 209 | let spinner = ProgressBar::new_spinner(); | ||
| 210 | spinner.set_message("Broadcasting... if this takes 20s+, there was a problem broadcasting to one or more relays even if it says 'Repository Initialised'."); | ||
| 211 | |||
| 212 | let client = create_client(&keys, repo_relays.clone())?; | ||
| 213 | for e in &events_to_broadcast { | ||
| 214 | match client.send_event(e.clone()) { | ||
| 215 | Ok(_) => (), | ||
| 216 | // TODO: this isn't working - if a relay is specified with a type it will wait 30ish secs and then return successful | ||
| 217 | Err(e) => { println!("error broadcasting repo event: {}",e); }, | ||
| 218 | } | ||
| 219 | // TODO: better error handling here / reporting. potentially warn if taking a while and report on troublesome relays | ||
| 220 | } | ||
| 221 | spinner.finish_with_message(format!( | ||
| 222 | "Repository Initialised! id: {}", | ||
| 223 | Nip19Event::new( | ||
| 224 | new_repo.id.clone(), | ||
| 225 | vec![&repo_relays[0]], | ||
| 226 | ) | ||
| 227 | .to_bech32() | ||
| 228 | .expect("Nip19Event to convert to to_bech32") | ||
| 229 | |||
| 230 | )); | ||
| 231 | // println!("Hint: only maintainers can push to master branch and merge PRs but anyone can clone and push a forked branch."); | ||
| 232 | |||
| 233 | // Instructions: | ||
| 234 | // 1. make some commits on 'master' or 'main' branch. | ||
| 235 | // 2. 'ngit push' will push them to the nostr repo (via a patch). | ||
| 236 | // 3. make a branch called 'feature-1' | ||
| 237 | // 3. 'ngit push' will create a PR | ||
| 238 | |||
| 239 | // repo | ||
| 240 | // repo | ||
| 241 | // PRs | ||
| 242 | |||
| 243 | // patch | ||
| 244 | |||
| 245 | |||
| 246 | Ok(()) | ||
| 247 | } | ||