upleb.uk

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

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