0465da85
Add delete command for issues and patches
a73x 2026-03-21 16:19
Adds `issue delete` and `patch delete` subcommands that remove the
local collab ref (refs/collab/issues/{id} or refs/collab/patches/{id}).
This is a local-only operation that permanently removes the issue/patch
from listing and show commands.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/src/cli.rs b/src/cli.rs index 89ab8a3..9c1a270 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -157,6 +157,11 @@ pub enum IssueCmd { /// Issue ID (prefix match) id: String, }, /// Delete an issue (removes the local collab ref) Delete { /// Issue ID (prefix match) id: String, }, } #[derive(Subcommand)] @@ -275,4 +280,9 @@ pub enum PatchCmd { #[arg(short, long)] reason: Option<String>, }, /// Delete a patch (removes the local collab ref) Delete { /// Patch ID (prefix match) id: String, }, } diff --git a/src/issue.rs b/src/issue.rs index cee4408..3636152 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -267,6 +267,12 @@ pub fn close( Ok(()) } pub fn delete(repo: &Repository, id_prefix: &str) -> Result<String, crate::error::Error> { let (ref_name, id) = state::resolve_issue_ref(repo, id_prefix)?; repo.find_reference(&ref_name)?.delete()?; Ok(id) } pub fn reopen(repo: &Repository, id_prefix: &str) -> Result<(), crate::error::Error> { let sk = signing::load_signing_key(&signing::signing_key_dir()?)?; let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?; diff --git a/src/lib.rs b/src/lib.rs index ce146e9..a773cd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,6 +138,11 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> { println!("Issue reopened."); Ok(()) } IssueCmd::Delete { id } => { let full_id = issue::delete(repo, &id)?; println!("Deleted issue {:.8}", full_id); Ok(()) } }, Commands::Patch(cmd) => match cmd { PatchCmd::Create { @@ -315,6 +320,11 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> { println!("Patch closed."); Ok(()) } PatchCmd::Delete { id } => { let full_id = patch::delete(repo, &id)?; println!("Deleted patch {:.8}", full_id); Ok(()) } }, Commands::Status => { let project_status = status::compute(repo)?; diff --git a/src/patch.rs b/src/patch.rs index f09a353..e756238 100644 --- a/src/patch.rs +++ b/src/patch.rs @@ -339,6 +339,12 @@ pub fn generate_diff(repo: &Repository, patch: &state::PatchState) -> Result<Str Ok(output) } pub fn delete(repo: &Repository, id_prefix: &str) -> Result<String, crate::error::Error> { let (ref_name, id) = state::resolve_patch_ref(repo, id_prefix)?; repo.find_reference(&ref_name)?.delete()?; Ok(id) } pub fn close( repo: &Repository, id_prefix: &str, diff --git a/tests/cli_test.rs b/tests/cli_test.rs index 497ed3f..2ea1c65 100644 --- a/tests/cli_test.rs +++ b/tests/cli_test.rs @@ -864,3 +864,77 @@ fn test_init_key_creates_key_files() { ); assert!(stdout.contains("Signing key generated")); } // =========================================================================== // Issue delete // =========================================================================== #[test] fn test_issue_delete_removes_from_list() { let repo = TestRepo::new("Alice", "alice@example.com"); let id = repo.issue_open("Bug to delete"); // Verify it shows up let out = repo.run_ok(&["issue", "list"]); assert!(out.contains("Bug to delete")); // Delete it let out = repo.run_ok(&["issue", "delete", &id]); assert!(out.contains("Deleted issue")); // Verify it's gone let out = repo.run_ok(&["issue", "list", "--all"]); assert!(!out.contains("Bug to delete")); } #[test] fn test_issue_delete_nonexistent_errors() { let repo = TestRepo::new("Alice", "alice@example.com"); let err = repo.run_err(&["issue", "delete", "deadbeef"]); assert!(!err.is_empty()); } #[test] fn test_issue_delete_then_show_errors() { let repo = TestRepo::new("Alice", "alice@example.com"); let id = repo.issue_open("Ephemeral"); repo.run_ok(&["issue", "delete", &id]); repo.run_err(&["issue", "show", &id]); } // =========================================================================== // Patch delete // =========================================================================== #[test] fn test_patch_delete_removes_from_list() { let repo = TestRepo::new("Alice", "alice@example.com"); let id = repo.patch_create("Patch to delete"); // Verify it shows up let out = repo.run_ok(&["patch", "list"]); assert!(out.contains("Patch to delete")); // Delete it let out = repo.run_ok(&["patch", "delete", &id]); assert!(out.contains("Deleted patch")); // Verify it's gone let out = repo.run_ok(&["patch", "list", "--all"]); assert!(!out.contains("Patch to delete")); } #[test] fn test_patch_delete_nonexistent_errors() { let repo = TestRepo::new("Alice", "alice@example.com"); let err = repo.run_err(&["patch", "delete", "deadbeef"]); assert!(!err.is_empty()); } #[test] fn test_patch_delete_then_show_errors() { let repo = TestRepo::new("Alice", "alice@example.com"); let id = repo.patch_create("Ephemeral patch"); repo.run_ok(&["patch", "delete", &id]); repo.run_err(&["patch", "show", &id]); }