a73x

324b0548

Wire up 'git-collab issue reopen <id>'

a73x   2026-08-09 16:50

Commit message
Wire up 'git-collab issue reopen <id>'

issue::reopen() appended Action::IssueReopen but nothing in the CLI
called it. Add the reopen subcommand, and move the ref back out of
refs/collab/archive/issues/ on reopen (mirroring how close() archives
it), so a reopened issue is visible to the default `issue list` again.

Fixes 57a15635

src/cli.rs
Old New
@@ -237,6 +237,11 @@ pub enum IssueCmd {
237 /// Issue ID (prefix match) 237 /// Issue ID (prefix match)
238 id: String, 238 id: String,
239 }, 239 },
240 /// Reopen a closed issue
241 Reopen {
242 /// Issue ID (prefix match)
243 id: String,
244 },
240 } 245 }
241 246
242 #[derive(Subcommand)] 247 #[derive(Subcommand)]
@@ -414,6 +419,7 @@ impl Commands {
414 | IssueCmd::Unlabel { .. } 419 | IssueCmd::Unlabel { .. }
415 | IssueCmd::Assign { .. } 420 | IssueCmd::Assign { .. }
416 | IssueCmd::Unassign { .. } 421 | IssueCmd::Unassign { .. }
422 | IssueCmd::Reopen { .. }
417 ), 423 ),
418 Commands::Patch(cmd) => matches!( 424 Commands::Patch(cmd) => matches!(
419 cmd, 425 cmd,
src/issue.rs
Old New
@@ -260,7 +260,12 @@ pub fn delete(repo: &Repository, id_prefix: &str) -> Result<String, crate::error
260 } 260 }
261 261
262 pub fn reopen(repo: &Repository, id_prefix: &str) -> Result<(), crate::error::Error> { 262 pub fn reopen(repo: &Repository, id_prefix: &str) -> Result<(), crate::error::Error> {
263 let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?; 263 let (ref_name, id) = state::resolve_issue_ref(repo, id_prefix)?;
264 dag::append_action(repo, &ref_name, Action::IssueReopen)?; 264 dag::append_action(repo, &ref_name, Action::IssueReopen)?;
265 // Move the ref back out of the archive namespace (undoes what close() did),
266 // so the reopened issue is visible to the default `issue list` again.
267 if ref_name.starts_with("refs/collab/archive/issues/") {
268 state::unarchive_issue_ref(repo, &id)?;
269 }
265 Ok(()) 270 Ok(())
266 } 271 }
src/lib.rs
Old New
@@ -278,6 +278,11 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
278 println!("Deleted issue {:.8}", full_id); 278 println!("Deleted issue {:.8}", full_id);
279 Ok(()) 279 Ok(())
280 } 280 }
281 IssueCmd::Reopen { id } => {
282 issue::reopen(repo, &id)?;
283 println!("Issue reopened.");
284 Ok(())
285 }
281 }, 286 },
282 Commands::Patch(cmd) => match cmd { 287 Commands::Patch(cmd) => match cmd {
283 PatchCmd::Create { 288 PatchCmd::Create {
src/state.rs
Old New
@@ -882,6 +882,16 @@ pub fn archive_issue_ref(repo: &Repository, id: &str) -> Result<(), crate::error
882 Ok(()) 882 Ok(())
883 } 883 }
884 884
885 /// Move an issue ref from archive back to the active namespace.
886 pub fn unarchive_issue_ref(repo: &Repository, id: &str) -> Result<(), crate::error::Error> {
887 let old_ref = format!("refs/collab/archive/issues/{}", id);
888 let oid = repo.refname_to_id(&old_ref)?;
889 let new_ref = format!("refs/collab/issues/{}", id);
890 repo.reference(&new_ref, oid, false, "unarchive issue")?;
891 repo.find_reference(&old_ref)?.delete()?;
892 Ok(())
893 }
894
885 /// Move a patch ref from active to archive namespace. 895 /// Move a patch ref from active to archive namespace.
886 pub fn archive_patch_ref(repo: &Repository, id: &str) -> Result<(), crate::error::Error> { 896 pub fn archive_patch_ref(repo: &Repository, id: &str) -> Result<(), crate::error::Error> {
887 let old_ref = format!("refs/collab/patches/{}", id); 897 let old_ref = format!("refs/collab/patches/{}", id);
tests/archive_test.rs
Old New
@@ -4,6 +4,7 @@ use tempfile::TempDir;
4 4
5 use git_collab::dag; 5 use git_collab::dag;
6 use git_collab::event::{Action, Event}; 6 use git_collab::event::{Action, Event};
7 use git_collab::issue;
7 use git_collab::state::{self, IssueState, IssueStatus, PatchState, PatchStatus}; 8 use git_collab::state::{self, IssueState, IssueStatus, PatchState, PatchStatus};
8 9
9 use common::{alice, close_issue, create_patch, init_repo, now, open_issue, test_signing_key}; 10 use common::{alice, close_issue, create_patch, init_repo, now, open_issue, test_signing_key};
@@ -82,6 +83,40 @@ fn test_resolve_issue_ref_finds_archived() {
82 assert!(resolved_ref.contains("archive")); 83 assert!(resolved_ref.contains("archive"));
83 } 84 }
84 85
86 #[test]
87 fn test_reopen_issue_moves_ref_out_of_archive() {
88 let tmp = TempDir::new().unwrap();
89 let repo = init_repo(tmp.path(), &alice());
90
91 let (ref_name, id) = open_issue(&repo, &alice(), "Reopen me");
92 issue::close(&repo, &id, None).unwrap();
93
94 // Sanity check: closing archived the ref.
95 let archive_ref = format!("refs/collab/archive/issues/{}", id);
96 assert!(repo.refname_to_id(&archive_ref).is_ok());
97 assert!(repo.refname_to_id(&ref_name).is_err());
98
99 issue::reopen(&repo, &id).unwrap();
100
101 // The ref must move back out of the archive namespace, or the reopened
102 // issue stays invisible to the default `issue list`.
103 assert!(
104 repo.refname_to_id(&ref_name).is_ok(),
105 "active ref should be restored"
106 );
107 assert!(
108 repo.refname_to_id(&archive_ref).is_err(),
109 "archive ref should be removed"
110 );
111
112 let state = IssueState::from_ref(&repo, &ref_name, &id).unwrap();
113 assert_eq!(state.status, IssueStatus::Open);
114
115 let issues = state::list_issues(&repo).unwrap();
116 assert_eq!(issues.len(), 1);
117 assert_eq!(issues[0].title, "Reopen me");
118 }
119
85 // --------------------------------------------------------------------------- 120 // ---------------------------------------------------------------------------
86 // Archive on close: patches 121 // Archive on close: patches
87 // --------------------------------------------------------------------------- 122 // ---------------------------------------------------------------------------
tests/cli_test.rs
Old New
@@ -89,6 +89,27 @@ fn test_issue_close_without_reason() {
89 } 89 }
90 90
91 #[test] 91 #[test]
92 fn test_issue_reopen() {
93 let repo = TestRepo::new("Alice", "alice@example.com");
94 let id = repo.issue_open("Closed by mistake");
95
96 repo.run_ok(&["issue", "close", &id]);
97 let out = repo.run_ok(&["issue", "show", &id]);
98 assert!(out.contains("[closed]"));
99
100 let out = repo.run_ok(&["issue", "reopen", &id]);
101 assert!(out.contains("Issue reopened"));
102
103 let out = repo.run_ok(&["issue", "show", &id]);
104 assert!(out.contains("[open]"));
105
106 // Reopening must move the ref back out of refs/collab/archive/, or the
107 // issue stays invisible to the default (non-archived) list.
108 let out = repo.run_ok(&["issue", "list"]);
109 assert!(out.contains("Closed by mistake"));
110 }
111
112 #[test]
92 fn test_issue_prefix_resolution() { 113 fn test_issue_prefix_resolution() {
93 let repo = TestRepo::new("Alice", "alice@example.com"); 114 let repo = TestRepo::new("Alice", "alice@example.com");
94 let id = repo.issue_open("Prefix test"); 115 let id = repo.issue_open("Prefix test");