Cargo.lock 🔗
@@ -5594,6 +5594,7 @@ dependencies = [
"sum_tree",
"tempfile",
"text",
+ "thiserror 2.0.12",
"time",
"unindent",
"url",
Antonio Scandurra , Agus Zubiaga , Bennet Bo Fenner , and Danilo Leal created
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>
Cargo.lock | 1
crates/assistant2/src/active_thread.rs | 3
crates/assistant2/src/message_editor.rs | 9
crates/assistant2/src/thread.rs | 83 ++++++-
crates/assistant_eval/src/eval.rs | 2
crates/fs/src/fake_git_repo.rs | 9
crates/git/Cargo.toml | 1
crates/git/src/repository.rs | 301 +++++++++++++++++++-------
crates/project/src/git_store.rs | 53 ++++
9 files changed, 350 insertions(+), 112 deletions(-)
@@ -5594,6 +5594,7 @@ dependencies = [
"sum_tree",
"tempfile",
"text",
+ "thiserror 2.0.12",
"time",
"unindent",
"url",
@@ -1021,8 +1021,7 @@ impl ActiveThread {
.when(first_message, |parent| {
parent.child(self.render_rules_item(cx))
})
- .when(!first_message && checkpoint.is_some(), |parent| {
- let checkpoint = checkpoint.clone().unwrap();
+ .when_some(checkpoint, |parent, checkpoint| {
let mut is_pending = false;
let mut error = None;
if let Some(last_restore_checkpoint) =
@@ -12,7 +12,6 @@ use gpui::{
};
use language_model::LanguageModelRegistry;
use language_model_selector::ToggleModelSelector;
-use project::Project;
use rope::Point;
use settings::Settings;
use std::time::Duration;
@@ -21,7 +20,6 @@ use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
};
-use util::ResultExt;
use vim_mode_setting::VimModeSetting;
use workspace::Workspace;
@@ -39,7 +37,6 @@ pub struct MessageEditor {
editor: Entity<Editor>,
#[allow(dead_code)]
workspace: WeakEntity<Workspace>,
- project: Entity<Project>,
context_store: Entity<ContextStore>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
@@ -110,7 +107,6 @@ impl MessageEditor {
Self {
editor: editor.clone(),
- project: thread.read(cx).project().clone(),
thread,
workspace,
context_store,
@@ -209,8 +205,6 @@ impl MessageEditor {
let thread = self.thread.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| {
refresh_task.await;
let (system_prompt_context, load_error) = system_prompt_context_task.await;
@@ -222,11 +216,10 @@ impl MessageEditor {
}
})
.ok();
- let checkpoint = checkpoint.await.log_err();
thread
.update(cx, |thread, cx| {
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);
})
.ok();
@@ -1,5 +1,6 @@
use std::fmt::Write as _;
use std::io::Write;
+use std::mem;
use std::sync::Arc;
use anyhow::{Context as _, Result};
@@ -176,7 +177,7 @@ pub struct Thread {
context: BTreeMap<ContextId, ContextSnapshot>,
context_by_message: HashMap<MessageId, Vec<ContextId>>,
system_prompt_context: Option<AssistantSystemPromptContext>,
- checkpoints_by_message: HashMap<MessageId, GitStoreCheckpoint>,
+ checkpoints_by_message: HashMap<MessageId, ThreadCheckpoint>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
project: Entity<Project>,
@@ -185,6 +186,8 @@ pub struct Thread {
tool_use: ToolUseState,
action_log: Entity<ActionLog>,
last_restore_checkpoint: Option<LastRestoreCheckpoint>,
+ pending_checkpoint: Option<Task<Result<ThreadCheckpoint>>>,
+ checkpoint_on_next_user_message: bool,
scripting_session: Entity<ScriptingSession>,
scripting_tool_use: ToolUseState,
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
@@ -216,6 +219,8 @@ impl Thread {
prompt_builder,
tools: tools.clone(),
last_restore_checkpoint: None,
+ pending_checkpoint: None,
+ checkpoint_on_next_user_message: true,
tool_use: ToolUseState::new(tools.clone()),
scripting_session: cx.new(|cx| ScriptingSession::new(project.clone(), cx)),
scripting_tool_use: ToolUseState::new(tools),
@@ -287,6 +292,8 @@ impl Thread {
completion_count: 0,
pending_completions: Vec::new(),
last_restore_checkpoint: None,
+ pending_checkpoint: None,
+ checkpoint_on_next_user_message: true,
project,
prompt_builder,
tools,
@@ -348,11 +355,7 @@ impl Thread {
}
pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> {
- let checkpoint = self.checkpoints_by_message.get(&id).cloned()?;
- Some(ThreadCheckpoint {
- message_id: id,
- git_checkpoint: checkpoint,
- })
+ self.checkpoints_by_message.get(&id).cloned()
}
pub fn restore_checkpoint(
@@ -364,12 +367,13 @@ impl Thread {
message_id: checkpoint.message_id,
});
cx.emit(ThreadEvent::CheckpointChanged);
+ cx.notify();
let project = self.project.read(cx);
let restore = project
.git_store()
.read(cx)
- .restore_checkpoint(checkpoint.git_checkpoint, cx);
+ .restore_checkpoint(checkpoint.git_checkpoint.clone(), cx);
cx.spawn(async move |this, cx| {
let result = restore.await;
this.update(cx, |this, cx| {
@@ -379,15 +383,62 @@ impl Thread {
error: err.to_string(),
});
} else {
- this.last_restore_checkpoint = None;
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.notify();
})?;
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> {
self.last_restore_checkpoint.as_ref()
}
@@ -466,18 +517,18 @@ impl Thread {
&mut self,
text: impl Into<String>,
context: Vec<ContextSnapshot>,
- checkpoint: Option<GitStoreCheckpoint>,
cx: &mut Context<Self>,
) -> MessageId {
+ if mem::take(&mut self.checkpoint_on_next_user_message) {
+ self.checkpoint(cx);
+ }
+
let message_id =
self.insert_message(Role::User, vec![MessageSegment::Text(text.into())], cx);
let context_ids = context.iter().map(|context| context.id).collect::<Vec<_>>();
self.context
.extend(context.into_iter().map(|context| (context.id, context)));
self.context_by_message.insert(message_id, context_ids);
- if let Some(checkpoint) = checkpoint {
- self.checkpoints_by_message.insert(message_id, checkpoint);
- }
message_id
}
@@ -999,6 +1050,7 @@ impl Thread {
thread
.update(cx, |thread, cx| {
+ thread.checkpoint(cx);
match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
@@ -1267,7 +1319,6 @@ impl Thread {
// so for now we provide some text to keep the model on track.
"Here are the tool results.",
Vec::new(),
- None,
cx,
);
}
@@ -1276,7 +1327,7 @@ impl Thread {
///
/// Returns whether a completion was canceled.
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
} else {
let mut canceled = false;
@@ -1289,7 +1340,9 @@ impl Thread {
});
}
canceled
- }
+ };
+ self.checkpoint(cx);
+ canceled
}
/// Returns the feedback given to the thread, if any.
@@ -96,7 +96,7 @@ impl Eval {
assistant.update(cx, |assistant, cx| {
assistant.thread.update(cx, |thread, cx| {
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.send_to_model(model, RequestKind::Chat, cx);
});
@@ -420,4 +420,13 @@ impl GitRepository for FakeGitRepository {
) -> BoxFuture<Result<()>> {
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
sum_tree.workspace = true
text.workspace = true
+thiserror.workspace = true
time.workspace = true
url.workspace = true
util.workspace = true
@@ -5,14 +5,15 @@ use collections::HashMap;
use futures::future::BoxFuture;
use futures::{select_biased, AsyncWriteExt, FutureExt as _};
use git2::BranchType;
-use gpui::{AppContext, AsyncApp, SharedString};
+use gpui::{AppContext, AsyncApp, BackgroundExecutor, SharedString};
use parking_lot::Mutex;
use rope::Rope;
use schemars::JsonSchema;
use serde::Deserialize;
use std::borrow::Borrow;
+use std::future;
use std::path::Component;
-use std::process::Stdio;
+use std::process::{ExitStatus, Stdio};
use std::sync::LazyLock;
use std::{
cmp::Ordering,
@@ -20,6 +21,7 @@ use std::{
sync::Arc,
};
use sum_tree::MapSeekTarget;
+use thiserror::Error;
use util::command::new_smol_command;
use util::ResultExt;
use uuid::Uuid;
@@ -298,6 +300,14 @@ pub trait GitRepository: Send + Sync {
checkpoint: GitRepositoryCheckpoint,
cx: AsyncApp,
) -> 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 {
@@ -1049,62 +1059,36 @@ impl GitRepository for RealGitRepository {
let executor = cx.background_executor().clone();
cx.background_spawn(async move {
let working_directory = working_directory?;
- let index_file_path = working_directory.join(".git/index.tmp");
-
- let delete_temp_index = util::defer({
- let index_file_path = index_file_path.clone();
- || {
- executor
- .spawn(async move {
- smol::fs::remove_file(index_file_path).await.log_err();
- })
- .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())
+ let mut git = GitBinary::new(git_binary_path, working_directory, executor)
+ .envs(checkpoint_author_envs());
+ git.with_temp_index(async |git| {
+ let head_sha = git.run(&["rev-parse", "HEAD"]).await.ok();
+ git.run(&["add", "--all"]).await?;
+ let tree = git.run(&["write-tree"]).await?;
+ let checkpoint_sha = if let Some(head_sha) = head_sha.as_deref() {
+ git.run(&["commit-tree", &tree, "-p", head_sha, "-m", "Checkpoint"])
+ .await?
} else {
- let error = String::from_utf8_lossy(&output.stderr);
- Err(anyhow!("Git command failed: {:?}", error))
- }
- };
-
- let head_sha = run_git_command(&["rev-parse", "HEAD"]).await.ok();
- run_git_command(&["add", "--all"]).await?;
- let tree = run_git_command(&["write-tree"]).await?;
- let checkpoint_sha = if let Some(head_sha) = head_sha.as_deref() {
- run_git_command(&["commit-tree", &tree, "-p", head_sha, "-m", "Checkpoint"]).await?
- } else {
- run_git_command(&["commit-tree", &tree, "-m", "Checkpoint"]).await?
- };
- 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();
+ git.run(&["commit-tree", &tree, "-m", "Checkpoint"]).await?
+ };
+ let ref_name = Uuid::new_v4().to_string();
+ git.run(&[
+ "update-ref",
+ &format!("refs/zed/{ref_name}"),
+ &checkpoint_sha,
+ ])
+ .await?;
- Ok(GitRepositoryCheckpoint {
- head_sha: if let Some(head_sha) = head_sha {
- Some(head_sha.parse()?)
- } else {
- None
- },
- sha: checkpoint_sha.parse()?,
+ Ok(GitRepositoryCheckpoint {
+ head_sha: if let Some(head_sha) = head_sha {
+ Some(head_sha.parse()?)
+ } else {
+ None
+ },
+ sha: checkpoint_sha.parse()?,
+ })
})
+ .await
})
.boxed()
}
@@ -1116,50 +1100,165 @@ impl GitRepository for RealGitRepository {
) -> 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 index_file_path = working_directory.join(".git/index.tmp");
-
- let run_git_command = async |args: &[&str], use_temp_index: bool| {
- let mut command = new_smol_command(&git_binary_path);
- command.current_dir(&working_directory);
- command.args(args);
- if use_temp_index {
- command.env("GIT_INDEX_FILE", &index_file_path);
- }
- let output = command.output().await?;
- if output.status.success() {
- anyhow::Ok(String::from_utf8(output.stdout)?.trim_end().to_string())
- } else {
- let error = String::from_utf8_lossy(&output.stderr);
- Err(anyhow!("Git command failed: {:?}", error))
- }
- };
- run_git_command(
- &[
- "restore",
- "--source",
- &checkpoint.sha.to_string(),
- "--worktree",
- ".",
- ],
- false,
- )
+ let mut git = GitBinary::new(git_binary_path, working_directory, executor);
+ git.run(&[
+ "restore",
+ "--source",
+ &checkpoint.sha.to_string(),
+ "--worktree",
+ ".",
+ ])
+ .await?;
+
+ git.with_temp_index(async move |git| {
+ git.run(&["read-tree", &checkpoint.sha.to_string()]).await?;
+ git.run(&["clean", "-d", "--force"]).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 {
- run_git_command(&["reset", "--mixed", &head_sha.to_string()], false).await?;
+ git.run(&["reset", "--mixed", &head_sha.to_string()])
+ .await?;
} else {
- run_git_command(&["update-ref", "-d", "HEAD"], false).await?;
+ git.run(&["update-ref", "-d", "HEAD"]).await?;
}
Ok(())
})
.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(
@@ -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]
fn test_branches_parsing() {
// 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(
&self,
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(