upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/ngit')
-rw-r--r--src/bin/ngit/cli.rs29
-rw-r--r--src/bin/ngit/main.rs11
-rw-r--r--src/bin/ngit/sub_commands/issue_status.rs30
-rw-r--r--src/bin/ngit/sub_commands/pr_status.rs29
4 files changed, 79 insertions, 20 deletions
diff --git a/src/bin/ngit/cli.rs b/src/bin/ngit/cli.rs
index b9b274e..37d85a2 100644
--- a/src/bin/ngit/cli.rs
+++ b/src/bin/ngit/cli.rs
@@ -270,6 +270,15 @@ pub enum PrCommands {
270 #[arg(long)] 270 #[arg(long)]
271 offline: bool, 271 offline: bool,
272 }, 272 },
273 /// convert a PR back to draft (author or maintainer only)
274 Draft {
275 /// Proposal event-id (hex) or nevent (bech32)
276 #[arg(value_name = "ID|nevent")]
277 id: String,
278 /// Use local cache only, skip network fetch
279 #[arg(long)]
280 offline: bool,
281 },
273 /// add a comment to a PR 282 /// add a comment to a PR
274 Comment { 283 Comment {
275 /// Proposal event-id (hex) or nevent (bech32) 284 /// Proposal event-id (hex) or nevent (bech32)
@@ -373,11 +382,29 @@ pub enum IssueCommands {
373 #[arg(long = "label", value_name = "LABEL")] 382 #[arg(long = "label", value_name = "LABEL")]
374 labels: Vec<String>, 383 labels: Vec<String>,
375 }, 384 },
376 /// close an issue (author or maintainer only) 385 /// close an issue without resolving it (author or maintainer only)
377 Close { 386 Close {
378 /// Issue event-id (hex) or nevent (bech32) 387 /// Issue event-id (hex) or nevent (bech32)
379 #[arg(value_name = "ID|nevent")] 388 #[arg(value_name = "ID|nevent")]
380 id: String, 389 id: String,
390 /// Optional reason (e.g. wontfix, duplicate, invalid)
391 #[arg(long)]
392 reason: Option<String>,
393 /// Use local cache only, skip network fetch
394 #[arg(long)]
395 offline: bool,
396 },
397 /// mark an issue as resolved (author or maintainer only)
398 #[command(
399 long_about = "mark an issue as resolved (author or maintainer only)\n\nuse this when the issue has been fixed or addressed, as distinct from closing without resolution"
400 )]
401 Resolved {
402 /// Issue event-id (hex) or nevent (bech32)
403 #[arg(value_name = "ID|nevent")]
404 id: String,
405 /// Optional reason or resolution summary
406 #[arg(long)]
407 reason: Option<String>,
381 /// Use local cache only, skip network fetch 408 /// Use local cache only, skip network fetch
382 #[arg(long)] 409 #[arg(long)]
383 offline: bool, 410 offline: bool,
diff --git a/src/bin/ngit/main.rs b/src/bin/ngit/main.rs
index b0cf375..a0cb3e6 100644
--- a/src/bin/ngit/main.rs
+++ b/src/bin/ngit/main.rs
@@ -111,6 +111,9 @@ async fn main() {
111 PrCommands::Ready { id, offline } => { 111 PrCommands::Ready { id, offline } => {
112 sub_commands::pr_status::launch_ready(id, *offline).await 112 sub_commands::pr_status::launch_ready(id, *offline).await
113 } 113 }
114 PrCommands::Draft { id, offline } => {
115 sub_commands::pr_status::launch_draft(id, *offline).await
116 }
114 PrCommands::Comment { 117 PrCommands::Comment {
115 id, 118 id,
116 body, 119 body,
@@ -178,8 +181,12 @@ async fn main() {
178 sub_commands::issue_create::launch(title.clone(), body.clone(), labels.clone()) 181 sub_commands::issue_create::launch(title.clone(), body.clone(), labels.clone())
179 .await 182 .await
180 } 183 }
181 IssueCommands::Close { id, offline } => { 184 IssueCommands::Close { id, reason, offline } => {
182 sub_commands::issue_status::launch_close(id, *offline).await 185 sub_commands::issue_status::launch_close(id, *offline, reason.as_deref()).await
186 }
187 IssueCommands::Resolved { id, reason, offline } => {
188 sub_commands::issue_status::launch_resolved(id, *offline, reason.as_deref())
189 .await
183 } 190 }
184 IssueCommands::Reopen { id, offline } => { 191 IssueCommands::Reopen { id, offline } => {
185 sub_commands::issue_status::launch_reopen(id, *offline).await 192 sub_commands::issue_status::launch_reopen(id, *offline).await
diff --git a/src/bin/ngit/sub_commands/issue_status.rs b/src/bin/ngit/sub_commands/issue_status.rs
index 3facee3..840ab8e 100644
--- a/src/bin/ngit/sub_commands/issue_status.rs
+++ b/src/bin/ngit/sub_commands/issue_status.rs
@@ -30,7 +30,13 @@ fn parse_event_id(id: &str) -> Result<EventId> {
30} 30}
31 31
32#[allow(clippy::too_many_lines)] 32#[allow(clippy::too_many_lines)]
33async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) -> Result<()> { 33async fn launch_status(
34 id: &str,
35 offline: bool,
36 new_kind: Kind,
37 action: &str,
38 reason: Option<&str>,
39) -> Result<()> {
34 let event_id = parse_event_id(id)?; 40 let event_id = parse_event_id(id)?;
35 41
36 let git_repo = Repo::discover().context("failed to find a git repository")?; 42 let git_repo = Repo::discover().context("failed to find a git repository")?;
@@ -64,7 +70,7 @@ async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) ->
64 70
65 // Only author or maintainer may change status 71 // Only author or maintainer may change status
66 if issue.pubkey != user_pubkey && !repo_ref.maintainers.contains(&user_pubkey) { 72 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"); 73 bail!("only the issue author or a repository maintainer can change the status of an issue");
68 } 74 }
69 75
70 // Fetch existing statuses to check current state 76 // Fetch existing statuses to check current state
@@ -96,6 +102,7 @@ async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) ->
96 let status_str = match new_kind { 102 let status_str = match new_kind {
97 Kind::GitStatusOpen => "open", 103 Kind::GitStatusOpen => "open",
98 Kind::GitStatusClosed => "closed", 104 Kind::GitStatusClosed => "closed",
105 Kind::GitStatusApplied => "resolved",
99 _ => "unknown", 106 _ => "unknown",
100 }; 107 };
101 println!("issue is already {status_str}"); 108 println!("issue is already {status_str}");
@@ -105,6 +112,7 @@ async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) ->
105 let alt_text = match new_kind { 112 let alt_text = match new_kind {
106 Kind::GitStatusOpen => "issue reopened", 113 Kind::GitStatusOpen => "issue reopened",
107 Kind::GitStatusClosed => "issue closed", 114 Kind::GitStatusClosed => "issue closed",
115 Kind::GitStatusApplied => "issue resolved",
108 _ => "issue status updated", 116 _ => "issue status updated",
109 }; 117 };
110 118
@@ -112,8 +120,10 @@ async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) ->
112 repo_ref.maintainers.iter().copied().collect(); 120 repo_ref.maintainers.iter().copied().collect();
113 public_keys.insert(issue.pubkey); 121 public_keys.insert(issue.pubkey);
114 122
123 let content = reason.unwrap_or("").to_string();
124
115 let status_event = sign_event( 125 let status_event = sign_event(
116 EventBuilder::new(new_kind, "").tags( 126 EventBuilder::new(new_kind, content).tags(
117 [ 127 [
118 vec![ 128 vec![
119 Tag::custom( 129 Tag::custom(
@@ -147,7 +157,7 @@ async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) ->
147 .concat(), 157 .concat(),
148 ), 158 ),
149 &signer, 159 &signer,
150 format!("{action} issue"), 160 format!("issue {action}"),
151 ) 161 )
152 .await?; 162 .await?;
153 163
@@ -165,14 +175,18 @@ async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) ->
165 ) 175 )
166 .await?; 176 .await?;
167 177
168 println!("issue {} {}d", &event_id.to_hex()[..8], action,); 178 println!("issue {} {action}", &event_id.to_hex()[..8]);
169 Ok(()) 179 Ok(())
170} 180}
171 181
172pub async fn launch_close(id: &str, offline: bool) -> Result<()> { 182pub async fn launch_close(id: &str, offline: bool, reason: Option<&str>) -> Result<()> {
173 launch_status(id, offline, Kind::GitStatusClosed, "close").await 183 launch_status(id, offline, Kind::GitStatusClosed, "closed", reason).await
174} 184}
175 185
176pub async fn launch_reopen(id: &str, offline: bool) -> Result<()> { 186pub async fn launch_reopen(id: &str, offline: bool) -> Result<()> {
177 launch_status(id, offline, Kind::GitStatusOpen, "reopen").await 187 launch_status(id, offline, Kind::GitStatusOpen, "reopened", None).await
188}
189
190pub async fn launch_resolved(id: &str, offline: bool, reason: Option<&str>) -> Result<()> {
191 launch_status(id, offline, Kind::GitStatusApplied, "resolved", reason).await
178} 192}
diff --git a/src/bin/ngit/sub_commands/pr_status.rs b/src/bin/ngit/sub_commands/pr_status.rs
index e84117d..12aafb7 100644
--- a/src/bin/ngit/sub_commands/pr_status.rs
+++ b/src/bin/ngit/sub_commands/pr_status.rs
@@ -30,7 +30,13 @@ fn parse_event_id(id: &str) -> Result<EventId> {
30} 30}
31 31
32#[allow(clippy::too_many_lines)] 32#[allow(clippy::too_many_lines)]
33async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) -> Result<()> { 33async fn launch_status(
34 id: &str,
35 offline: bool,
36 new_kind: Kind,
37 action: &str,
38 reason: Option<&str>,
39) -> Result<()> {
34 let event_id = parse_event_id(id)?; 40 let event_id = parse_event_id(id)?;
35 41
36 let git_repo = Repo::discover().context("failed to find a git repository")?; 42 let git_repo = Repo::discover().context("failed to find a git repository")?;
@@ -65,7 +71,7 @@ async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) ->
65 71
66 // Only author or maintainer may change status 72 // Only author or maintainer may change status
67 if proposal.pubkey != user_pubkey && !repo_ref.maintainers.contains(&user_pubkey) { 73 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"); 74 bail!("only the PR author or a repository maintainer can change the status of a PR");
69 } 75 }
70 76
71 // Fetch existing statuses to check current state 77 // Fetch existing statuses to check current state
@@ -124,8 +130,10 @@ async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) ->
124 repo_ref.maintainers.iter().copied().collect(); 130 repo_ref.maintainers.iter().copied().collect();
125 public_keys.insert(proposal.pubkey); 131 public_keys.insert(proposal.pubkey);
126 132
133 let content = reason.unwrap_or("").to_string();
134
127 let status_event = sign_event( 135 let status_event = sign_event(
128 EventBuilder::new(new_kind, "").tags( 136 EventBuilder::new(new_kind, content).tags(
129 [ 137 [
130 vec![ 138 vec![
131 Tag::custom( 139 Tag::custom(
@@ -159,7 +167,7 @@ async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) ->
159 .concat(), 167 .concat(),
160 ), 168 ),
161 &signer, 169 &signer,
162 format!("{action} PR"), 170 format!("PR {action}"),
163 ) 171 )
164 .await?; 172 .await?;
165 173
@@ -178,22 +186,25 @@ async fn launch_status(id: &str, offline: bool, new_kind: Kind, action: &str) ->
178 .await?; 186 .await?;
179 187
180 println!( 188 println!(
181 "PR {} {}d: {}", 189 "PR {} {action}: {}",
182 &event_id.to_hex()[..8], 190 &event_id.to_hex()[..8],
183 action,
184 proposal.pubkey.to_bech32().unwrap_or_default() 191 proposal.pubkey.to_bech32().unwrap_or_default()
185 ); 192 );
186 Ok(()) 193 Ok(())
187} 194}
188 195
189pub async fn launch_close(id: &str, offline: bool) -> Result<()> { 196pub async fn launch_close(id: &str, offline: bool) -> Result<()> {
190 launch_status(id, offline, Kind::GitStatusClosed, "close").await 197 launch_status(id, offline, Kind::GitStatusClosed, "closed", None).await
191} 198}
192 199
193pub async fn launch_reopen(id: &str, offline: bool) -> Result<()> { 200pub async fn launch_reopen(id: &str, offline: bool) -> Result<()> {
194 launch_status(id, offline, Kind::GitStatusOpen, "reopen").await 201 launch_status(id, offline, Kind::GitStatusOpen, "reopened", None).await
195} 202}
196 203
197pub async fn launch_ready(id: &str, offline: bool) -> Result<()> { 204pub async fn launch_ready(id: &str, offline: bool) -> Result<()> {
198 launch_status(id, offline, Kind::GitStatusOpen, "mark as ready").await 205 launch_status(id, offline, Kind::GitStatusOpen, "marked as ready", None).await
206}
207
208pub async fn launch_draft(id: &str, offline: bool) -> Result<()> {
209 launch_status(id, offline, Kind::GitStatusDraft, "converted to draft", None).await
199} 210}