diff options
Diffstat (limited to 'src/bin/git_remote_nostr/utils.rs')
| -rw-r--r-- | src/bin/git_remote_nostr/utils.rs | 248 |
1 files changed, 248 insertions, 0 deletions
diff --git a/src/bin/git_remote_nostr/utils.rs b/src/bin/git_remote_nostr/utils.rs new file mode 100644 index 0000000..c53c34f --- /dev/null +++ b/src/bin/git_remote_nostr/utils.rs | |||
| @@ -0,0 +1,248 @@ | |||
| 1 | use std::{ | ||
| 2 | collections::HashMap, | ||
| 3 | io::{self, Stdin}, | ||
| 4 | }; | ||
| 5 | |||
| 6 | use anyhow::{bail, Context, Result}; | ||
| 7 | use git2::Repository; | ||
| 8 | use ngit::{ | ||
| 9 | client::{ | ||
| 10 | get_all_proposal_patch_events_from_cache, get_events_from_cache, | ||
| 11 | get_proposals_and_revisions_from_cache, | ||
| 12 | }, | ||
| 13 | git::{Repo, RepoActions}, | ||
| 14 | git_events::{ | ||
| 15 | event_is_revision_root, event_to_cover_letter, get_most_recent_patch_with_ancestors, | ||
| 16 | status_kinds, | ||
| 17 | }, | ||
| 18 | repo_ref::RepoRef, | ||
| 19 | }; | ||
| 20 | use nostr_sdk::{Event, EventId, Kind, PublicKey, Url}; | ||
| 21 | |||
| 22 | pub fn get_short_git_server_name(git_repo: &Repo, url: &str) -> std::string::String { | ||
| 23 | if let Ok(name) = get_remote_name_by_url(&git_repo.git_repo, url) { | ||
| 24 | return name; | ||
| 25 | } | ||
| 26 | if let Ok(url) = Url::parse(url) { | ||
| 27 | if let Some(domain) = url.domain() { | ||
| 28 | return domain.to_string(); | ||
| 29 | } | ||
| 30 | } | ||
| 31 | url.to_string() | ||
| 32 | } | ||
| 33 | |||
| 34 | pub fn get_remote_name_by_url(git_repo: &Repository, url: &str) -> Result<String> { | ||
| 35 | let remotes = git_repo.remotes()?; | ||
| 36 | Ok(remotes | ||
| 37 | .iter() | ||
| 38 | .find(|r| { | ||
| 39 | if let Some(name) = r { | ||
| 40 | if let Some(remote_url) = git_repo.find_remote(name).unwrap().url() { | ||
| 41 | url == remote_url | ||
| 42 | } else { | ||
| 43 | false | ||
| 44 | } | ||
| 45 | } else { | ||
| 46 | false | ||
| 47 | } | ||
| 48 | }) | ||
| 49 | .context("could not find remote with matching url")? | ||
| 50 | .context("remote with matching url must be named")? | ||
| 51 | .to_string()) | ||
| 52 | } | ||
| 53 | |||
| 54 | pub fn get_oids_from_fetch_batch( | ||
| 55 | stdin: &Stdin, | ||
| 56 | initial_oid: &str, | ||
| 57 | initial_refstr: &str, | ||
| 58 | ) -> Result<HashMap<String, String>> { | ||
| 59 | let mut line = String::new(); | ||
| 60 | let mut batch = HashMap::new(); | ||
| 61 | batch.insert(initial_refstr.to_string(), initial_oid.to_string()); | ||
| 62 | loop { | ||
| 63 | let tokens = read_line(stdin, &mut line)?; | ||
| 64 | match tokens.as_slice() { | ||
| 65 | ["fetch", oid, refstr] => { | ||
| 66 | batch.insert((*refstr).to_string(), (*oid).to_string()); | ||
| 67 | } | ||
| 68 | [] => break, | ||
| 69 | _ => bail!( | ||
| 70 | "after a `fetch` command we are only expecting another fetch or an empty line" | ||
| 71 | ), | ||
| 72 | } | ||
| 73 | } | ||
| 74 | Ok(batch) | ||
| 75 | } | ||
| 76 | |||
| 77 | /// Read one line from stdin, and split it into tokens. | ||
| 78 | pub fn read_line<'a>(stdin: &io::Stdin, line: &'a mut String) -> io::Result<Vec<&'a str>> { | ||
| 79 | line.clear(); | ||
| 80 | |||
| 81 | let read = stdin.read_line(line)?; | ||
| 82 | if read == 0 { | ||
| 83 | return Ok(vec![]); | ||
| 84 | } | ||
| 85 | let line = line.trim(); | ||
| 86 | let tokens = line.split(' ').filter(|t| !t.is_empty()).collect(); | ||
| 87 | |||
| 88 | Ok(tokens) | ||
| 89 | } | ||
| 90 | |||
| 91 | pub fn switch_clone_url_between_ssh_and_https(url: &str) -> Result<String> { | ||
| 92 | if url.starts_with("https://") { | ||
| 93 | // Convert HTTPS to git@ syntax | ||
| 94 | let parts: Vec<&str> = url.trim_start_matches("https://").split('/').collect(); | ||
| 95 | if parts.len() >= 2 { | ||
| 96 | // Construct the git@ URL | ||
| 97 | Ok(format!("git@{}:{}", parts[0], parts[1..].join("/"))) | ||
| 98 | } else { | ||
| 99 | // If the format is unexpected, return an error | ||
| 100 | bail!("Invalid HTTPS URL format: {}", url); | ||
| 101 | } | ||
| 102 | } else if url.starts_with("ssh://") { | ||
| 103 | // Convert SSH to git@ syntax | ||
| 104 | let parts: Vec<&str> = url.trim_start_matches("ssh://").split('/').collect(); | ||
| 105 | if parts.len() >= 2 { | ||
| 106 | // Construct the git@ URL | ||
| 107 | Ok(format!("git@{}:{}", parts[0], parts[1..].join("/"))) | ||
| 108 | } else { | ||
| 109 | // If the format is unexpected, return an error | ||
| 110 | bail!("Invalid SSH URL format: {}", url); | ||
| 111 | } | ||
| 112 | } else if url.starts_with("git@") { | ||
| 113 | // Convert git@ syntax to HTTPS | ||
| 114 | let parts: Vec<&str> = url.split(':').collect(); | ||
| 115 | if parts.len() == 2 { | ||
| 116 | // Construct the HTTPS URL | ||
| 117 | Ok(format!( | ||
| 118 | "https://{}/{}", | ||
| 119 | parts[0].trim_end_matches('@'), | ||
| 120 | parts[1] | ||
| 121 | )) | ||
| 122 | } else { | ||
| 123 | // If the format is unexpected, return an error | ||
| 124 | bail!("Invalid git@ URL format: {}", url); | ||
| 125 | } | ||
| 126 | } else { | ||
| 127 | // If the URL is neither HTTPS, SSH, nor git@, return an error | ||
| 128 | bail!("Unsupported URL protocol: {}", url); | ||
| 129 | } | ||
| 130 | } | ||
| 131 | |||
| 132 | pub async fn get_open_proposals( | ||
| 133 | git_repo: &Repo, | ||
| 134 | repo_ref: &RepoRef, | ||
| 135 | ) -> Result<HashMap<EventId, (Event, Vec<Event>)>> { | ||
| 136 | let git_repo_path = git_repo.get_path()?; | ||
| 137 | let proposals: Vec<nostr::Event> = | ||
| 138 | get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) | ||
| 139 | .await? | ||
| 140 | .iter() | ||
| 141 | .filter(|e| !event_is_revision_root(e)) | ||
| 142 | .cloned() | ||
| 143 | .collect(); | ||
| 144 | |||
| 145 | let statuses: Vec<nostr::Event> = { | ||
| 146 | let mut statuses = get_events_from_cache( | ||
| 147 | git_repo_path, | ||
| 148 | vec![ | ||
| 149 | nostr::Filter::default() | ||
| 150 | .kinds(status_kinds().clone()) | ||
| 151 | .events(proposals.iter().map(nostr::Event::id)), | ||
| 152 | ], | ||
| 153 | ) | ||
| 154 | .await?; | ||
| 155 | statuses.sort_by_key(|e| e.created_at); | ||
| 156 | statuses.reverse(); | ||
| 157 | statuses | ||
| 158 | }; | ||
| 159 | let mut open_proposals = HashMap::new(); | ||
| 160 | |||
| 161 | for proposal in proposals { | ||
| 162 | let status = if let Some(e) = statuses | ||
| 163 | .iter() | ||
| 164 | .filter(|e| { | ||
| 165 | status_kinds().contains(&e.kind()) | ||
| 166 | && e.tags() | ||
| 167 | .iter() | ||
| 168 | .any(|t| t.as_vec()[1].eq(&proposal.id.to_string())) | ||
| 169 | }) | ||
| 170 | .collect::<Vec<&nostr::Event>>() | ||
| 171 | .first() | ||
| 172 | { | ||
| 173 | e.kind() | ||
| 174 | } else { | ||
| 175 | Kind::GitStatusOpen | ||
| 176 | }; | ||
| 177 | if status.eq(&Kind::GitStatusOpen) { | ||
| 178 | if let Ok(commits_events) = | ||
| 179 | get_all_proposal_patch_events_from_cache(git_repo_path, repo_ref, &proposal.id) | ||
| 180 | .await | ||
| 181 | { | ||
| 182 | if let Ok(most_recent_proposal_patch_chain) = | ||
| 183 | get_most_recent_patch_with_ancestors(commits_events.clone()) | ||
| 184 | { | ||
| 185 | open_proposals | ||
| 186 | .insert(proposal.id(), (proposal, most_recent_proposal_patch_chain)); | ||
| 187 | } | ||
| 188 | } | ||
| 189 | } | ||
| 190 | } | ||
| 191 | Ok(open_proposals) | ||
| 192 | } | ||
| 193 | |||
| 194 | pub async fn get_all_proposals( | ||
| 195 | git_repo: &Repo, | ||
| 196 | repo_ref: &RepoRef, | ||
| 197 | ) -> Result<HashMap<EventId, (Event, Vec<Event>)>> { | ||
| 198 | let git_repo_path = git_repo.get_path()?; | ||
| 199 | let proposals: Vec<nostr::Event> = | ||
| 200 | get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) | ||
| 201 | .await? | ||
| 202 | .iter() | ||
| 203 | .filter(|e| !event_is_revision_root(e)) | ||
| 204 | .cloned() | ||
| 205 | .collect(); | ||
| 206 | |||
| 207 | let mut all_proposals = HashMap::new(); | ||
| 208 | |||
| 209 | for proposal in proposals { | ||
| 210 | if let Ok(commits_events) = | ||
| 211 | get_all_proposal_patch_events_from_cache(git_repo_path, repo_ref, &proposal.id).await | ||
| 212 | { | ||
| 213 | if let Ok(most_recent_proposal_patch_chain) = | ||
| 214 | get_most_recent_patch_with_ancestors(commits_events.clone()) | ||
| 215 | { | ||
| 216 | all_proposals.insert(proposal.id(), (proposal, most_recent_proposal_patch_chain)); | ||
| 217 | } | ||
| 218 | } | ||
| 219 | } | ||
| 220 | Ok(all_proposals) | ||
| 221 | } | ||
| 222 | |||
| 223 | pub fn find_proposal_and_patches_by_branch_name<'a>( | ||
| 224 | refstr: &'a str, | ||
| 225 | open_proposals: &'a HashMap<EventId, (Event, Vec<Event>)>, | ||
| 226 | current_user: &Option<PublicKey>, | ||
| 227 | ) -> Option<(&'a EventId, &'a (Event, Vec<Event>))> { | ||
| 228 | open_proposals.iter().find(|(_, (proposal, _))| { | ||
| 229 | if let Ok(cl) = event_to_cover_letter(proposal) { | ||
| 230 | if let Ok(mut branch_name) = cl.get_branch_name() { | ||
| 231 | branch_name = if let Some(public_key) = current_user { | ||
| 232 | if proposal.author().eq(public_key) { | ||
| 233 | cl.branch_name.to_string() | ||
| 234 | } else { | ||
| 235 | branch_name | ||
| 236 | } | ||
| 237 | } else { | ||
| 238 | branch_name | ||
| 239 | }; | ||
| 240 | branch_name.eq(&refstr.replace("refs/heads/", "")) | ||
| 241 | } else { | ||
| 242 | false | ||
| 243 | } | ||
| 244 | } else { | ||
| 245 | false | ||
| 246 | } | ||
| 247 | }) | ||
| 248 | } | ||