upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/pr_status.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/ngit/sub_commands/pr_status.rs')
-rw-r--r--src/bin/ngit/sub_commands/pr_status.rs199
1 files changed, 199 insertions, 0 deletions
diff --git a/src/bin/ngit/sub_commands/pr_status.rs b/src/bin/ngit/sub_commands/pr_status.rs
new file mode 100644
index 0000000..e84117d
--- /dev/null
+++ b/src/bin/ngit/sub_commands/pr_status.rs
@@ -0,0 +1,199 @@
1use anyhow::{Context, Result, bail};
2use ngit::{
3 client::{Params, get_proposals_and_revisions_from_cache, send_events, sign_event},
4 git_events::{get_status, status_kinds},
5};
6use nostr::{EventBuilder, Tag, TagStandard, ToBech32, nips::nip19::Nip19};
7use nostr_sdk::{EventId, FromBech32, Kind, nips::nip10::Marker};
8
9use crate::{
10 client::{
11 Client, Connect, fetching_with_report, get_events_from_local_cache, get_repo_ref_from_cache,
12 },
13 git::{Repo, RepoActions},
14 login,
15 repo_ref::get_repo_coordinates_when_remote_unknown,
16};
17
18fn parse_event_id(id: &str) -> Result<EventId> {
19 if let Ok(nip19) = Nip19::from_bech32(id) {
20 match nip19 {
21 nostr::nips::nip19::Nip19::Event(e) => return Ok(e.event_id),
22 nostr::nips::nip19::Nip19::EventId(event_id) => return Ok(event_id),
23 _ => {}
24 }
25 }
26 if let Ok(event_id) = EventId::from_hex(id) {
27 return Ok(event_id);
28 }
29 bail!("invalid event-id or nevent: {id}")
30}
31
32#[allow(clippy::too_many_lines)]
33async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) -> Result<()> {
34 let event_id = parse_event_id(id)?;
35
36 let git_repo = Repo::discover().context("failed to find a git repository")?;
37 let git_repo_path = git_repo.get_path()?;
38
39 let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));
40 let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?;
41
42 if !offline {
43 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
44 }
45
46 let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
47
48 let proposals_and_revisions =
49 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()).await?;
50
51 let proposal = proposals_and_revisions
52 .iter()
53 .find(|e| e.id == event_id)
54 .context(format!(
55 "PR with id {} not found in cache",
56 event_id.to_hex()
57 ))?
58 .clone();
59
60 // Login to get signer and user pubkey
61 let (signer, user_ref, _) =
62 login::login_or_signup(&Some(&git_repo), &None, &None, Some(&client), true).await?;
63
64 let user_pubkey = signer.get_public_key().await?;
65
66 // Only author or maintainer may change status
67 if proposal.pubkey != user_pubkey && !repo_ref.maintainers.contains(&user_pubkey) {
68 bail!("only the PR author or a repository maintainer can {action} a PR");
69 }
70
71 // Fetch existing statuses to check current state
72 let statuses = {
73 let mut s = get_events_from_local_cache(
74 git_repo_path,
75 vec![
76 nostr::Filter::default()
77 .kinds(status_kinds().clone())
78 .events(proposals_and_revisions.iter().map(|e| e.id)),
79 nostr::Filter::default()
80 .custom_tags(
81 nostr::filter::SingleLetterTag::uppercase(nostr::filter::Alphabet::E),
82 proposals_and_revisions.iter().map(|e| e.id),
83 )
84 .kinds(status_kinds().clone()),
85 ],
86 )
87 .await?;
88 s.sort_by_key(|e| e.created_at);
89 s.reverse();
90 s
91 };
92
93 let proposals_vec: Vec<nostr::Event> = proposals_and_revisions
94 .iter()
95 .filter(|e| !ngit::git_events::event_is_revision_root(e))
96 .cloned()
97 .collect();
98
99 let current_status = get_status(&proposal, &repo_ref, &statuses, &proposals_vec);
100
101 // Guard against no-op transitions
102 if current_status == new_kind {
103 let status_str = match new_kind {
104 Kind::GitStatusOpen => "open",
105 Kind::GitStatusClosed => "closed",
106 Kind::GitStatusDraft => "draft",
107 Kind::GitStatusApplied => "applied",
108 _ => "unknown",
109 };
110 println!("PR is already {status_str}");
111 return Ok(());
112 }
113
114 let alt_text = match new_kind {
115 Kind::GitStatusOpen => "PR reopened",
116 Kind::GitStatusClosed => "PR closed",
117 Kind::GitStatusDraft => "PR marked as draft",
118 Kind::GitStatusApplied => "PR applied/merged",
119 _ => "PR status updated",
120 };
121
122 // Build status event following the same pattern as push.rs
123 let mut public_keys: std::collections::HashSet<nostr::PublicKey> =
124 repo_ref.maintainers.iter().copied().collect();
125 public_keys.insert(proposal.pubkey);
126
127 let status_event = sign_event(
128 EventBuilder::new(new_kind, "").tags(
129 [
130 vec![
131 Tag::custom(
132 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
133 vec![alt_text.to_string()],
134 ),
135 Tag::from_standardized(TagStandard::Event {
136 event_id: proposal.id,
137 relay_url: repo_ref.relays.first().cloned(),
138 marker: Some(Marker::Root),
139 public_key: None,
140 uppercase: false,
141 }),
142 ],
143 public_keys.iter().map(|pk| Tag::public_key(*pk)).collect(),
144 repo_ref
145 .coordinates()
146 .iter()
147 .map(|c| {
148 Tag::from_standardized(TagStandard::Coordinate {
149 coordinate: c.coordinate.clone(),
150 relay_url: c.relays.first().cloned(),
151 uppercase: false,
152 })
153 })
154 .collect::<Vec<Tag>>(),
155 vec![Tag::from_standardized(nostr::TagStandard::Reference(
156 repo_ref.root_commit.to_string(),
157 ))],
158 ]
159 .concat(),
160 ),
161 &signer,
162 format!("{action} PR"),
163 )
164 .await?;
165
166 let mut client = client;
167 client.set_signer(signer).await;
168
169 send_events(
170 &client,
171 Some(git_repo_path),
172 vec![status_event],
173 user_ref.relays.write(),
174 repo_ref.relays.clone(),
175 true,
176 false,
177 )
178 .await?;
179
180 println!(
181 "PR {} {}d: {}",
182 &event_id.to_hex()[..8],
183 action,
184 proposal.pubkey.to_bech32().unwrap_or_default()
185 );
186 Ok(())
187}
188
189pub async fn launch_close(id: &str, offline: bool) -> Result<()> {
190 launch_status(id, offline, Kind::GitStatusClosed, "close").await
191}
192
193pub async fn launch_reopen(id: &str, offline: bool) -> Result<()> {
194 launch_status(id, offline, Kind::GitStatusOpen, "reopen").await
195}
196
197pub async fn launch_ready(id: &str, offline: bool) -> Result<()> {
198 launch_status(id, offline, Kind::GitStatusOpen, "mark as ready").await
199}