Delete unused checkpoints (#27260)

Release Notes:

- N/A
This commit is contained in:
Antonio Scandurra 2025-03-21 17:39:01 +01:00 committed by GitHub
parent a52e2f9553
commit 0e9e2d70cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 177 additions and 67 deletions

View file

@ -12,6 +12,7 @@ use gpui::{
}; };
use language_model::LanguageModelRegistry; use language_model::LanguageModelRegistry;
use language_model_selector::ToggleModelSelector; use language_model_selector::ToggleModelSelector;
use project::Project;
use rope::Point; use rope::Point;
use settings::Settings; use settings::Settings;
use std::time::Duration; use std::time::Duration;
@ -37,6 +38,7 @@ pub struct MessageEditor {
editor: Entity<Editor>, editor: Entity<Editor>,
#[allow(dead_code)] #[allow(dead_code)]
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
project: Entity<Project>,
context_store: Entity<ContextStore>, context_store: Entity<ContextStore>,
context_strip: Entity<ContextStrip>, context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>, context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
@ -107,6 +109,7 @@ impl MessageEditor {
Self { Self {
editor: editor.clone(), editor: editor.clone(),
project: thread.read(cx).project().clone(),
thread, thread,
workspace, workspace,
context_store, context_store,
@ -205,7 +208,9 @@ impl MessageEditor {
let thread = self.thread.clone(); let thread = self.thread.clone();
let context_store = self.context_store.clone(); let context_store = self.context_store.clone();
let checkpoint = self.project.read(cx).git_store().read(cx).checkpoint(cx);
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
let checkpoint = checkpoint.await.ok();
refresh_task.await; refresh_task.await;
let (system_prompt_context, load_error) = system_prompt_context_task.await; let (system_prompt_context, load_error) = system_prompt_context_task.await;
thread thread
@ -219,7 +224,7 @@ impl MessageEditor {
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>(); let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
thread.insert_user_message(user_message, context, cx); thread.insert_user_message(user_message, context, checkpoint, cx);
thread.send_to_model(model, request_kind, cx); thread.send_to_model(model, request_kind, cx);
}) })
.ok(); .ok();

View file

@ -1,6 +1,5 @@
use std::fmt::Write as _; use std::fmt::Write as _;
use std::io::Write; use std::io::Write;
use std::mem;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
@ -186,8 +185,7 @@ pub struct Thread {
tool_use: ToolUseState, tool_use: ToolUseState,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
last_restore_checkpoint: Option<LastRestoreCheckpoint>, last_restore_checkpoint: Option<LastRestoreCheckpoint>,
pending_checkpoint: Option<Task<Result<ThreadCheckpoint>>>, pending_checkpoint: Option<ThreadCheckpoint>,
checkpoint_on_next_user_message: bool,
scripting_session: Entity<ScriptingSession>, scripting_session: Entity<ScriptingSession>,
scripting_tool_use: ToolUseState, scripting_tool_use: ToolUseState,
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>, initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
@ -220,7 +218,6 @@ impl Thread {
tools: tools.clone(), tools: tools.clone(),
last_restore_checkpoint: None, last_restore_checkpoint: None,
pending_checkpoint: None, pending_checkpoint: None,
checkpoint_on_next_user_message: true,
tool_use: ToolUseState::new(tools.clone()), tool_use: ToolUseState::new(tools.clone()),
scripting_session: cx.new(|cx| ScriptingSession::new(project.clone(), cx)), scripting_session: cx.new(|cx| ScriptingSession::new(project.clone(), cx)),
scripting_tool_use: ToolUseState::new(tools), scripting_tool_use: ToolUseState::new(tools),
@ -293,7 +290,6 @@ impl Thread {
pending_completions: Vec::new(), pending_completions: Vec::new(),
last_restore_checkpoint: None, last_restore_checkpoint: None,
pending_checkpoint: None, pending_checkpoint: None,
checkpoint_on_next_user_message: true,
project, project,
prompt_builder, prompt_builder,
tools, tools,
@ -385,11 +381,8 @@ impl Thread {
} else { } else {
this.truncate(checkpoint.message_id, cx); this.truncate(checkpoint.message_id, cx);
this.last_restore_checkpoint = None; this.last_restore_checkpoint = None;
this.pending_checkpoint = Some(Task::ready(Ok(ThreadCheckpoint {
message_id: this.next_message_id,
git_checkpoint: checkpoint.git_checkpoint,
})));
} }
this.pending_checkpoint = None;
cx.emit(ThreadEvent::CheckpointChanged); cx.emit(ThreadEvent::CheckpointChanged);
cx.notify(); cx.notify();
})?; })?;
@ -397,46 +390,62 @@ impl Thread {
}) })
} }
fn checkpoint(&mut self, cx: &mut Context<Self>) { fn finalize_pending_checkpoint(&mut self, cx: &mut Context<Self>) {
if self.is_generating() { let pending_checkpoint = if self.is_generating() {
return; return;
} } else if let Some(checkpoint) = self.pending_checkpoint.take() {
checkpoint
} else {
return;
};
let git_store = self.project.read(cx).git_store().clone(); let git_store = self.project.read(cx).git_store().clone();
let new_checkpoint = git_store.read(cx).checkpoint(cx); let final_checkpoint = git_store.read(cx).checkpoint(cx);
let old_checkpoint = self.pending_checkpoint.take(); cx.spawn(async move |this, cx| match final_checkpoint.await {
let next_user_message_id = self.next_message_id; Ok(final_checkpoint) => {
self.pending_checkpoint = Some(cx.spawn(async move |this, cx| { let equal = git_store
let new_checkpoint = new_checkpoint.await?; .read_with(cx, |store, cx| {
store.compare_checkpoints(
pending_checkpoint.git_checkpoint.clone(),
final_checkpoint.clone(),
cx,
)
})?
.await
.unwrap_or(false);
if let Some(old_checkpoint) = old_checkpoint { if equal {
if let Ok(old_checkpoint) = old_checkpoint.await { git_store
let equal = git_store
.read_with(cx, |store, cx| { .read_with(cx, |store, cx| {
store.compare_checkpoints( store.delete_checkpoint(pending_checkpoint.git_checkpoint, cx)
old_checkpoint.git_checkpoint.clone(),
new_checkpoint.clone(),
cx,
)
})? })?
.await; .detach();
} else {
if equal.ok() != Some(true) { this.update(cx, |this, cx| {
this.update(cx, |this, cx| { this.insert_checkpoint(pending_checkpoint, cx)
this.checkpoints_by_message })?;
.insert(old_checkpoint.message_id, old_checkpoint);
cx.emit(ThreadEvent::CheckpointChanged);
cx.notify();
})?;
}
} }
}
Ok(ThreadCheckpoint { git_store
message_id: next_user_message_id, .read_with(cx, |store, cx| {
git_checkpoint: new_checkpoint, store.delete_checkpoint(final_checkpoint, cx)
}) })?
})); .detach();
Ok(())
}
Err(_) => this.update(cx, |this, cx| {
this.insert_checkpoint(pending_checkpoint, cx)
}),
})
.detach();
}
fn insert_checkpoint(&mut self, checkpoint: ThreadCheckpoint, cx: &mut Context<Self>) {
self.checkpoints_by_message
.insert(checkpoint.message_id, checkpoint);
cx.emit(ThreadEvent::CheckpointChanged);
cx.notify();
} }
pub fn last_restore_checkpoint(&self) -> Option<&LastRestoreCheckpoint> { pub fn last_restore_checkpoint(&self) -> Option<&LastRestoreCheckpoint> {
@ -517,18 +526,21 @@ impl Thread {
&mut self, &mut self,
text: impl Into<String>, text: impl Into<String>,
context: Vec<ContextSnapshot>, context: Vec<ContextSnapshot>,
git_checkpoint: Option<GitStoreCheckpoint>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> MessageId { ) -> MessageId {
if mem::take(&mut self.checkpoint_on_next_user_message) {
self.checkpoint(cx);
}
let message_id = let message_id =
self.insert_message(Role::User, vec![MessageSegment::Text(text.into())], cx); self.insert_message(Role::User, vec![MessageSegment::Text(text.into())], cx);
let context_ids = context.iter().map(|context| context.id).collect::<Vec<_>>(); let context_ids = context.iter().map(|context| context.id).collect::<Vec<_>>();
self.context self.context
.extend(context.into_iter().map(|context| (context.id, context))); .extend(context.into_iter().map(|context| (context.id, context)));
self.context_by_message.insert(message_id, context_ids); self.context_by_message.insert(message_id, context_ids);
if let Some(git_checkpoint) = git_checkpoint {
self.pending_checkpoint = Some(ThreadCheckpoint {
message_id,
git_checkpoint,
});
}
message_id message_id
} }
@ -1050,7 +1062,7 @@ impl Thread {
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
thread.checkpoint(cx); thread.finalize_pending_checkpoint(cx);
match result.as_ref() { match result.as_ref() {
Ok(stop_reason) => match stop_reason { Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => { StopReason::ToolUse => {
@ -1319,6 +1331,7 @@ impl Thread {
// so for now we provide some text to keep the model on track. // so for now we provide some text to keep the model on track.
"Here are the tool results.", "Here are the tool results.",
Vec::new(), Vec::new(),
None,
cx, cx,
); );
} }
@ -1341,7 +1354,7 @@ impl Thread {
} }
canceled canceled
}; };
self.checkpoint(cx); self.finalize_pending_checkpoint(cx);
canceled canceled
} }

View file

@ -96,7 +96,7 @@ impl Eval {
assistant.update(cx, |assistant, cx| { assistant.update(cx, |assistant, cx| {
assistant.thread.update(cx, |thread, cx| { assistant.thread.update(cx, |thread, cx| {
let context = vec![]; let context = vec![];
thread.insert_user_message(self.user_prompt.clone(), context, cx); thread.insert_user_message(self.user_prompt.clone(), context, None, cx);
thread.set_system_prompt_context(system_prompt_context); thread.set_system_prompt_context(system_prompt_context);
thread.send_to_model(model, RequestKind::Chat, cx); thread.send_to_model(model, RequestKind::Chat, cx);
}); });

View file

@ -429,4 +429,12 @@ impl GitRepository for FakeGitRepository {
) -> BoxFuture<Result<bool>> { ) -> BoxFuture<Result<bool>> {
unimplemented!() unimplemented!()
} }
fn delete_checkpoint(
&self,
_checkpoint: GitRepositoryCheckpoint,
_cx: AsyncApp,
) -> BoxFuture<Result<()>> {
unimplemented!()
}
} }

View file

@ -308,6 +308,13 @@ pub trait GitRepository: Send + Sync {
right: GitRepositoryCheckpoint, right: GitRepositoryCheckpoint,
cx: AsyncApp, cx: AsyncApp,
) -> BoxFuture<Result<bool>>; ) -> BoxFuture<Result<bool>>;
/// Deletes a previously-created checkpoint.
fn delete_checkpoint(
&self,
checkpoint: GitRepositoryCheckpoint,
cx: AsyncApp,
) -> BoxFuture<Result<()>>;
} }
pub enum DiffType { pub enum DiffType {
@ -351,10 +358,11 @@ impl RealGitRepository {
} }
} }
#[derive(Copy, Clone)] #[derive(Clone, Debug)]
pub struct GitRepositoryCheckpoint { pub struct GitRepositoryCheckpoint {
ref_name: String,
head_sha: Option<Oid>, head_sha: Option<Oid>,
sha: Oid, commit_sha: Oid,
} }
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects // https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
@ -1071,21 +1079,17 @@ impl GitRepository for RealGitRepository {
} else { } else {
git.run(&["commit-tree", &tree, "-m", "Checkpoint"]).await? git.run(&["commit-tree", &tree, "-m", "Checkpoint"]).await?
}; };
let ref_name = Uuid::new_v4().to_string(); let ref_name = format!("refs/zed/{}", Uuid::new_v4());
git.run(&[ git.run(&["update-ref", &ref_name, &checkpoint_sha]).await?;
"update-ref",
&format!("refs/zed/{ref_name}"),
&checkpoint_sha,
])
.await?;
Ok(GitRepositoryCheckpoint { Ok(GitRepositoryCheckpoint {
ref_name,
head_sha: if let Some(head_sha) = head_sha { head_sha: if let Some(head_sha) = head_sha {
Some(head_sha.parse()?) Some(head_sha.parse()?)
} else { } else {
None None
}, },
sha: checkpoint_sha.parse()?, commit_sha: checkpoint_sha.parse()?,
}) })
}) })
.await .await
@ -1109,14 +1113,15 @@ impl GitRepository for RealGitRepository {
git.run(&[ git.run(&[
"restore", "restore",
"--source", "--source",
&checkpoint.sha.to_string(), &checkpoint.commit_sha.to_string(),
"--worktree", "--worktree",
".", ".",
]) ])
.await?; .await?;
git.with_temp_index(async move |git| { git.with_temp_index(async move |git| {
git.run(&["read-tree", &checkpoint.sha.to_string()]).await?; git.run(&["read-tree", &checkpoint.commit_sha.to_string()])
.await?;
git.run(&["clean", "-d", "--force"]).await git.run(&["clean", "-d", "--force"]).await
}) })
.await?; .await?;
@ -1154,8 +1159,8 @@ impl GitRepository for RealGitRepository {
.run(&[ .run(&[
"diff-tree", "diff-tree",
"--quiet", "--quiet",
&left.sha.to_string(), &left.commit_sha.to_string(),
&right.sha.to_string(), &right.commit_sha.to_string(),
]) ])
.await; .await;
match result { match result {
@ -1175,6 +1180,24 @@ impl GitRepository for RealGitRepository {
}) })
.boxed() .boxed()
} }
fn delete_checkpoint(
&self,
checkpoint: GitRepositoryCheckpoint,
cx: AsyncApp,
) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
let executor = cx.background_executor().clone();
cx.background_spawn(async move {
let working_directory = working_directory?;
let git = GitBinary::new(git_binary_path, working_directory, executor);
git.run(&["update-ref", "-d", &checkpoint.ref_name]).await?;
Ok(())
})
.boxed()
}
} }
struct GitBinary { struct GitBinary {
@ -1574,7 +1597,9 @@ mod tests {
.await .await
.unwrap(); .unwrap();
repo.restore_checkpoint(checkpoint, cx.to_async()) // Ensure checkpoint stays alive even after a Git GC.
repo.gc(cx.to_async()).await.unwrap();
repo.restore_checkpoint(checkpoint.clone(), cx.to_async())
.await .await
.unwrap(); .unwrap();
@ -1595,6 +1620,15 @@ mod tests {
.ok(), .ok(),
None None
); );
// Garbage collecting after deleting a checkpoint makes it unreachable.
repo.delete_checkpoint(checkpoint.clone(), cx.to_async())
.await
.unwrap();
repo.gc(cx.to_async()).await.unwrap();
repo.restore_checkpoint(checkpoint.clone(), cx.to_async())
.await
.unwrap_err();
} }
#[gpui::test] #[gpui::test]
@ -1737,7 +1771,7 @@ mod tests {
let checkpoint2 = repo.checkpoint(cx.to_async()).await.unwrap(); let checkpoint2 = repo.checkpoint(cx.to_async()).await.unwrap();
assert!(!repo assert!(!repo
.compare_checkpoints(checkpoint1, checkpoint2, cx.to_async()) .compare_checkpoints(checkpoint1, checkpoint2.clone(), cx.to_async())
.await .await
.unwrap()); .unwrap());
@ -1774,4 +1808,21 @@ mod tests {
}] }]
) )
} }
impl RealGitRepository {
/// Force a Git garbage collection on the repository.
fn gc(&self, cx: AsyncApp) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
let executor = cx.background_executor().clone();
cx.background_spawn(async move {
let git_binary_path = git_binary_path.clone();
let working_directory = working_directory?;
let git = GitBinary::new(git_binary_path, working_directory, executor);
git.run(&["gc", "--prune=now"]).await?;
Ok(())
})
.boxed()
}
}
} }

View file

@ -549,8 +549,7 @@ impl GitStore {
} }
cx.background_executor().spawn(async move { cx.background_executor().spawn(async move {
let checkpoints: Vec<GitRepositoryCheckpoint> = let checkpoints = future::try_join_all(checkpoints).await?;
future::try_join_all(checkpoints).await?;
Ok(GitStoreCheckpoint { Ok(GitStoreCheckpoint {
checkpoints_by_dot_git_abs_path: dot_git_abs_paths checkpoints_by_dot_git_abs_path: dot_git_abs_paths
.into_iter() .into_iter()
@ -617,6 +616,26 @@ impl GitStore {
}) })
} }
pub fn delete_checkpoint(&self, checkpoint: GitStoreCheckpoint, cx: &App) -> Task<Result<()>> {
let repositories_by_dot_git_abs_path = self
.repositories
.values()
.map(|repo| (repo.read(cx).dot_git_abs_path.clone(), repo))
.collect::<HashMap<_, _>>();
let mut tasks = Vec::new();
for (dot_git_abs_path, checkpoint) in checkpoint.checkpoints_by_dot_git_abs_path {
if let Some(repository) = repositories_by_dot_git_abs_path.get(&dot_git_abs_path) {
let delete = repository.read(cx).delete_checkpoint(checkpoint);
tasks.push(async move { delete.await? });
}
}
cx.background_spawn(async move {
future::try_join_all(tasks).await?;
Ok(())
})
}
/// Blames a buffer. /// Blames a buffer.
pub fn blame_buffer( pub fn blame_buffer(
&self, &self,
@ -3319,6 +3338,20 @@ impl Repository {
} }
}) })
} }
pub fn delete_checkpoint(
&self,
checkpoint: GitRepositoryCheckpoint,
) -> oneshot::Receiver<Result<()>> {
self.send_job(move |repo, cx| async move {
match repo {
RepositoryState::Local(git_repository) => {
git_repository.delete_checkpoint(checkpoint, cx).await
}
RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
}
})
}
} }
fn get_permalink_in_rust_registry_src( fn get_permalink_in_rust_registry_src(