upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/sub_commands/prs/list.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/sub_commands/prs/list.rs')
-rw-r--r--src/sub_commands/prs/list.rs267
1 files changed, 0 insertions, 267 deletions
diff --git a/src/sub_commands/prs/list.rs b/src/sub_commands/prs/list.rs
deleted file mode 100644
index d4dcfec..0000000
--- a/src/sub_commands/prs/list.rs
+++ /dev/null
@@ -1,267 +0,0 @@
1use anyhow::{bail, Context, Result};
2
3use super::create::event_is_patch_set_root;
4#[cfg(not(test))]
5use crate::client::Client;
6#[cfg(test)]
7use crate::client::MockConnect;
8use crate::{
9 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms},
10 client::Connect,
11 git::{Repo, RepoActions},
12 repo_ref::{self, RepoRef, REPO_REF_KIND},
13 sub_commands::prs::create::{event_is_cover_letter, event_to_cover_letter, PATCH_KIND},
14 Cli,
15};
16
17#[derive(Debug, clap::Args)]
18pub struct SubCommandArgs {
19 /// TODO ignore merged, and closed
20 #[arg(long, action)]
21 open_only: bool,
22}
23
24#[allow(clippy::too_many_lines)]
25pub async fn launch(
26 _cli_args: &Cli,
27 _pr_args: &super::SubCommandArgs,
28 _args: &SubCommandArgs,
29) -> Result<()> {
30 let git_repo = Repo::discover().context("cannot find a git repository")?;
31
32 let root_commit = git_repo
33 .get_root_commit()
34 .context("failed to get root commit of the repository")?;
35
36 // TODO: check for empty repo
37 // TODO: check for existing maintaiers file
38 // TODO: check for other claims
39
40 #[cfg(not(test))]
41 let client = Client::default();
42 #[cfg(test)]
43 let client = <MockConnect as std::default::Default>::default();
44
45 let repo_ref = repo_ref::fetch(
46 &git_repo,
47 root_commit.to_string(),
48 &client,
49 client.get_fallback_relays().clone(),
50 )
51 .await?;
52
53 println!("finding PRs...");
54
55 let pr_events: Vec<nostr::Event> =
56 find_pr_events(&client, &repo_ref, &root_commit.to_string()).await?;
57
58 let selected_index = Interactor::default().choice(
59 PromptChoiceParms::default()
60 .with_prompt("All PRs")
61 .with_choices(
62 pr_events
63 .iter()
64 .map(|e| {
65 if let Ok(cl) = event_to_cover_letter(e) {
66 cl.title
67 } else if let Ok(msg) = tag_value(e, "description") {
68 msg.split('\n').collect::<Vec<&str>>()[0].to_string()
69 } else {
70 e.id.to_string()
71 }
72 })
73 .collect(),
74 ),
75 )?;
76
77 println!("finding commits...");
78
79 let commits_events: Vec<nostr::Event> =
80 find_commits_for_pr_event(&client, &pr_events[selected_index], &repo_ref).await?;
81
82 confirm_checkout(&git_repo)?;
83
84 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events)
85 .context("cannot get most recent patch for PR")?;
86
87 let branch_name: String = event_to_cover_letter(&pr_events[selected_index])
88 .context("cannot assign a branch name as event is not a patch set root")?
89 .branch_name;
90
91 let applied = git_repo
92 .apply_patch_chain(&branch_name, most_recent_pr_patch_chain)
93 .context("cannot apply patch chain")?;
94
95 if applied.is_empty() {
96 println!("checked out PR branch. no new commits to pull");
97 } else {
98 println!(
99 "checked out PR branch. pulled {} new commits",
100 applied.len(),
101 );
102 }
103 Ok(())
104}
105
106fn confirm_checkout(git_repo: &Repo) -> Result<()> {
107 if !Interactor::default().confirm(
108 PromptConfirmParms::default()
109 .with_prompt("check out branch?")
110 .with_default(true),
111 )? {
112 bail!("Exiting...");
113 }
114
115 if git_repo.has_outstanding_changes()? {
116 bail!(
117 "cannot pull PR branch when repository is not clean. discard or stash (un)staged changes and try again."
118 );
119 }
120 Ok(())
121}
122
123pub fn tag_value(event: &nostr::Event, tag_name: &str) -> Result<String> {
124 Ok(event
125 .tags
126 .iter()
127 .find(|t| t.as_vec()[0].eq(tag_name))
128 .context(format!("tag '{tag_name}'not present"))?
129 .as_vec()[1]
130 .clone())
131}
132
133pub fn get_most_recent_patch_with_ancestors(
134 mut patches: Vec<nostr::Event>,
135) -> Result<Vec<nostr::Event>> {
136 patches.sort_by_key(|e| e.created_at);
137
138 let first_patch = patches.first().context("no patches found")?;
139
140 let patches_with_youngest_created_at: Vec<&nostr::Event> = patches
141 .iter()
142 .filter(|p| p.created_at.eq(&first_patch.created_at))
143 .collect();
144
145 let latest_commit_id = tag_value(
146 // get the first patch which isn't a parent of a patch event created at the same
147 // time
148 patches_with_youngest_created_at
149 .clone()
150 .iter()
151 .find(|p| {
152 if let Ok(commit) = tag_value(p, "commit") {
153 !patches_with_youngest_created_at.iter().any(|p2| {
154 if let Ok(parent) = tag_value(p2, "parent-commit") {
155 commit.eq(&parent)
156 } else {
157 false // skip
158 }
159 })
160 } else {
161 false // skip
162 }
163 })
164 .context("cannot find patches_with_youngest_created_at")?,
165 "commit",
166 )?;
167
168 let mut res = vec![];
169
170 let mut commit_id_to_search = latest_commit_id;
171
172 while let Some(event) = patches.iter().find(|e| {
173 if let Ok(commit) = tag_value(e, "commit") {
174 commit.eq(&commit_id_to_search)
175 } else {
176 false // skip
177 }
178 }) {
179 res.push(event.clone());
180 commit_id_to_search = tag_value(event, "parent-commit")?;
181 }
182 Ok(res)
183}
184
185pub async fn find_pr_events(
186 #[cfg(test)] client: &crate::client::MockConnect,
187 #[cfg(not(test))] client: &Client,
188 repo_ref: &RepoRef,
189 root_commit: &str,
190) -> Result<Vec<nostr::Event>> {
191 Ok(client
192 .get_events(
193 repo_ref.relays.clone(),
194 vec![
195 nostr::Filter::default()
196 .kind(nostr::Kind::Custom(PATCH_KIND))
197 .custom_tag(nostr::Alphabet::T, vec!["root"])
198 .identifiers(
199 repo_ref
200 .maintainers
201 .iter()
202 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)),
203 ),
204 // also pick up prs from the same repo but no target at our maintainers repo events
205 nostr::Filter::default()
206 .kind(nostr::Kind::Custom(PATCH_KIND))
207 .custom_tag(nostr::Alphabet::T, vec!["root"])
208 .reference(root_commit),
209 ],
210 )
211 .await
212 .context("cannot get pr events")?
213 .iter()
214 .filter(|e| {
215 event_is_patch_set_root(e)
216 && (e
217 .tags
218 .iter()
219 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(root_commit))
220 || e.tags.iter().any(|t| {
221 t.as_vec().len() > 1
222 && repo_ref
223 .maintainers
224 .iter()
225 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier))
226 .any(|d| t.as_vec()[1].eq(&d))
227 }))
228 })
229 .map(std::borrow::ToOwned::to_owned)
230 .collect::<Vec<nostr::Event>>())
231}
232
233pub async fn find_commits_for_pr_event(
234 #[cfg(test)] client: &crate::client::MockConnect,
235 #[cfg(not(test))] client: &Client,
236 pr_event: &nostr::Event,
237 repo_ref: &RepoRef,
238) -> Result<Vec<nostr::Event>> {
239 let mut patch_events: Vec<nostr::Event> = client
240 .get_events(
241 repo_ref.relays.clone(),
242 vec![
243 nostr::Filter::default()
244 .kind(nostr::Kind::Custom(PATCH_KIND))
245 // this requires every patch to reference the root event
246 // this will not pick up v2,v3 patch sets
247 // TODO: fetch commits for v2.. patch sets
248 .event(pr_event.id),
249 ],
250 )
251 .await
252 .context("cannot fetch patch events")?
253 .iter()
254 .filter(|e| {
255 e.kind.as_u64() == PATCH_KIND
256 && e.tags
257 .iter()
258 .any(|t| t.as_vec().len() > 2 && t.as_vec()[1].eq(&pr_event.id.to_string()))
259 })
260 .map(std::borrow::ToOwned::to_owned)
261 .collect();
262
263 if !event_is_cover_letter(pr_event) {
264 patch_events.push(pr_event.clone());
265 }
266 Ok(patch_events)
267}