upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/git_remote_nostr/utils.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/git_remote_nostr/utils.rs')
-rw-r--r--src/bin/git_remote_nostr/utils.rs248
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 @@
1use std::{
2 collections::HashMap,
3 io::{self, Stdin},
4};
5
6use anyhow::{bail, Context, Result};
7use git2::Repository;
8use 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};
20use nostr_sdk::{Event, EventId, Kind, PublicKey, Url};
21
22pub 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
34pub 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
54pub 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.
78pub 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
91pub 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
132pub 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
194pub 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
223pub 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}