Show "Restore Checkpoint" only when there were changes (#27243)
Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <hi@aguz.me> Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de> Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
parent
9d965bc98a
commit
e14ebcf267
9 changed files with 350 additions and 112 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -5594,6 +5594,7 @@ dependencies = [
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"text",
|
"text",
|
||||||
|
"thiserror 2.0.12",
|
||||||
"time",
|
"time",
|
||||||
"unindent",
|
"unindent",
|
||||||
"url",
|
"url",
|
||||||
|
|
|
@ -1021,8 +1021,7 @@ impl ActiveThread {
|
||||||
.when(first_message, |parent| {
|
.when(first_message, |parent| {
|
||||||
parent.child(self.render_rules_item(cx))
|
parent.child(self.render_rules_item(cx))
|
||||||
})
|
})
|
||||||
.when(!first_message && checkpoint.is_some(), |parent| {
|
.when_some(checkpoint, |parent, checkpoint| {
|
||||||
let checkpoint = checkpoint.clone().unwrap();
|
|
||||||
let mut is_pending = false;
|
let mut is_pending = false;
|
||||||
let mut error = None;
|
let mut error = None;
|
||||||
if let Some(last_restore_checkpoint) =
|
if let Some(last_restore_checkpoint) =
|
||||||
|
|
|
@ -12,7 +12,6 @@ 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;
|
||||||
|
@ -21,7 +20,6 @@ use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
|
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
|
||||||
use vim_mode_setting::VimModeSetting;
|
use vim_mode_setting::VimModeSetting;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -39,7 +37,6 @@ 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>,
|
||||||
|
@ -110,7 +107,6 @@ impl MessageEditor {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
editor: editor.clone(),
|
editor: editor.clone(),
|
||||||
project: thread.read(cx).project().clone(),
|
|
||||||
thread,
|
thread,
|
||||||
workspace,
|
workspace,
|
||||||
context_store,
|
context_store,
|
||||||
|
@ -209,8 +205,6 @@ 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 git_store = self.project.read(cx).git_store();
|
|
||||||
let checkpoint = git_store.read(cx).checkpoint(cx);
|
|
||||||
cx.spawn(async move |_, cx| {
|
cx.spawn(async move |_, cx| {
|
||||||
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;
|
||||||
|
@ -222,11 +216,10 @@ impl MessageEditor {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
let checkpoint = checkpoint.await.log_err();
|
|
||||||
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, checkpoint, cx);
|
thread.insert_user_message(user_message, context, cx);
|
||||||
thread.send_to_model(model, request_kind, cx);
|
thread.send_to_model(model, request_kind, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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};
|
||||||
|
@ -176,7 +177,7 @@ pub struct Thread {
|
||||||
context: BTreeMap<ContextId, ContextSnapshot>,
|
context: BTreeMap<ContextId, ContextSnapshot>,
|
||||||
context_by_message: HashMap<MessageId, Vec<ContextId>>,
|
context_by_message: HashMap<MessageId, Vec<ContextId>>,
|
||||||
system_prompt_context: Option<AssistantSystemPromptContext>,
|
system_prompt_context: Option<AssistantSystemPromptContext>,
|
||||||
checkpoints_by_message: HashMap<MessageId, GitStoreCheckpoint>,
|
checkpoints_by_message: HashMap<MessageId, ThreadCheckpoint>,
|
||||||
completion_count: usize,
|
completion_count: usize,
|
||||||
pending_completions: Vec<PendingCompletion>,
|
pending_completions: Vec<PendingCompletion>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
|
@ -185,6 +186,8 @@ 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>>>,
|
||||||
|
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>>>>,
|
||||||
|
@ -216,6 +219,8 @@ impl Thread {
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
tools: tools.clone(),
|
tools: tools.clone(),
|
||||||
last_restore_checkpoint: None,
|
last_restore_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),
|
||||||
|
@ -287,6 +292,8 @@ impl Thread {
|
||||||
completion_count: 0,
|
completion_count: 0,
|
||||||
pending_completions: Vec::new(),
|
pending_completions: Vec::new(),
|
||||||
last_restore_checkpoint: None,
|
last_restore_checkpoint: None,
|
||||||
|
pending_checkpoint: None,
|
||||||
|
checkpoint_on_next_user_message: true,
|
||||||
project,
|
project,
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
tools,
|
tools,
|
||||||
|
@ -348,11 +355,7 @@ impl Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> {
|
pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> {
|
||||||
let checkpoint = self.checkpoints_by_message.get(&id).cloned()?;
|
self.checkpoints_by_message.get(&id).cloned()
|
||||||
Some(ThreadCheckpoint {
|
|
||||||
message_id: id,
|
|
||||||
git_checkpoint: checkpoint,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore_checkpoint(
|
pub fn restore_checkpoint(
|
||||||
|
@ -364,12 +367,13 @@ impl Thread {
|
||||||
message_id: checkpoint.message_id,
|
message_id: checkpoint.message_id,
|
||||||
});
|
});
|
||||||
cx.emit(ThreadEvent::CheckpointChanged);
|
cx.emit(ThreadEvent::CheckpointChanged);
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
let restore = project
|
let restore = project
|
||||||
.git_store()
|
.git_store()
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.restore_checkpoint(checkpoint.git_checkpoint, cx);
|
.restore_checkpoint(checkpoint.git_checkpoint.clone(), cx);
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let result = restore.await;
|
let result = restore.await;
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
@ -379,15 +383,62 @@ impl Thread {
|
||||||
error: err.to_string(),
|
error: err.to_string(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.last_restore_checkpoint = None;
|
|
||||||
this.truncate(checkpoint.message_id, cx);
|
this.truncate(checkpoint.message_id, cx);
|
||||||
|
this.last_restore_checkpoint = None;
|
||||||
|
this.pending_checkpoint = Some(Task::ready(Ok(ThreadCheckpoint {
|
||||||
|
message_id: this.next_message_id,
|
||||||
|
git_checkpoint: checkpoint.git_checkpoint,
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
cx.emit(ThreadEvent::CheckpointChanged);
|
cx.emit(ThreadEvent::CheckpointChanged);
|
||||||
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
result
|
result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn checkpoint(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.is_generating() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let git_store = self.project.read(cx).git_store().clone();
|
||||||
|
let new_checkpoint = git_store.read(cx).checkpoint(cx);
|
||||||
|
let old_checkpoint = self.pending_checkpoint.take();
|
||||||
|
let next_user_message_id = self.next_message_id;
|
||||||
|
self.pending_checkpoint = Some(cx.spawn(async move |this, cx| {
|
||||||
|
let new_checkpoint = new_checkpoint.await?;
|
||||||
|
|
||||||
|
if let Some(old_checkpoint) = old_checkpoint {
|
||||||
|
if let Ok(old_checkpoint) = old_checkpoint.await {
|
||||||
|
let equal = git_store
|
||||||
|
.read_with(cx, |store, cx| {
|
||||||
|
store.compare_checkpoints(
|
||||||
|
old_checkpoint.git_checkpoint.clone(),
|
||||||
|
new_checkpoint.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if equal.ok() != Some(true) {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.checkpoints_by_message
|
||||||
|
.insert(old_checkpoint.message_id, old_checkpoint);
|
||||||
|
cx.emit(ThreadEvent::CheckpointChanged);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ThreadCheckpoint {
|
||||||
|
message_id: next_user_message_id,
|
||||||
|
git_checkpoint: new_checkpoint,
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn last_restore_checkpoint(&self) -> Option<&LastRestoreCheckpoint> {
|
pub fn last_restore_checkpoint(&self) -> Option<&LastRestoreCheckpoint> {
|
||||||
self.last_restore_checkpoint.as_ref()
|
self.last_restore_checkpoint.as_ref()
|
||||||
}
|
}
|
||||||
|
@ -466,18 +517,18 @@ impl Thread {
|
||||||
&mut self,
|
&mut self,
|
||||||
text: impl Into<String>,
|
text: impl Into<String>,
|
||||||
context: Vec<ContextSnapshot>,
|
context: Vec<ContextSnapshot>,
|
||||||
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(checkpoint) = checkpoint {
|
|
||||||
self.checkpoints_by_message.insert(message_id, checkpoint);
|
|
||||||
}
|
|
||||||
message_id
|
message_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -999,6 +1050,7 @@ impl Thread {
|
||||||
|
|
||||||
thread
|
thread
|
||||||
.update(cx, |thread, cx| {
|
.update(cx, |thread, cx| {
|
||||||
|
thread.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 => {
|
||||||
|
@ -1267,7 +1319,6 @@ 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1276,7 +1327,7 @@ impl Thread {
|
||||||
///
|
///
|
||||||
/// Returns whether a completion was canceled.
|
/// Returns whether a completion was canceled.
|
||||||
pub fn cancel_last_completion(&mut self, cx: &mut Context<Self>) -> bool {
|
pub fn cancel_last_completion(&mut self, cx: &mut Context<Self>) -> bool {
|
||||||
if self.pending_completions.pop().is_some() {
|
let canceled = if self.pending_completions.pop().is_some() {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let mut canceled = false;
|
let mut canceled = false;
|
||||||
|
@ -1289,7 +1340,9 @@ impl Thread {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
canceled
|
canceled
|
||||||
}
|
};
|
||||||
|
self.checkpoint(cx);
|
||||||
|
canceled
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the feedback given to the thread, if any.
|
/// Returns the feedback given to the thread, if any.
|
||||||
|
|
|
@ -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, None, cx);
|
thread.insert_user_message(self.user_prompt.clone(), context, 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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -420,4 +420,13 @@ impl GitRepository for FakeGitRepository {
|
||||||
) -> BoxFuture<Result<()>> {
|
) -> BoxFuture<Result<()>> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compare_checkpoints(
|
||||||
|
&self,
|
||||||
|
_left: GitRepositoryCheckpoint,
|
||||||
|
_right: GitRepositoryCheckpoint,
|
||||||
|
_cx: AsyncApp,
|
||||||
|
) -> BoxFuture<Result<bool>> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ serde.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
sum_tree.workspace = true
|
sum_tree.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
|
|
@ -5,14 +5,15 @@ use collections::HashMap;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use futures::{select_biased, AsyncWriteExt, FutureExt as _};
|
use futures::{select_biased, AsyncWriteExt, FutureExt as _};
|
||||||
use git2::BranchType;
|
use git2::BranchType;
|
||||||
use gpui::{AppContext, AsyncApp, SharedString};
|
use gpui::{AppContext, AsyncApp, BackgroundExecutor, SharedString};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
|
use std::future;
|
||||||
use std::path::Component;
|
use std::path::Component;
|
||||||
use std::process::Stdio;
|
use std::process::{ExitStatus, Stdio};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
|
@ -20,6 +21,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use sum_tree::MapSeekTarget;
|
use sum_tree::MapSeekTarget;
|
||||||
|
use thiserror::Error;
|
||||||
use util::command::new_smol_command;
|
use util::command::new_smol_command;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -298,6 +300,14 @@ pub trait GitRepository: Send + Sync {
|
||||||
checkpoint: GitRepositoryCheckpoint,
|
checkpoint: GitRepositoryCheckpoint,
|
||||||
cx: AsyncApp,
|
cx: AsyncApp,
|
||||||
) -> BoxFuture<Result<()>>;
|
) -> BoxFuture<Result<()>>;
|
||||||
|
|
||||||
|
/// Compares two checkpoints, returning true if they are equal
|
||||||
|
fn compare_checkpoints(
|
||||||
|
&self,
|
||||||
|
left: GitRepositoryCheckpoint,
|
||||||
|
right: GitRepositoryCheckpoint,
|
||||||
|
cx: AsyncApp,
|
||||||
|
) -> BoxFuture<Result<bool>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum DiffType {
|
pub enum DiffType {
|
||||||
|
@ -1049,62 +1059,36 @@ impl GitRepository for RealGitRepository {
|
||||||
let executor = cx.background_executor().clone();
|
let executor = cx.background_executor().clone();
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let working_directory = working_directory?;
|
let working_directory = working_directory?;
|
||||||
let index_file_path = working_directory.join(".git/index.tmp");
|
let mut git = GitBinary::new(git_binary_path, working_directory, executor)
|
||||||
|
.envs(checkpoint_author_envs());
|
||||||
let delete_temp_index = util::defer({
|
git.with_temp_index(async |git| {
|
||||||
let index_file_path = index_file_path.clone();
|
let head_sha = git.run(&["rev-parse", "HEAD"]).await.ok();
|
||||||
|| {
|
git.run(&["add", "--all"]).await?;
|
||||||
executor
|
let tree = git.run(&["write-tree"]).await?;
|
||||||
.spawn(async move {
|
let checkpoint_sha = if let Some(head_sha) = head_sha.as_deref() {
|
||||||
smol::fs::remove_file(index_file_path).await.log_err();
|
git.run(&["commit-tree", &tree, "-p", head_sha, "-m", "Checkpoint"])
|
||||||
})
|
.await?
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let run_git_command = async |args: &[&str]| {
|
|
||||||
let output = new_smol_command(&git_binary_path)
|
|
||||||
.current_dir(&working_directory)
|
|
||||||
.env("GIT_INDEX_FILE", &index_file_path)
|
|
||||||
.envs(checkpoint_author_envs())
|
|
||||||
.args(args)
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
if output.status.success() {
|
|
||||||
anyhow::Ok(String::from_utf8(output.stdout)?.trim_end().to_string())
|
|
||||||
} else {
|
} else {
|
||||||
let error = String::from_utf8_lossy(&output.stderr);
|
git.run(&["commit-tree", &tree, "-m", "Checkpoint"]).await?
|
||||||
Err(anyhow!("Git command failed: {:?}", error))
|
};
|
||||||
}
|
let ref_name = Uuid::new_v4().to_string();
|
||||||
};
|
git.run(&[
|
||||||
|
"update-ref",
|
||||||
|
&format!("refs/zed/{ref_name}"),
|
||||||
|
&checkpoint_sha,
|
||||||
|
])
|
||||||
|
.await?;
|
||||||
|
|
||||||
let head_sha = run_git_command(&["rev-parse", "HEAD"]).await.ok();
|
Ok(GitRepositoryCheckpoint {
|
||||||
run_git_command(&["add", "--all"]).await?;
|
head_sha: if let Some(head_sha) = head_sha {
|
||||||
let tree = run_git_command(&["write-tree"]).await?;
|
Some(head_sha.parse()?)
|
||||||
let checkpoint_sha = if let Some(head_sha) = head_sha.as_deref() {
|
} else {
|
||||||
run_git_command(&["commit-tree", &tree, "-p", head_sha, "-m", "Checkpoint"]).await?
|
None
|
||||||
} else {
|
},
|
||||||
run_git_command(&["commit-tree", &tree, "-m", "Checkpoint"]).await?
|
sha: checkpoint_sha.parse()?,
|
||||||
};
|
})
|
||||||
let ref_name = Uuid::new_v4().to_string();
|
|
||||||
run_git_command(&[
|
|
||||||
"update-ref",
|
|
||||||
&format!("refs/zed/{ref_name}"),
|
|
||||||
&checkpoint_sha,
|
|
||||||
])
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
smol::fs::remove_file(index_file_path).await.ok();
|
|
||||||
delete_temp_index.abort();
|
|
||||||
|
|
||||||
Ok(GitRepositoryCheckpoint {
|
|
||||||
head_sha: if let Some(head_sha) = head_sha {
|
|
||||||
Some(head_sha.parse()?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
sha: checkpoint_sha.parse()?,
|
|
||||||
})
|
})
|
||||||
|
.await
|
||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
@ -1116,50 +1100,165 @@ impl GitRepository for RealGitRepository {
|
||||||
) -> BoxFuture<Result<()>> {
|
) -> BoxFuture<Result<()>> {
|
||||||
let working_directory = self.working_directory();
|
let working_directory = self.working_directory();
|
||||||
let git_binary_path = self.git_binary_path.clone();
|
let git_binary_path = self.git_binary_path.clone();
|
||||||
|
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let working_directory = working_directory?;
|
let working_directory = working_directory?;
|
||||||
let index_file_path = working_directory.join(".git/index.tmp");
|
|
||||||
|
|
||||||
let run_git_command = async |args: &[&str], use_temp_index: bool| {
|
let mut git = GitBinary::new(git_binary_path, working_directory, executor);
|
||||||
let mut command = new_smol_command(&git_binary_path);
|
git.run(&[
|
||||||
command.current_dir(&working_directory);
|
"restore",
|
||||||
command.args(args);
|
"--source",
|
||||||
if use_temp_index {
|
&checkpoint.sha.to_string(),
|
||||||
command.env("GIT_INDEX_FILE", &index_file_path);
|
"--worktree",
|
||||||
}
|
".",
|
||||||
let output = command.output().await?;
|
])
|
||||||
if output.status.success() {
|
.await?;
|
||||||
anyhow::Ok(String::from_utf8(output.stdout)?.trim_end().to_string())
|
|
||||||
} else {
|
git.with_temp_index(async move |git| {
|
||||||
let error = String::from_utf8_lossy(&output.stderr);
|
git.run(&["read-tree", &checkpoint.sha.to_string()]).await?;
|
||||||
Err(anyhow!("Git command failed: {:?}", error))
|
git.run(&["clean", "-d", "--force"]).await
|
||||||
}
|
})
|
||||||
};
|
|
||||||
|
|
||||||
run_git_command(
|
|
||||||
&[
|
|
||||||
"restore",
|
|
||||||
"--source",
|
|
||||||
&checkpoint.sha.to_string(),
|
|
||||||
"--worktree",
|
|
||||||
".",
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
run_git_command(&["read-tree", &checkpoint.sha.to_string()], true).await?;
|
|
||||||
run_git_command(&["clean", "-d", "--force"], true).await?;
|
|
||||||
|
|
||||||
if let Some(head_sha) = checkpoint.head_sha {
|
if let Some(head_sha) = checkpoint.head_sha {
|
||||||
run_git_command(&["reset", "--mixed", &head_sha.to_string()], false).await?;
|
git.run(&["reset", "--mixed", &head_sha.to_string()])
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
run_git_command(&["update-ref", "-d", "HEAD"], false).await?;
|
git.run(&["update-ref", "-d", "HEAD"]).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compare_checkpoints(
|
||||||
|
&self,
|
||||||
|
left: GitRepositoryCheckpoint,
|
||||||
|
right: GitRepositoryCheckpoint,
|
||||||
|
cx: AsyncApp,
|
||||||
|
) -> BoxFuture<Result<bool>> {
|
||||||
|
if left.head_sha != right.head_sha {
|
||||||
|
return future::ready(Ok(false)).boxed();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
let result = git
|
||||||
|
.run(&[
|
||||||
|
"diff-tree",
|
||||||
|
"--quiet",
|
||||||
|
&left.sha.to_string(),
|
||||||
|
&right.sha.to_string(),
|
||||||
|
])
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(error) => {
|
||||||
|
if let Some(GitBinaryCommandError { status, .. }) =
|
||||||
|
error.downcast_ref::<GitBinaryCommandError>()
|
||||||
|
{
|
||||||
|
if status.code() == Some(1) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GitBinary {
|
||||||
|
git_binary_path: PathBuf,
|
||||||
|
working_directory: PathBuf,
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
index_file_path: Option<PathBuf>,
|
||||||
|
envs: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitBinary {
|
||||||
|
fn new(
|
||||||
|
git_binary_path: PathBuf,
|
||||||
|
working_directory: PathBuf,
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
git_binary_path,
|
||||||
|
working_directory,
|
||||||
|
executor,
|
||||||
|
index_file_path: None,
|
||||||
|
envs: HashMap::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn envs(mut self, envs: HashMap<String, String>) -> Self {
|
||||||
|
self.envs = envs;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn with_temp_index<R>(
|
||||||
|
&mut self,
|
||||||
|
f: impl AsyncFnOnce(&Self) -> Result<R>,
|
||||||
|
) -> Result<R> {
|
||||||
|
let index_file_path = self.working_directory.join(".git/index.tmp");
|
||||||
|
|
||||||
|
let delete_temp_index = util::defer({
|
||||||
|
let index_file_path = index_file_path.clone();
|
||||||
|
let executor = self.executor.clone();
|
||||||
|
move || {
|
||||||
|
executor
|
||||||
|
.spawn(async move {
|
||||||
|
smol::fs::remove_file(index_file_path).await.log_err();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.index_file_path = Some(index_file_path.clone());
|
||||||
|
let result = f(self).await;
|
||||||
|
self.index_file_path = None;
|
||||||
|
let result = result?;
|
||||||
|
|
||||||
|
smol::fs::remove_file(index_file_path).await.ok();
|
||||||
|
delete_temp_index.abort();
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self, args: &[&str]) -> Result<String> {
|
||||||
|
let mut command = new_smol_command(&self.git_binary_path);
|
||||||
|
command.current_dir(&self.working_directory);
|
||||||
|
command.args(args);
|
||||||
|
if let Some(index_file_path) = self.index_file_path.as_ref() {
|
||||||
|
command.env("GIT_INDEX_FILE", index_file_path);
|
||||||
|
}
|
||||||
|
command.envs(&self.envs);
|
||||||
|
let output = command.output().await?;
|
||||||
|
if output.status.success() {
|
||||||
|
anyhow::Ok(String::from_utf8(output.stdout)?.trim_end().to_string())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(GitBinaryCommandError {
|
||||||
|
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||||
|
status: output.status,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
#[error("Git command failed: {stdout}")]
|
||||||
|
struct GitBinaryCommandError {
|
||||||
|
stdout: String,
|
||||||
|
status: ExitStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_remote_command(
|
async fn run_remote_command(
|
||||||
|
@ -1619,6 +1718,36 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_compare_checkpoints(cx: &mut TestAppContext) {
|
||||||
|
cx.executor().allow_parking();
|
||||||
|
|
||||||
|
let repo_dir = tempfile::tempdir().unwrap();
|
||||||
|
git2::Repository::init(repo_dir.path()).unwrap();
|
||||||
|
let repo = RealGitRepository::new(&repo_dir.path().join(".git"), None).unwrap();
|
||||||
|
|
||||||
|
smol::fs::write(repo_dir.path().join("file1"), "content1")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let checkpoint1 = repo.checkpoint(cx.to_async()).await.unwrap();
|
||||||
|
|
||||||
|
smol::fs::write(repo_dir.path().join("file2"), "content2")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let checkpoint2 = repo.checkpoint(cx.to_async()).await.unwrap();
|
||||||
|
|
||||||
|
assert!(!repo
|
||||||
|
.compare_checkpoints(checkpoint1, checkpoint2, cx.to_async())
|
||||||
|
.await
|
||||||
|
.unwrap());
|
||||||
|
|
||||||
|
let checkpoint3 = repo.checkpoint(cx.to_async()).await.unwrap();
|
||||||
|
assert!(repo
|
||||||
|
.compare_checkpoints(checkpoint2, checkpoint3, cx.to_async())
|
||||||
|
.await
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_branches_parsing() {
|
fn test_branches_parsing() {
|
||||||
// suppress "help: octal escapes are not supported, `\0` is always null"
|
// suppress "help: octal escapes are not supported, `\0` is always null"
|
||||||
|
|
|
@ -580,6 +580,44 @@ impl GitStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compares two checkpoints, returning true if they are equal.
|
||||||
|
pub fn compare_checkpoints(
|
||||||
|
&self,
|
||||||
|
left: GitStoreCheckpoint,
|
||||||
|
mut right: GitStoreCheckpoint,
|
||||||
|
cx: &App,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
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, left_checkpoint) in left.checkpoints_by_dot_git_abs_path {
|
||||||
|
if let Some(right_checkpoint) = right
|
||||||
|
.checkpoints_by_dot_git_abs_path
|
||||||
|
.remove(&dot_git_abs_path)
|
||||||
|
{
|
||||||
|
if let Some(repository) = repositories_by_dot_git_abs_path.get(&dot_git_abs_path) {
|
||||||
|
let compare = repository
|
||||||
|
.read(cx)
|
||||||
|
.compare_checkpoints(left_checkpoint, right_checkpoint);
|
||||||
|
tasks.push(async move { compare.await? });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Task::ready(Ok(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
Ok(future::try_join_all(tasks)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.all(|result| result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blames a buffer.
|
||||||
pub fn blame_buffer(
|
pub fn blame_buffer(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Entity<Buffer>,
|
buffer: &Entity<Buffer>,
|
||||||
|
@ -3266,6 +3304,21 @@ impl Repository {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn compare_checkpoints(
|
||||||
|
&self,
|
||||||
|
left: GitRepositoryCheckpoint,
|
||||||
|
right: GitRepositoryCheckpoint,
|
||||||
|
) -> oneshot::Receiver<Result<bool>> {
|
||||||
|
self.send_job(move |repo, cx| async move {
|
||||||
|
match repo {
|
||||||
|
RepositoryState::Local(git_repository) => {
|
||||||
|
git_repository.compare_checkpoints(left, right, cx).await
|
||||||
|
}
|
||||||
|
RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_permalink_in_rust_registry_src(
|
fn get_permalink_in_rust_registry_src(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue