upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/issue_status.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/ngit/sub_commands/issue_status.rs')
-rw-r--r--src/bin/ngit/sub_commands/issue_status.rs178
1 files changed, 178 insertions, 0 deletions
diff --git a/src/bin/ngit/sub_commands/issue_status.rs b/src/bin/ngit/sub_commands/issue_status.rs
new file mode 100644
index 0000000..3facee3
--- /dev/null
+++ b/src/bin/ngit/sub_commands/issue_status.rs
@@ -0,0 +1,178 @@
1use anyhow::{Context, Result, bail};
2use ngit::{
3 client::{Params, get_issues_from_cache, send_events, sign_event},
4 git_events::{get_status, status_kinds},
5};
6use nostr::{EventBuilder, Tag, TagStandard, 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 issues = get_issues_from_cache(git_repo_path, repo_ref.coordinates()).await?;
49
50 let issue = issues
51 .iter()
52 .find(|e| e.id == event_id)
53 .context(format!(
54 "issue with id {} not found in cache",
55 event_id.to_hex()
56 ))?
57 .clone();
58
59 // Login to get signer and user pubkey
60 let (signer, user_ref, _) =
61 login::login_or_signup(&Some(&git_repo), &None, &None, Some(&client), true).await?;
62
63 let user_pubkey = signer.get_public_key().await?;
64
65 // Only author or maintainer may change status
66 if issue.pubkey != user_pubkey && !repo_ref.maintainers.contains(&user_pubkey) {
67 bail!("only the issue author or a repository maintainer can {action} an issue");
68 }
69
70 // Fetch existing statuses to check current state
71 let statuses = {
72 let mut s = get_events_from_local_cache(
73 git_repo_path,
74 vec![
75 nostr::Filter::default()
76 .kinds(status_kinds().clone())
77 .events(issues.iter().map(|e| e.id)),
78 nostr::Filter::default()
79 .custom_tags(
80 nostr::filter::SingleLetterTag::uppercase(nostr::filter::Alphabet::E),
81 issues.iter().map(|e| e.id),
82 )
83 .kinds(status_kinds().clone()),
84 ],
85 )
86 .await?;
87 s.sort_by_key(|e| e.created_at);
88 s.reverse();
89 s
90 };
91
92 let empty_proposals: Vec<nostr::Event> = vec![];
93 let current_status = get_status(&issue, &repo_ref, &statuses, &empty_proposals);
94
95 if current_status == new_kind {
96 let status_str = match new_kind {
97 Kind::GitStatusOpen => "open",
98 Kind::GitStatusClosed => "closed",
99 _ => "unknown",
100 };
101 println!("issue is already {status_str}");
102 return Ok(());
103 }
104
105 let alt_text = match new_kind {
106 Kind::GitStatusOpen => "issue reopened",
107 Kind::GitStatusClosed => "issue closed",
108 _ => "issue status updated",
109 };
110
111 let mut public_keys: std::collections::HashSet<nostr::PublicKey> =
112 repo_ref.maintainers.iter().copied().collect();
113 public_keys.insert(issue.pubkey);
114
115 let status_event = sign_event(
116 EventBuilder::new(new_kind, "").tags(
117 [
118 vec![
119 Tag::custom(
120 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
121 vec![alt_text.to_string()],
122 ),
123 Tag::from_standardized(TagStandard::Event {
124 event_id: issue.id,
125 relay_url: repo_ref.relays.first().cloned(),
126 marker: Some(Marker::Root),
127 public_key: None,
128 uppercase: false,
129 }),
130 ],
131 public_keys.iter().map(|pk| Tag::public_key(*pk)).collect(),
132 repo_ref
133 .coordinates()
134 .iter()
135 .map(|c| {
136 Tag::from_standardized(TagStandard::Coordinate {
137 coordinate: c.coordinate.clone(),
138 relay_url: c.relays.first().cloned(),
139 uppercase: false,
140 })
141 })
142 .collect::<Vec<Tag>>(),
143 vec![Tag::from_standardized(nostr::TagStandard::Reference(
144 repo_ref.root_commit.to_string(),
145 ))],
146 ]
147 .concat(),
148 ),
149 &signer,
150 format!("{action} issue"),
151 )
152 .await?;
153
154 let mut client = client;
155 client.set_signer(signer).await;
156
157 send_events(
158 &client,
159 Some(git_repo_path),
160 vec![status_event],
161 user_ref.relays.write(),
162 repo_ref.relays.clone(),
163 true,
164 false,
165 )
166 .await?;
167
168 println!("issue {} {}d", &event_id.to_hex()[..8], action,);
169 Ok(())
170}
171
172pub async fn launch_close(id: &str, offline: bool) -> Result<()> {
173 launch_status(id, offline, Kind::GitStatusClosed, "close").await
174}
175
176pub async fn launch_reopen(id: &str, offline: bool) -> Result<()> {
177 launch_status(id, offline, Kind::GitStatusOpen, "reopen").await
178}