From c4c262a5e9bfeb30bc0106d9ea51dfce7e4fa1f3 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 4 Sep 2024 16:44:43 +0100 Subject: refactor(remote): split into modules to make it easier to read --- src/bin/git_remote_nostr/utils.rs | 248 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 src/bin/git_remote_nostr/utils.rs (limited to 'src/bin/git_remote_nostr/utils.rs') 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 @@ +use std::{ + collections::HashMap, + io::{self, Stdin}, +}; + +use anyhow::{bail, Context, Result}; +use git2::Repository; +use ngit::{ + client::{ + get_all_proposal_patch_events_from_cache, get_events_from_cache, + get_proposals_and_revisions_from_cache, + }, + git::{Repo, RepoActions}, + git_events::{ + event_is_revision_root, event_to_cover_letter, get_most_recent_patch_with_ancestors, + status_kinds, + }, + repo_ref::RepoRef, +}; +use nostr_sdk::{Event, EventId, Kind, PublicKey, Url}; + +pub fn get_short_git_server_name(git_repo: &Repo, url: &str) -> std::string::String { + if let Ok(name) = get_remote_name_by_url(&git_repo.git_repo, url) { + return name; + } + if let Ok(url) = Url::parse(url) { + if let Some(domain) = url.domain() { + return domain.to_string(); + } + } + url.to_string() +} + +pub fn get_remote_name_by_url(git_repo: &Repository, url: &str) -> Result { + let remotes = git_repo.remotes()?; + Ok(remotes + .iter() + .find(|r| { + if let Some(name) = r { + if let Some(remote_url) = git_repo.find_remote(name).unwrap().url() { + url == remote_url + } else { + false + } + } else { + false + } + }) + .context("could not find remote with matching url")? + .context("remote with matching url must be named")? + .to_string()) +} + +pub fn get_oids_from_fetch_batch( + stdin: &Stdin, + initial_oid: &str, + initial_refstr: &str, +) -> Result> { + let mut line = String::new(); + let mut batch = HashMap::new(); + batch.insert(initial_refstr.to_string(), initial_oid.to_string()); + loop { + let tokens = read_line(stdin, &mut line)?; + match tokens.as_slice() { + ["fetch", oid, refstr] => { + batch.insert((*refstr).to_string(), (*oid).to_string()); + } + [] => break, + _ => bail!( + "after a `fetch` command we are only expecting another fetch or an empty line" + ), + } + } + Ok(batch) +} + +/// Read one line from stdin, and split it into tokens. +pub fn read_line<'a>(stdin: &io::Stdin, line: &'a mut String) -> io::Result> { + line.clear(); + + let read = stdin.read_line(line)?; + if read == 0 { + return Ok(vec![]); + } + let line = line.trim(); + let tokens = line.split(' ').filter(|t| !t.is_empty()).collect(); + + Ok(tokens) +} + +pub fn switch_clone_url_between_ssh_and_https(url: &str) -> Result { + if url.starts_with("https://") { + // Convert HTTPS to git@ syntax + let parts: Vec<&str> = url.trim_start_matches("https://").split('/').collect(); + if parts.len() >= 2 { + // Construct the git@ URL + Ok(format!("git@{}:{}", parts[0], parts[1..].join("/"))) + } else { + // If the format is unexpected, return an error + bail!("Invalid HTTPS URL format: {}", url); + } + } else if url.starts_with("ssh://") { + // Convert SSH to git@ syntax + let parts: Vec<&str> = url.trim_start_matches("ssh://").split('/').collect(); + if parts.len() >= 2 { + // Construct the git@ URL + Ok(format!("git@{}:{}", parts[0], parts[1..].join("/"))) + } else { + // If the format is unexpected, return an error + bail!("Invalid SSH URL format: {}", url); + } + } else if url.starts_with("git@") { + // Convert git@ syntax to HTTPS + let parts: Vec<&str> = url.split(':').collect(); + if parts.len() == 2 { + // Construct the HTTPS URL + Ok(format!( + "https://{}/{}", + parts[0].trim_end_matches('@'), + parts[1] + )) + } else { + // If the format is unexpected, return an error + bail!("Invalid git@ URL format: {}", url); + } + } else { + // If the URL is neither HTTPS, SSH, nor git@, return an error + bail!("Unsupported URL protocol: {}", url); + } +} + +pub async fn get_open_proposals( + git_repo: &Repo, + repo_ref: &RepoRef, +) -> Result)>> { + let git_repo_path = git_repo.get_path()?; + let proposals: Vec = + get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) + .await? + .iter() + .filter(|e| !event_is_revision_root(e)) + .cloned() + .collect(); + + let statuses: Vec = { + let mut statuses = get_events_from_cache( + git_repo_path, + vec![ + nostr::Filter::default() + .kinds(status_kinds().clone()) + .events(proposals.iter().map(nostr::Event::id)), + ], + ) + .await?; + statuses.sort_by_key(|e| e.created_at); + statuses.reverse(); + statuses + }; + let mut open_proposals = HashMap::new(); + + for proposal in proposals { + let status = if let Some(e) = statuses + .iter() + .filter(|e| { + status_kinds().contains(&e.kind()) + && e.tags() + .iter() + .any(|t| t.as_vec()[1].eq(&proposal.id.to_string())) + }) + .collect::>() + .first() + { + e.kind() + } else { + Kind::GitStatusOpen + }; + if status.eq(&Kind::GitStatusOpen) { + if let Ok(commits_events) = + get_all_proposal_patch_events_from_cache(git_repo_path, repo_ref, &proposal.id) + .await + { + if let Ok(most_recent_proposal_patch_chain) = + get_most_recent_patch_with_ancestors(commits_events.clone()) + { + open_proposals + .insert(proposal.id(), (proposal, most_recent_proposal_patch_chain)); + } + } + } + } + Ok(open_proposals) +} + +pub async fn get_all_proposals( + git_repo: &Repo, + repo_ref: &RepoRef, +) -> Result)>> { + let git_repo_path = git_repo.get_path()?; + let proposals: Vec = + get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) + .await? + .iter() + .filter(|e| !event_is_revision_root(e)) + .cloned() + .collect(); + + let mut all_proposals = HashMap::new(); + + for proposal in proposals { + if let Ok(commits_events) = + get_all_proposal_patch_events_from_cache(git_repo_path, repo_ref, &proposal.id).await + { + if let Ok(most_recent_proposal_patch_chain) = + get_most_recent_patch_with_ancestors(commits_events.clone()) + { + all_proposals.insert(proposal.id(), (proposal, most_recent_proposal_patch_chain)); + } + } + } + Ok(all_proposals) +} + +pub fn find_proposal_and_patches_by_branch_name<'a>( + refstr: &'a str, + open_proposals: &'a HashMap)>, + current_user: &Option, +) -> Option<(&'a EventId, &'a (Event, Vec))> { + open_proposals.iter().find(|(_, (proposal, _))| { + if let Ok(cl) = event_to_cover_letter(proposal) { + if let Ok(mut branch_name) = cl.get_branch_name() { + branch_name = if let Some(public_key) = current_user { + if proposal.author().eq(public_key) { + cl.branch_name.to_string() + } else { + branch_name + } + } else { + branch_name + }; + branch_name.eq(&refstr.replace("refs/heads/", "")) + } else { + false + } + } else { + false + } + }) +} -- cgit v1.2.3