From e2f2e3861bf3e70ae1cb19ddb02fc617be5ff8f5 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 6 Apr 2026 16:19:18 -0400 Subject: [PATCH] Add original_commit_hash to ArchivedGitWorktree - New field on ArchivedGitWorktree struct storing the HEAD SHA from before WIP commits were created during archival. Used as a pre-restore sanity check, fallback reset target, and post-restore verification. - DB migration adding original_commit_hash column, backfilled from the legacy commit_hash column for existing rows. - Updated create_archived_worktree signature, SQL INSERT, and bindings on both ThreadMetadataDb and ThreadMetadataStore. - Updated SELECT query and Column impl to read the new field. - Updated all 4 archived worktree tests. --- crates/agent_ui/src/thread_metadata_store.rs | 30 +++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs index 1de5c93ff179ffc31f4fa9187484dd99c388cbc4..4766e00424a0c9d007c09637468da5e9fd11fe94 100644 --- a/crates/agent_ui/src/thread_metadata_store.rs +++ b/crates/agent_ui/src/thread_metadata_store.rs @@ -181,6 +181,12 @@ pub struct ArchivedGitWorktree { /// we had before archiving, including what was staged, what was unstaged, and /// what was committed. pub unstaged_commit_hash: String, + /// SHA of the commit that HEAD pointed at before we created the two WIP + /// commits during archival. After resetting past the WIP commits during + /// restore, HEAD should land back on this commit. It also serves as a + /// pre-restore sanity check (abort if this commit no longer exists in the + /// repo) and as a fallback target if the WIP resets fail. + pub original_commit_hash: String, } /// The store holds all metadata needed to show threads in the sidebar/the archive. @@ -434,6 +440,7 @@ impl ThreadMetadataStore { branch_name: Option, staged_commit_hash: String, unstaged_commit_hash: String, + original_commit_hash: String, cx: &App, ) -> Task> { let db = self.db.clone(); @@ -444,6 +451,7 @@ impl ThreadMetadataStore { branch_name, staged_commit_hash, unstaged_commit_hash, + original_commit_hash, ) .await }) @@ -743,6 +751,10 @@ impl Domain for ThreadMetadataDb { ALTER TABLE archived_git_worktrees ADD COLUMN unstaged_commit_hash TEXT; UPDATE archived_git_worktrees SET staged_commit_hash = commit_hash, unstaged_commit_hash = commit_hash WHERE staged_commit_hash IS NULL; ), + sql!( + ALTER TABLE archived_git_worktrees ADD COLUMN original_commit_hash TEXT; + UPDATE archived_git_worktrees SET original_commit_hash = commit_hash WHERE original_commit_hash IS NULL; + ), ]; } @@ -839,12 +851,13 @@ impl ThreadMetadataDb { branch_name: Option, staged_commit_hash: String, unstaged_commit_hash: String, + original_commit_hash: String, ) -> anyhow::Result { self.write(move |conn| { let mut stmt = Statement::prepare( conn, - "INSERT INTO archived_git_worktrees(worktree_path, main_repo_path, branch_name, commit_hash, staged_commit_hash, unstaged_commit_hash) \ - VALUES (?1, ?2, ?3, ?4, ?5, ?6) \ + "INSERT INTO archived_git_worktrees(worktree_path, main_repo_path, branch_name, commit_hash, staged_commit_hash, unstaged_commit_hash, original_commit_hash) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) \ RETURNING id", )?; let mut i = stmt.bind(&worktree_path, 1)?; @@ -852,7 +865,8 @@ impl ThreadMetadataDb { i = stmt.bind(&branch_name, i)?; i = stmt.bind(&unstaged_commit_hash, i)?; i = stmt.bind(&staged_commit_hash, i)?; - stmt.bind(&unstaged_commit_hash, i)?; + i = stmt.bind(&unstaged_commit_hash, i)?; + stmt.bind(&original_commit_hash, i)?; stmt.maybe_row::()?.context("expected RETURNING id") }) .await @@ -881,7 +895,7 @@ impl ThreadMetadataDb { session_id: String, ) -> anyhow::Result> { self.select_bound::( - "SELECT a.id, a.worktree_path, a.main_repo_path, a.branch_name, a.staged_commit_hash, a.unstaged_commit_hash \ + "SELECT a.id, a.worktree_path, a.main_repo_path, a.branch_name, a.staged_commit_hash, a.unstaged_commit_hash, a.original_commit_hash \ FROM archived_git_worktrees a \ JOIN thread_archived_worktrees t ON a.id = t.archived_worktree_id \ WHERE t.session_id = ?1", @@ -975,6 +989,7 @@ impl Column for ArchivedGitWorktree { let (branch_name, next): (Option, i32) = Column::column(statement, next)?; let (staged_commit_hash, next): (String, i32) = Column::column(statement, next)?; let (unstaged_commit_hash, next): (String, i32) = Column::column(statement, next)?; + let (original_commit_hash, next): (String, i32) = Column::column(statement, next)?; Ok(( ArchivedGitWorktree { @@ -984,6 +999,7 @@ impl Column for ArchivedGitWorktree { branch_name, staged_commit_hash, unstaged_commit_hash, + original_commit_hash, }, next, )) @@ -2132,6 +2148,7 @@ mod tests { Some("feature-branch".to_string()), "staged_aaa".to_string(), "unstaged_bbb".to_string(), + "original_000".to_string(), cx, ) }) @@ -2160,6 +2177,7 @@ mod tests { assert_eq!(wt.branch_name.as_deref(), Some("feature-branch")); assert_eq!(wt.staged_commit_hash, "staged_aaa"); assert_eq!(wt.unstaged_commit_hash, "unstaged_bbb"); + assert_eq!(wt.original_commit_hash, "original_000"); } #[gpui::test] @@ -2175,6 +2193,7 @@ mod tests { Some("main".to_string()), "deadbeef".to_string(), "deadbeef".to_string(), + "original_000".to_string(), cx, ) }) @@ -2215,6 +2234,7 @@ mod tests { None, "abc123".to_string(), "abc123".to_string(), + "original_000".to_string(), cx, ) }) @@ -2267,6 +2287,7 @@ mod tests { Some("branch-a".to_string()), "staged_a".to_string(), "unstaged_a".to_string(), + "original_000".to_string(), cx, ) }) @@ -2281,6 +2302,7 @@ mod tests { Some("branch-b".to_string()), "staged_b".to_string(), "unstaged_b".to_string(), + "original_000".to_string(), cx, ) })