upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/set_subject.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/ngit/sub_commands/set_subject.rs')
-rw-r--r--src/bin/ngit/sub_commands/set_subject.rs203
1 files changed, 203 insertions, 0 deletions
diff --git a/src/bin/ngit/sub_commands/set_subject.rs b/src/bin/ngit/sub_commands/set_subject.rs
new file mode 100644
index 0000000..65ff1d3
--- /dev/null
+++ b/src/bin/ngit/sub_commands/set_subject.rs
@@ -0,0 +1,203 @@
1use anyhow::{Context, Result, bail};
2use ngit::{
3 client::{Params, get_issues_from_cache, get_proposals_and_revisions_from_cache, send_events},
4 git_events::{KIND_LABEL, get_labels_and_subject},
5};
6use nostr::{EventBuilder, Tag, TagStandard};
7use nostr_sdk::{EventId, FromBech32};
8use nostr::nips::nip19::Nip19;
9
10use crate::{
11 client::{
12 Client, Connect, fetching_with_report, get_events_from_local_cache, get_repo_ref_from_cache,
13 save_event_in_local_cache,
14 },
15 git::{Repo, RepoActions},
16 login,
17 repo_ref::get_repo_coordinates_when_remote_unknown,
18};
19
20fn parse_event_id(id: &str) -> Result<EventId> {
21 if let Ok(nip19) = Nip19::from_bech32(id) {
22 match nip19 {
23 Nip19::Event(e) => return Ok(e.event_id),
24 Nip19::EventId(event_id) => return Ok(event_id),
25 _ => {}
26 }
27 }
28 if let Ok(event_id) = EventId::from_hex(id) {
29 return Ok(event_id);
30 }
31 bail!("invalid event-id or nevent: {id}")
32}
33
34/// Shared implementation: publish a NIP-32 kind-1985 `#subject` label event
35/// for `target`, overriding its displayed title/subject.
36///
37/// Only the author of the target event or a repository maintainer may set the
38/// subject. The subject is not applied to the underlying git commit message —
39/// it only affects how the PR/issue title is displayed.
40#[allow(clippy::too_many_lines)]
41async fn publish_set_subject_event(
42 id: &str,
43 subject: &str,
44 offline: bool,
45 target_kind: &str, // "issue" or "PR" — used in error messages
46) -> Result<()> {
47 let subject = subject.trim();
48 if subject.is_empty() {
49 bail!("--subject value must not be empty");
50 }
51
52 let event_id = parse_event_id(id)?;
53
54 let git_repo = Repo::discover().context("failed to find a git repository")?;
55 let git_repo_path = git_repo.get_path()?;
56
57 let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));
58 let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?;
59
60 if !offline {
61 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
62 }
63
64 let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
65
66 // Resolve the target event from cache.
67 let target = if target_kind == "issue" {
68 let issues = get_issues_from_cache(git_repo_path, repo_ref.coordinates()).await?;
69 issues
70 .into_iter()
71 .find(|e| e.id == event_id)
72 .context(format!(
73 "issue with id {} not found in cache",
74 event_id.to_hex()
75 ))?
76 } else {
77 let proposals =
78 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()).await?;
79 proposals
80 .into_iter()
81 .find(|e| e.id == event_id)
82 .context(format!(
83 "PR with id {} not found in cache",
84 event_id.to_hex()
85 ))?
86 };
87
88 // Login — we need the signer and user pubkey.
89 let (signer, user_ref, _) =
90 login::login_or_signup(&Some(&git_repo), &None, &None, Some(&client), true).await?;
91
92 let user_pubkey = signer.get_public_key().await?;
93
94 // Permission check: only the author or a maintainer may set the subject.
95 if target.pubkey != user_pubkey && !repo_ref.maintainers.contains(&user_pubkey) {
96 bail!(
97 "only the {target_kind} author or a repository maintainer can set the subject of a {target_kind}"
98 );
99 }
100
101 // Fetch existing label events so we can check whether the subject is
102 // already set to the requested value.
103 let existing_label_events = get_events_from_local_cache(
104 git_repo_path,
105 vec![
106 nostr::Filter::default()
107 .event(event_id)
108 .kind(KIND_LABEL),
109 ],
110 )
111 .await?;
112
113 let (_, existing_subject) = get_labels_and_subject(&target, &repo_ref, &existing_label_events);
114
115 if existing_subject.as_deref() == Some(subject) {
116 println!(
117 "{target_kind} {} already has subject: {}",
118 &event_id.to_hex()[..8],
119 subject,
120 );
121 return Ok(());
122 }
123
124 // Build the kind-1985 subject label event.
125 //
126 // Structure (NIP-32 §subject namespace):
127 // ["L", "#subject"] — namespace declaration
128 // ["l", "<new title>", "#subject"] — the new subject value
129 // ["e", <target-id>, <relay>] — reference to the labelled event
130 // ["p", <author-pubkey>] — notify the author
131 let relay_hint = repo_ref.relays.first().cloned();
132
133 let mut tags: Vec<Tag> = vec![
134 // Namespace declaration
135 Tag::custom(
136 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("L")),
137 vec!["#subject".to_string()],
138 ),
139 // Subject value
140 Tag::custom(
141 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("l")),
142 vec![subject.to_string(), "#subject".to_string()],
143 ),
144 ];
145
146 // Reference the target event.
147 tags.push(Tag::from_standardized(TagStandard::Event {
148 event_id: target.id,
149 relay_url: relay_hint.clone(),
150 marker: None,
151 public_key: None,
152 uppercase: false,
153 }));
154
155 // Notify the target event author.
156 tags.push(Tag::public_key(target.pubkey));
157
158 // Human-readable alt text.
159 tags.push(Tag::custom(
160 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
161 vec![format!("set {target_kind} subject to: {subject}")],
162 ));
163
164 let subject_event = ngit::client::sign_event(
165 EventBuilder::new(KIND_LABEL, "").tags(tags),
166 &signer,
167 format!("set {target_kind} subject"),
168 )
169 .await?;
170
171 // Save to local cache immediately so subsequent reads reflect the new subject.
172 save_event_in_local_cache(git_repo_path, &subject_event).await?;
173
174 let mut client = client;
175 client.set_signer(signer).await;
176
177 send_events(
178 &client,
179 Some(git_repo_path),
180 vec![subject_event],
181 user_ref.relays.write(),
182 repo_ref.relays.clone(),
183 true,
184 false,
185 )
186 .await?;
187
188 println!(
189 "{} {} subject set to: {}",
190 target_kind,
191 &event_id.to_hex()[..8],
192 subject,
193 );
194 Ok(())
195}
196
197pub async fn launch_issue_set_subject(id: &str, subject: &str, offline: bool) -> Result<()> {
198 publish_set_subject_event(id, subject, offline, "issue").await
199}
200
201pub async fn launch_pr_set_subject(id: &str, subject: &str, offline: bool) -> Result<()> {
202 publish_set_subject_event(id, subject, offline, "PR").await
203}