diff options
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/ngit/cli.rs | 5 | ||||
| -rw-r--r-- | src/bin/ngit/main.rs | 1 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/checkout.rs | 269 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/mod.rs | 1 |
4 files changed, 276 insertions, 0 deletions
diff --git a/src/bin/ngit/cli.rs b/src/bin/ngit/cli.rs index 47f4b27..5c1a097 100644 --- a/src/bin/ngit/cli.rs +++ b/src/bin/ngit/cli.rs | |||
| @@ -106,6 +106,11 @@ pub enum Commands { | |||
| 106 | Send(sub_commands::send::SubCommandArgs), | 106 | Send(sub_commands::send::SubCommandArgs), |
| 107 | /// list PRs; checkout, apply or download selected | 107 | /// list PRs; checkout, apply or download selected |
| 108 | List, | 108 | List, |
| 109 | /// checkout a proposal branch by event-id or nevent | ||
| 110 | Checkout { | ||
| 111 | /// Proposal event-id (hex) or nevent (bech32) | ||
| 112 | id: String, | ||
| 113 | }, | ||
| 109 | /// update repo git servers to reflect nostr state (add, update or delete | 114 | /// update repo git servers to reflect nostr state (add, update or delete |
| 110 | /// remote refs) | 115 | /// remote refs) |
| 111 | Sync(sub_commands::sync::SubCommandArgs), | 116 | Sync(sub_commands::sync::SubCommandArgs), |
diff --git a/src/bin/ngit/main.rs b/src/bin/ngit/main.rs index b6b51d0..2c9e10f 100644 --- a/src/bin/ngit/main.rs +++ b/src/bin/ngit/main.rs | |||
| @@ -53,6 +53,7 @@ async fn main() { | |||
| 53 | } | 53 | } |
| 54 | Commands::Send(args) => sub_commands::send::launch(&cli, args, false).await, | 54 | Commands::Send(args) => sub_commands::send::launch(&cli, args, false).await, |
| 55 | Commands::Sync(args) => sub_commands::sync::launch(args).await, | 55 | Commands::Sync(args) => sub_commands::sync::launch(args).await, |
| 56 | Commands::Checkout { id } => sub_commands::checkout::launch(id).await, | ||
| 56 | } | 57 | } |
| 57 | } else { | 58 | } else { |
| 58 | // Handle the case where no command is provided | 59 | // Handle the case where no command is provided |
diff --git a/src/bin/ngit/sub_commands/checkout.rs b/src/bin/ngit/sub_commands/checkout.rs new file mode 100644 index 0000000..0df1134 --- /dev/null +++ b/src/bin/ngit/sub_commands/checkout.rs | |||
| @@ -0,0 +1,269 @@ | |||
| 1 | use std::collections::HashSet; | ||
| 2 | |||
| 3 | use anyhow::{Context, Result, bail}; | ||
| 4 | use ngit::{ | ||
| 5 | client::{ | ||
| 6 | Params, get_all_proposal_patch_pr_pr_update_events_from_cache, | ||
| 7 | get_proposals_and_revisions_from_cache, | ||
| 8 | }, | ||
| 9 | fetch::fetch_from_git_server, | ||
| 10 | git_events::{ | ||
| 11 | KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch, | ||
| 12 | get_pr_tip_event_or_most_recent_patch_with_ancestors, tag_value, | ||
| 13 | }, | ||
| 14 | repo_ref::{RepoRef, is_grasp_server_in_list}, | ||
| 15 | }; | ||
| 16 | use nostr::nips::nip19::Nip19; | ||
| 17 | use nostr_sdk::{EventId, FromBech32}; | ||
| 18 | |||
| 19 | use crate::{ | ||
| 20 | client::{Client, Connect, fetching_with_report, get_repo_ref_from_cache}, | ||
| 21 | git::{Repo, RepoActions, str_to_sha1}, | ||
| 22 | git_events::{event_to_cover_letter, patch_supports_commit_ids}, | ||
| 23 | repo_ref::get_repo_coordinates_when_remote_unknown, | ||
| 24 | }; | ||
| 25 | |||
| 26 | pub async fn launch(id: &str) -> Result<()> { | ||
| 27 | let event_id = parse_event_id(id)?; | ||
| 28 | |||
| 29 | let git_repo = Repo::discover().context("failed to find a git repository")?; | ||
| 30 | let git_repo_path = git_repo.get_path()?; | ||
| 31 | |||
| 32 | let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo))); | ||
| 33 | |||
| 34 | let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?; | ||
| 35 | |||
| 36 | fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; | ||
| 37 | |||
| 38 | let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?; | ||
| 39 | |||
| 40 | let proposals_and_revisions: Vec<nostr::Event> = | ||
| 41 | get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()).await?; | ||
| 42 | |||
| 43 | let proposal = proposals_and_revisions | ||
| 44 | .iter() | ||
| 45 | .find(|e| e.id == event_id) | ||
| 46 | .context(format!("proposal with id {} not found in cache", event_id.to_hex()))?; | ||
| 47 | |||
| 48 | let cover_letter = event_to_cover_letter(proposal) | ||
| 49 | .context("failed to extract proposal details from proposal root event")?; | ||
| 50 | |||
| 51 | let commits_events: Vec<nostr::Event> = get_all_proposal_patch_pr_pr_update_events_from_cache( | ||
| 52 | git_repo_path, | ||
| 53 | &repo_ref, | ||
| 54 | &proposal.id, | ||
| 55 | ) | ||
| 56 | .await?; | ||
| 57 | |||
| 58 | let most_recent_proposal_patch_chain_or_pr_or_pr_update = | ||
| 59 | get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone()) | ||
| 60 | .context("failed to find any PR or patch events on this proposal")?; | ||
| 61 | |||
| 62 | if most_recent_proposal_patch_chain_or_pr_or_pr_update | ||
| 63 | .iter() | ||
| 64 | .any(|e| [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&e.kind)) | ||
| 65 | { | ||
| 66 | checkout_pr( | ||
| 67 | &git_repo, | ||
| 68 | &repo_ref, | ||
| 69 | &cover_letter, | ||
| 70 | &most_recent_proposal_patch_chain_or_pr_or_pr_update, | ||
| 71 | ) | ||
| 72 | } else { | ||
| 73 | checkout_patch( | ||
| 74 | &git_repo, | ||
| 75 | &cover_letter, | ||
| 76 | &most_recent_proposal_patch_chain_or_pr_or_pr_update, | ||
| 77 | ) | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | fn parse_event_id(id: &str) -> Result<EventId> { | ||
| 82 | if let Ok(nip19) = Nip19::from_bech32(id) { | ||
| 83 | match nip19 { | ||
| 84 | Nip19::Event(e) => return Ok(e.event_id), | ||
| 85 | Nip19::EventId(event_id) => return Ok(event_id), | ||
| 86 | _ => {} | ||
| 87 | } | ||
| 88 | } | ||
| 89 | if let Ok(event_id) = EventId::from_hex(id) { | ||
| 90 | return Ok(event_id); | ||
| 91 | } | ||
| 92 | bail!("invalid event-id or nevent: {id}") | ||
| 93 | } | ||
| 94 | |||
| 95 | fn checkout_pr( | ||
| 96 | git_repo: &Repo, | ||
| 97 | repo_ref: &RepoRef, | ||
| 98 | cover_letter: &crate::git_events::CoverLetter, | ||
| 99 | most_recent_proposal_patch_chain_or_pr_or_pr_update: &[nostr::Event], | ||
| 100 | ) -> Result<()> { | ||
| 101 | let branch_name = cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?; | ||
| 102 | let local_branch_tip = git_repo.get_tip_of_branch(&branch_name).ok(); | ||
| 103 | let proposal_tip_event = most_recent_proposal_patch_chain_or_pr_or_pr_update | ||
| 104 | .first() | ||
| 105 | .context("most_recent_proposal_patch_chain_or_pr_or_pr_update will always contain an event with c tag")?; | ||
| 106 | let proposal_tip = tag_value(proposal_tip_event, "c")?; | ||
| 107 | |||
| 108 | if let Some(local_branch_tip) = local_branch_tip { | ||
| 109 | git_repo | ||
| 110 | .checkout(&branch_name) | ||
| 111 | .context("cannot checkout existing proposal branch")?; | ||
| 112 | if local_branch_tip.to_string() == proposal_tip { | ||
| 113 | println!("checked out up-to-date proposal branch '{branch_name}'"); | ||
| 114 | return Ok(()); | ||
| 115 | } | ||
| 116 | if git_repo.does_commit_exist(&proposal_tip)? { | ||
| 117 | git_repo.create_branch_at_commit(&branch_name, &proposal_tip)?; | ||
| 118 | git_repo.checkout(&branch_name)?; | ||
| 119 | println!("checked out proposal branch and updated tip '{branch_name}'"); | ||
| 120 | return Ok(()); | ||
| 121 | } | ||
| 122 | } | ||
| 123 | |||
| 124 | fetch_oid_for_from_servers_for_pr( | ||
| 125 | &proposal_tip, | ||
| 126 | git_repo, | ||
| 127 | repo_ref, | ||
| 128 | proposal_tip_event, | ||
| 129 | )?; | ||
| 130 | git_repo.create_branch_at_commit(&branch_name, &proposal_tip)?; | ||
| 131 | git_repo.checkout(&branch_name)?; | ||
| 132 | if local_branch_tip.is_some() { | ||
| 133 | println!("checked out proposal branch and pulled updates '{branch_name}'"); | ||
| 134 | } else { | ||
| 135 | println!("created and checked out proposal branch '{branch_name}'"); | ||
| 136 | } | ||
| 137 | Ok(()) | ||
| 138 | } | ||
| 139 | |||
| 140 | fn checkout_patch( | ||
| 141 | git_repo: &Repo, | ||
| 142 | cover_letter: &crate::git_events::CoverLetter, | ||
| 143 | most_recent_proposal_patch_chain_or_pr_or_pr_update: &[nostr::Event], | ||
| 144 | ) -> Result<()> { | ||
| 145 | let no_support_for_patches_as_branch = most_recent_proposal_patch_chain_or_pr_or_pr_update | ||
| 146 | .iter() | ||
| 147 | .any(|event| !patch_supports_commit_ids(event)); | ||
| 148 | |||
| 149 | if no_support_for_patches_as_branch { | ||
| 150 | bail!( | ||
| 151 | "this proposal cannot be checked out as a branch because some patches do not have a parent commit.\n\ | ||
| 152 | Try `ngit apply --stdout` to apply patches to the current branch, or use `ngit list` for interactive options." | ||
| 153 | ); | ||
| 154 | } | ||
| 155 | |||
| 156 | let proposal_base_commit = str_to_sha1(&tag_value( | ||
| 157 | most_recent_proposal_patch_chain_or_pr_or_pr_update | ||
| 158 | .last() | ||
| 159 | .context("there should be at least one patch")?, | ||
| 160 | "parent-commit", | ||
| 161 | )?) | ||
| 162 | .context("failed to get valid parent commit id from patch")?; | ||
| 163 | |||
| 164 | let (main_branch_name, _master_tip) = git_repo.get_main_or_master_branch()?; | ||
| 165 | |||
| 166 | if !git_repo.does_commit_exist(&proposal_base_commit.to_string())? { | ||
| 167 | bail!( | ||
| 168 | "the proposal parent commit doesn't exist in your local repository.\n\ | ||
| 169 | Try running `git pull` on '{main_branch_name}' first, or use `ngit apply --stdout` to apply patches to the current branch." | ||
| 170 | ); | ||
| 171 | } | ||
| 172 | |||
| 173 | if git_repo.has_outstanding_changes()? { | ||
| 174 | bail!( | ||
| 175 | "working directory is not clean. Discard or stash (un)staged changes and try again." | ||
| 176 | ); | ||
| 177 | } | ||
| 178 | |||
| 179 | let branch_name = cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?; | ||
| 180 | let branch_exists = git_repo | ||
| 181 | .get_local_branch_names() | ||
| 182 | .context("failed to get local branch names")? | ||
| 183 | .iter() | ||
| 184 | .any(|n| n.eq(&branch_name)); | ||
| 185 | |||
| 186 | if !branch_exists { | ||
| 187 | let _ = git_repo | ||
| 188 | .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain_or_pr_or_pr_update.to_vec()) | ||
| 189 | .context("failed to apply patch chain")?; | ||
| 190 | println!("checked out proposal as '{branch_name}' branch"); | ||
| 191 | return Ok(()); | ||
| 192 | } | ||
| 193 | |||
| 194 | let local_branch_tip = git_repo.get_tip_of_branch(&branch_name)?; | ||
| 195 | |||
| 196 | let proposal_tip = str_to_sha1( | ||
| 197 | &get_commit_id_from_patch( | ||
| 198 | most_recent_proposal_patch_chain_or_pr_or_pr_update | ||
| 199 | .first() | ||
| 200 | .context("there should be at least one patch")?, | ||
| 201 | ) | ||
| 202 | .context("failed to get valid commit_id from patch")?, | ||
| 203 | ) | ||
| 204 | .context("failed to get valid commit_id from patch")?; | ||
| 205 | |||
| 206 | if proposal_tip.eq(&local_branch_tip) { | ||
| 207 | git_repo.checkout(&branch_name)?; | ||
| 208 | println!("branch '{branch_name}' checked out and up-to-date"); | ||
| 209 | return Ok(()); | ||
| 210 | } | ||
| 211 | |||
| 212 | git_repo.create_branch_at_commit(&branch_name, &proposal_base_commit.to_string())?; | ||
| 213 | git_repo.checkout(&branch_name)?; | ||
| 214 | let _ = git_repo | ||
| 215 | .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain_or_pr_or_pr_update.to_vec()) | ||
| 216 | .context("failed to apply patch chain")?; | ||
| 217 | println!("checked out updated proposal as '{branch_name}' branch"); | ||
| 218 | Ok(()) | ||
| 219 | } | ||
| 220 | |||
| 221 | fn fetch_oid_for_from_servers_for_pr( | ||
| 222 | oid: &str, | ||
| 223 | git_repo: &Repo, | ||
| 224 | repo_ref: &RepoRef, | ||
| 225 | pr_or_pr_update_event: &nostr::Event, | ||
| 226 | ) -> Result<()> { | ||
| 227 | let git_servers = { | ||
| 228 | let mut seen: HashSet<String> = HashSet::new(); | ||
| 229 | let mut out: Vec<String> = vec![]; | ||
| 230 | for tag in pr_or_pr_update_event.tags.as_slice() { | ||
| 231 | if tag.kind().eq(&nostr::event::TagKind::Clone) { | ||
| 232 | for clone_url in tag.as_slice().iter().skip(1) { | ||
| 233 | seen.insert(clone_url.clone()); | ||
| 234 | } | ||
| 235 | } | ||
| 236 | } | ||
| 237 | for server in &repo_ref.git_server { | ||
| 238 | if seen.insert(server.clone()) { | ||
| 239 | out.push(server.clone()); | ||
| 240 | } | ||
| 241 | } | ||
| 242 | out | ||
| 243 | }; | ||
| 244 | |||
| 245 | let mut errors = vec![]; | ||
| 246 | let term = console::Term::stderr(); | ||
| 247 | |||
| 248 | for git_server_url in &git_servers { | ||
| 249 | if let Err(error) = fetch_from_git_server( | ||
| 250 | git_repo, | ||
| 251 | &[oid.to_string()], | ||
| 252 | git_server_url, | ||
| 253 | &repo_ref.to_nostr_git_url(&None), | ||
| 254 | &term, | ||
| 255 | is_grasp_server_in_list(git_server_url, &repo_ref.grasp_servers()), | ||
| 256 | ) { | ||
| 257 | errors.push(error); | ||
| 258 | } else { | ||
| 259 | println!("fetched proposal git data from {git_server_url}"); | ||
| 260 | break; | ||
| 261 | } | ||
| 262 | } | ||
| 263 | if !git_repo.does_commit_exist(oid)? { | ||
| 264 | bail!( | ||
| 265 | "cannot find proposal git data from proposal git server hint or repository git servers" | ||
| 266 | ) | ||
| 267 | } | ||
| 268 | Ok(()) | ||
| 269 | } | ||
diff --git a/src/bin/ngit/sub_commands/mod.rs b/src/bin/ngit/sub_commands/mod.rs index 9c84ef2..e9f91db 100644 --- a/src/bin/ngit/sub_commands/mod.rs +++ b/src/bin/ngit/sub_commands/mod.rs | |||
| @@ -1,3 +1,4 @@ | |||
| 1 | pub mod checkout; | ||
| 1 | pub mod create; | 2 | pub mod create; |
| 2 | pub mod export_keys; | 3 | pub mod export_keys; |
| 3 | pub mod init; | 4 | pub mod init; |