Compare commits
25 Commits
debug-shel
...
review-onl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a69335491 | ||
|
|
26e461b4d4 | ||
|
|
93e3780ffc | ||
|
|
e45151b893 | ||
|
|
7d6cc2e028 | ||
|
|
c06d004fce | ||
|
|
137485819a | ||
|
|
fd17b4486c | ||
|
|
af0011b9bd | ||
|
|
65d687458c | ||
|
|
b0568d24ed | ||
|
|
53227bb847 | ||
|
|
cc76992a55 | ||
|
|
365817370a | ||
|
|
52aaa6c561 | ||
|
|
36ba2ac238 | ||
|
|
807b261403 | ||
|
|
f83a11741c | ||
|
|
2b84d34591 | ||
|
|
9984694ca7 | ||
|
|
a9744d6c00 | ||
|
|
4d13db41b3 | ||
|
|
05093d988b | ||
|
|
933048e867 | ||
|
|
99ba285738 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -453,6 +453,7 @@ dependencies = [
|
||||
"assistant_slash_command",
|
||||
"assistant_tool",
|
||||
"async-watch",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
|
||||
@@ -25,6 +25,7 @@ assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
@@ -85,6 +86,7 @@ workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -521,6 +521,10 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
ThreadEvent::CheckpointChanged => cx.notify(),
|
||||
ThreadEvent::DiffChanged => {
|
||||
// todo!("update list of changed files")
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ mod profile_selector;
|
||||
mod terminal_codegen;
|
||||
mod terminal_inline_assistant;
|
||||
mod thread;
|
||||
mod thread_diff;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod tool_use;
|
||||
@@ -36,6 +37,7 @@ use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal}
|
||||
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
|
||||
pub(crate) use crate::thread_diff::*;
|
||||
pub use crate::thread_store::ThreadStore;
|
||||
|
||||
actions!(
|
||||
|
||||
@@ -317,7 +317,7 @@ impl Render for MessageEditor {
|
||||
|
||||
let project = self.thread.read(cx).project();
|
||||
let changed_files = if let Some(repository) = project.read(cx).active_repository(cx) {
|
||||
repository.read(cx).status().count()
|
||||
repository.read(cx).cached_status().count()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ use crate::thread_store::{
|
||||
SerializedToolUse,
|
||||
};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState};
|
||||
use crate::{ChangeAuthor, ThreadDiff};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
@@ -190,6 +191,7 @@ pub struct Thread {
|
||||
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
|
||||
cumulative_token_usage: TokenUsage,
|
||||
feedback: Option<ThreadFeedback>,
|
||||
diff: Entity<ThreadDiff>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -220,13 +222,14 @@ impl Thread {
|
||||
tool_use: ToolUseState::new(tools.clone()),
|
||||
action_log: cx.new(|_| ActionLog::new()),
|
||||
initial_project_snapshot: {
|
||||
let project_snapshot = Self::project_snapshot(project, cx);
|
||||
let project_snapshot = Self::project_snapshot(project.clone(), cx);
|
||||
cx.foreground_executor()
|
||||
.spawn(async move { Some(project_snapshot.await) })
|
||||
.shared()
|
||||
},
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
feedback: None,
|
||||
diff: cx.new(|cx| ThreadDiff::new(project.clone(), cx)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +283,7 @@ impl Thread {
|
||||
pending_completions: Vec::new(),
|
||||
last_restore_checkpoint: None,
|
||||
pending_checkpoint: None,
|
||||
diff: cx.new(|cx| ThreadDiff::new(project.clone(), cx)),
|
||||
project,
|
||||
prompt_builder,
|
||||
tools,
|
||||
@@ -1231,6 +1235,9 @@ impl Thread {
|
||||
tool: Arc<dyn Tool>,
|
||||
cx: &mut Context<Thread>,
|
||||
) -> Task<()> {
|
||||
self.diff
|
||||
.update(cx, |diff, cx| diff.compute_changes(ChangeAuthor::User, cx));
|
||||
|
||||
let run_tool = tool.run(
|
||||
input,
|
||||
messages,
|
||||
@@ -1245,6 +1252,10 @@ impl Thread {
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread
|
||||
.diff
|
||||
.update(cx, |diff, cx| diff.compute_changes(ChangeAuthor::Agent, cx));
|
||||
|
||||
let pending_tool_use = thread
|
||||
.tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), output);
|
||||
@@ -1562,6 +1573,7 @@ pub enum ThreadEvent {
|
||||
canceled: bool,
|
||||
},
|
||||
CheckpointChanged,
|
||||
DiffChanged,
|
||||
ToolConfirmationNeeded,
|
||||
}
|
||||
|
||||
|
||||
125
crates/assistant2/src/thread_diff.rs
Normal file
125
crates/assistant2/src/thread_diff.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use anyhow::Result;
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashMap;
|
||||
use futures::{future::Shared, FutureExt};
|
||||
use gpui::{prelude::*, App, Entity, Task};
|
||||
use language::Buffer;
|
||||
use project::{
|
||||
git_store::{GitStore, GitStoreCheckpoint, GitStoreIndex, GitStoreStatus},
|
||||
Project,
|
||||
};
|
||||
use util::TryFutureExt;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ChangeAuthor {
|
||||
User,
|
||||
Agent,
|
||||
}
|
||||
|
||||
pub struct ThreadDiff {
|
||||
base: Shared<Task<Option<GitStoreIndex>>>,
|
||||
diffs_by_buffer: HashMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
last_checkpoint: Option<Task<Result<GitStoreCheckpoint>>>,
|
||||
project: Entity<Project>,
|
||||
git_store: Entity<GitStore>,
|
||||
}
|
||||
|
||||
impl ThreadDiff {
|
||||
pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
|
||||
let mut this = Self {
|
||||
base: cx
|
||||
.background_spawn(
|
||||
project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.create_index(cx)
|
||||
.log_err(),
|
||||
)
|
||||
.shared(),
|
||||
diffs_by_buffer: HashMap::default(),
|
||||
last_checkpoint: None,
|
||||
git_store: project.read(cx).git_store().clone(),
|
||||
project,
|
||||
};
|
||||
this.compute_changes(ChangeAuthor::User, cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn compute_changes(&mut self, author: ChangeAuthor, cx: &mut Context<Self>) {
|
||||
let last_checkpoint = self.last_checkpoint.take();
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
let checkpoint = git_store.read(cx).checkpoint(cx);
|
||||
let base = self.base.clone();
|
||||
self.last_checkpoint = Some(cx.spawn(async move |this, cx| {
|
||||
let checkpoint = checkpoint.await?;
|
||||
|
||||
if let Some(base) = base.await {
|
||||
if let Some(last_checkpoint) = last_checkpoint {
|
||||
if let Ok(last_checkpoint) = last_checkpoint.await {
|
||||
if author == ChangeAuthor::User {
|
||||
let diff = git_store
|
||||
.read_with(cx, |store, cx| {
|
||||
store.diff_checkpoints(last_checkpoint, checkpoint.clone(), cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
if let Ok(diff) = diff {
|
||||
_ = git_store
|
||||
.read_with(cx, |store, cx| {
|
||||
store.apply_diff(base.clone(), diff, cx)
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status = git_store
|
||||
.read_with(cx, |store, cx| store.status(Some(base), cx))?
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
this.update(cx, |this, cx| this.set_status(status, cx))?;
|
||||
}
|
||||
|
||||
Ok(checkpoint)
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: GitStoreStatus, cx: &mut Context<Self>) {}
|
||||
}
|
||||
|
||||
struct ThreadDiffSource {
|
||||
thread_diff: Entity<ThreadDiff>,
|
||||
git_store: Entity<GitStore>,
|
||||
}
|
||||
|
||||
impl git_ui::project_diff::DiffSource for ThreadDiff {
|
||||
fn status(&self, cx: &App) -> Vec<(project::ProjectPath, git::status::FileStatus, bool)> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
todo!();
|
||||
// for (repo, repo_path, change) in self.changes.iter(&self.git_store, cx) {
|
||||
// let Some(project_path) = repo.read(cx).repo_path_to_project_path(repo_path) else {
|
||||
// continue;
|
||||
// };
|
||||
|
||||
// results.push((
|
||||
// project_path,
|
||||
// // todo!("compute the correct status")
|
||||
// git::status::FileStatus::worktree(git::status::StatusCode::Modified),
|
||||
// false,
|
||||
// ))
|
||||
// }
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
fn open_uncommitted_diff(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<BufferDiff>>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ use futures::future::{self, BoxFuture};
|
||||
use git::{
|
||||
blame::Blame,
|
||||
repository::{
|
||||
AskPassSession, Branch, CommitDetails, GitRepository, GitRepositoryCheckpoint, PushOptions,
|
||||
Remote, RepoPath, ResetMode,
|
||||
AskPassSession, Branch, CommitDetails, GitIndex, GitRepository, GitRepositoryCheckpoint,
|
||||
PushOptions, Remote, RepoPath, ResetMode,
|
||||
},
|
||||
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
|
||||
};
|
||||
@@ -81,7 +81,11 @@ impl FakeGitRepository {
|
||||
impl GitRepository for FakeGitRepository {
|
||||
fn reload_index(&self) {}
|
||||
|
||||
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
|
||||
fn load_index_text(
|
||||
&self,
|
||||
index: Option<GitIndex>,
|
||||
path: RepoPath,
|
||||
) -> BoxFuture<Option<String>> {
|
||||
async {
|
||||
self.with_state_async(false, move |state| {
|
||||
state
|
||||
@@ -171,7 +175,11 @@ impl GitRepository for FakeGitRepository {
|
||||
self.path()
|
||||
}
|
||||
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'static, Result<GitStatus>> {
|
||||
fn status(
|
||||
&self,
|
||||
index: Option<GitIndex>,
|
||||
path_prefixes: &[RepoPath],
|
||||
) -> BoxFuture<'static, Result<GitStatus>> {
|
||||
let status = self.status_blocking(path_prefixes);
|
||||
async move { status }.boxed()
|
||||
}
|
||||
@@ -414,7 +422,7 @@ impl GitRepository for FakeGitRepository {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn checkpoint(&self) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
|
||||
fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -433,4 +441,20 @@ impl GitRepository for FakeGitRepository {
|
||||
fn delete_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn diff_checkpoints(
|
||||
&self,
|
||||
_base_checkpoint: GitRepositoryCheckpoint,
|
||||
_target_checkpoint: GitRepositoryCheckpoint,
|
||||
) -> BoxFuture<Result<String>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn create_index(&self) -> BoxFuture<Result<GitIndex>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn apply_diff(&self, _index: GitIndex, _diff: String) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::future;
|
||||
use std::path::Component;
|
||||
use std::process::{ExitStatus, Stdio};
|
||||
use std::sync::LazyLock;
|
||||
@@ -21,6 +20,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{future, mem};
|
||||
use sum_tree::MapSeekTarget;
|
||||
use thiserror::Error;
|
||||
use util::command::{new_smol_command, new_std_command};
|
||||
@@ -161,7 +161,8 @@ pub trait GitRepository: Send + Sync {
|
||||
/// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
|
||||
///
|
||||
/// Also returns `None` for symlinks.
|
||||
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>>;
|
||||
fn load_index_text(&self, index: Option<GitIndex>, path: RepoPath)
|
||||
-> BoxFuture<Option<String>>;
|
||||
|
||||
/// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path.
|
||||
///
|
||||
@@ -183,7 +184,11 @@ pub trait GitRepository: Send + Sync {
|
||||
|
||||
fn merge_head_shas(&self) -> Vec<String>;
|
||||
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'static, Result<GitStatus>>;
|
||||
fn status(
|
||||
&self,
|
||||
index: Option<GitIndex>,
|
||||
path_prefixes: &[RepoPath],
|
||||
) -> BoxFuture<'static, Result<GitStatus>>;
|
||||
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
|
||||
|
||||
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>;
|
||||
@@ -286,7 +291,7 @@ pub trait GitRepository: Send + Sync {
|
||||
fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>>;
|
||||
|
||||
/// Creates a checkpoint for the repository.
|
||||
fn checkpoint(&self) -> BoxFuture<Result<GitRepositoryCheckpoint>>;
|
||||
fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>>;
|
||||
|
||||
/// Resets to a previously-created checkpoint.
|
||||
fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>;
|
||||
@@ -300,6 +305,17 @@ pub trait GitRepository: Send + Sync {
|
||||
|
||||
/// Deletes a previously-created checkpoint.
|
||||
fn delete_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>;
|
||||
|
||||
/// Computes a diff between two checkpoints.
|
||||
fn diff_checkpoints(
|
||||
&self,
|
||||
base_checkpoint: GitRepositoryCheckpoint,
|
||||
target_checkpoint: GitRepositoryCheckpoint,
|
||||
) -> BoxFuture<Result<String>>;
|
||||
|
||||
fn create_index(&self) -> BoxFuture<Result<GitIndex>>;
|
||||
|
||||
fn apply_diff(&self, index: GitIndex, diff: String) -> BoxFuture<Result<()>>;
|
||||
}
|
||||
|
||||
pub enum DiffType {
|
||||
@@ -356,8 +372,10 @@ pub struct GitRepositoryCheckpoint {
|
||||
commit_sha: Oid,
|
||||
}
|
||||
|
||||
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
||||
const GIT_MODE_SYMLINK: u32 = 0o120000;
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct GitIndex {
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
impl GitRepository for RealGitRepository {
|
||||
fn reload_index(&self) {
|
||||
@@ -464,31 +482,82 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
|
||||
let repo = self.repository.clone();
|
||||
fn load_index_text(
|
||||
&self,
|
||||
index: Option<GitIndex>,
|
||||
path: RepoPath,
|
||||
) -> BoxFuture<Option<String>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
let executor = self.executor.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
|
||||
// This check is required because index.get_path() unwraps internally :(
|
||||
check_path_to_repo_path_errors(path)?;
|
||||
|
||||
let mut index = repo.index()?;
|
||||
index.read(false)?;
|
||||
|
||||
const STAGE_NORMAL: i32 = 0;
|
||||
let oid = match index.get_path(path, STAGE_NORMAL) {
|
||||
Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
let content = repo.find_blob(oid)?.content().to_owned();
|
||||
Ok(Some(String::from_utf8(content)?))
|
||||
match check_path_to_repo_path_errors(&path) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
log::error!("Error with repo path: {:?}", err);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
match logic(&repo.lock(), &path) {
|
||||
Ok(value) => return value,
|
||||
Err(err) => log::error!("Error loading index text: {:?}", err),
|
||||
|
||||
let working_directory = match working_directory {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => {
|
||||
log::error!("Error getting working directory: {:?}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let mut git = GitBinary::new(git_binary_path, working_directory, executor);
|
||||
let text = git
|
||||
.with_option_index(index, async |git| {
|
||||
// First check if the file is a symlink using ls-files
|
||||
let ls_files_output = git
|
||||
.run(&[
|
||||
OsStr::new("ls-files"),
|
||||
OsStr::new("--stage"),
|
||||
path.to_unix_style().as_ref(),
|
||||
])
|
||||
.await
|
||||
.context("error running ls-files")?;
|
||||
|
||||
// Parse ls-files output to check if it's a symlink
|
||||
// Format is: "100644 <sha> 0 <filename>" where 100644 is the mode
|
||||
if ls_files_output.is_empty() {
|
||||
return Ok(None); // File not in index
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = ls_files_output.split_whitespace().collect();
|
||||
if parts.len() < 2 {
|
||||
return Err(anyhow!(
|
||||
"unexpected ls-files output format: {}",
|
||||
ls_files_output
|
||||
));
|
||||
}
|
||||
|
||||
// Check if it's a symlink (120000 mode)
|
||||
if parts[0] == "120000" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let sha = parts[1];
|
||||
|
||||
// Now get the content
|
||||
Ok(Some(
|
||||
git.run_raw(&["cat-file", "blob", sha])
|
||||
.await
|
||||
.context("error getting blob content")?,
|
||||
))
|
||||
})
|
||||
.await;
|
||||
|
||||
match text {
|
||||
Ok(text) => text,
|
||||
Err(error) => {
|
||||
log::error!("Error getting text: {}", error);
|
||||
None
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
@@ -607,16 +676,36 @@ impl GitRepository for RealGitRepository {
|
||||
shas
|
||||
}
|
||||
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'static, Result<GitStatus>> {
|
||||
fn status(
|
||||
&self,
|
||||
index: Option<GitIndex>,
|
||||
path_prefixes: &[RepoPath],
|
||||
) -> BoxFuture<'static, Result<GitStatus>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
let executor = self.executor.clone();
|
||||
let args = git_status_args(path_prefixes);
|
||||
let mut args = vec![
|
||||
OsString::from("--no-optional-locks"),
|
||||
OsString::from("status"),
|
||||
OsString::from("--porcelain=v1"),
|
||||
OsString::from("--untracked-files=all"),
|
||||
OsString::from("--no-renames"),
|
||||
OsString::from("-z"),
|
||||
];
|
||||
args.extend(path_prefixes.iter().map(|path_prefix| {
|
||||
if path_prefix.0.as_ref() == Path::new("") {
|
||||
Path::new(".").into()
|
||||
} else {
|
||||
path_prefix.as_os_str().into()
|
||||
}
|
||||
}));
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let working_directory = working_directory?;
|
||||
let git = GitBinary::new(git_binary_path, working_directory, executor);
|
||||
git.run(&args).await?.parse()
|
||||
let mut git = GitBinary::new(git_binary_path, working_directory, executor);
|
||||
git.with_option_index(index, async |git| git.run(&args).await)
|
||||
.await?
|
||||
.parse()
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
@@ -1071,7 +1160,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn checkpoint(&self) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
|
||||
fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
let executor = self.executor.clone();
|
||||
@@ -1203,6 +1292,66 @@ impl GitRepository for RealGitRepository {
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn diff_checkpoints(
|
||||
&self,
|
||||
base_checkpoint: GitRepositoryCheckpoint,
|
||||
target_checkpoint: GitRepositoryCheckpoint,
|
||||
) -> BoxFuture<Result<String>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
||||
let executor = self.executor.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let working_directory = working_directory?;
|
||||
let git = GitBinary::new(git_binary_path, working_directory, executor);
|
||||
git.run(&[
|
||||
"diff",
|
||||
"--find-renames",
|
||||
"--patch",
|
||||
&base_checkpoint.ref_name,
|
||||
&target_checkpoint.ref_name,
|
||||
])
|
||||
.await
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn create_index(&self) -> BoxFuture<Result<GitIndex>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
||||
let executor = self.executor.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let working_directory = working_directory?;
|
||||
let mut git = GitBinary::new(git_binary_path, working_directory, executor);
|
||||
let index = GitIndex { id: Uuid::new_v4() };
|
||||
git.with_index(index, async move |git| git.run(&["add", "--all"]).await)
|
||||
.await?;
|
||||
Ok(index)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn apply_diff(&self, index: GitIndex, diff: String) -> BoxFuture<Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
||||
let executor = self.executor.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let working_directory = working_directory?;
|
||||
let mut git = GitBinary::new(git_binary_path, working_directory, executor);
|
||||
git.with_index(index, async move |git| {
|
||||
git.run_with_stdin(&["apply", "--cached", "-"], diff).await
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
|
||||
@@ -1256,7 +1405,7 @@ impl GitBinary {
|
||||
&mut self,
|
||||
f: impl AsyncFnOnce(&Self) -> Result<R>,
|
||||
) -> Result<R> {
|
||||
let index_file_path = self.working_directory.join(".git/index.tmp");
|
||||
let index_file_path = self.path_for_index(GitIndex { id: Uuid::new_v4() });
|
||||
|
||||
let delete_temp_index = util::defer({
|
||||
let index_file_path = index_file_path.clone();
|
||||
@@ -1281,7 +1430,81 @@ impl GitBinary {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn with_index<R>(
|
||||
&mut self,
|
||||
index: GitIndex,
|
||||
f: impl AsyncFnOnce(&Self) -> Result<R>,
|
||||
) -> Result<R> {
|
||||
self.with_option_index(Some(index), f).await
|
||||
}
|
||||
|
||||
pub async fn with_option_index<R>(
|
||||
&mut self,
|
||||
index: Option<GitIndex>,
|
||||
f: impl AsyncFnOnce(&Self) -> Result<R>,
|
||||
) -> Result<R> {
|
||||
let new_index_path = index.map(|index| self.path_for_index(index));
|
||||
let old_index_path = mem::replace(&mut self.index_file_path, new_index_path);
|
||||
let result = f(self).await;
|
||||
self.index_file_path = old_index_path;
|
||||
result
|
||||
}
|
||||
|
||||
fn path_for_index(&self, index: GitIndex) -> PathBuf {
|
||||
self.working_directory
|
||||
.join(".git")
|
||||
.join(format!("index-{}.tmp", index.id.to_string()))
|
||||
}
|
||||
|
||||
pub async fn run<S>(&self, args: impl IntoIterator<Item = S>) -> Result<String>
|
||||
where
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
let mut stdout = self.run_raw(args).await?;
|
||||
if stdout.chars().last() == Some('\n') {
|
||||
stdout.pop();
|
||||
}
|
||||
Ok(stdout)
|
||||
}
|
||||
|
||||
/// Returns the result of the command without trimming the trailing newline.
|
||||
pub async fn run_raw<S>(&self, args: impl IntoIterator<Item = S>) -> Result<String>
|
||||
where
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
let mut command = self.build_command(args);
|
||||
let output = command.output().await?;
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
} else {
|
||||
Err(anyhow!(GitBinaryCommandError {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
status: output.status,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_with_stdin(&self, args: &[&str], stdin: String) -> Result<String> {
|
||||
let mut command = self.build_command(args);
|
||||
command.stdin(Stdio::piped());
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let mut child_stdin = child.stdin.take().context("failed to write to stdin")?;
|
||||
child_stdin.write_all(stdin.as_bytes()).await?;
|
||||
drop(child_stdin);
|
||||
|
||||
let output = child.output().await?;
|
||||
if output.status.success() {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_command<S>(&self, args: impl IntoIterator<Item = S>) -> smol::process::Command
|
||||
where
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
@@ -1292,15 +1515,7 @@ impl GitBinary {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1570,8 +1785,9 @@ fn checkpoint_author_envs() -> HashMap<String, String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::status::FileStatus;
|
||||
use crate::status::{FileStatus, StatusCode, TrackedStatus};
|
||||
use gpui::TestAppContext;
|
||||
use unindent::Unindent;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_checkpoint_basic(cx: &mut TestAppContext) {
|
||||
@@ -1751,7 +1967,7 @@ mod tests {
|
||||
"content2"
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status(&[]).await.unwrap().entries.as_ref(),
|
||||
repo.status(None, &[]).await.unwrap().entries.as_ref(),
|
||||
&[
|
||||
(RepoPath::from_str("new_file1"), FileStatus::Untracked),
|
||||
(RepoPath::from_str("new_file2"), FileStatus::Untracked)
|
||||
@@ -1790,6 +2006,92 @@ mod tests {
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_secondary_indices(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, cx.executor()).unwrap();
|
||||
let index = repo.create_index().await.unwrap();
|
||||
smol::fs::write(repo_dir.path().join("file1"), "file1\n")
|
||||
.await
|
||||
.unwrap();
|
||||
smol::fs::write(repo_dir.path().join("file2"), "file2\n")
|
||||
.await
|
||||
.unwrap();
|
||||
let diff = r#"
|
||||
diff --git a/file2 b/file2
|
||||
new file mode 100644
|
||||
index 0000000..cbc4e2e
|
||||
--- /dev/null
|
||||
+++ b/file2
|
||||
@@ -0,0 +1 @@
|
||||
+file2
|
||||
"#
|
||||
.unindent();
|
||||
repo.apply_diff(index.clone(), diff.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
repo.status(Some(index), &[])
|
||||
.await
|
||||
.unwrap()
|
||||
.entries
|
||||
.as_ref(),
|
||||
vec![
|
||||
(RepoPath::from_str("file1"), FileStatus::Untracked),
|
||||
(
|
||||
RepoPath::from_str("file2"),
|
||||
FileStatus::index(StatusCode::Added)
|
||||
)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
repo.load_index_text(Some(index), RepoPath::from_str("file1"))
|
||||
.await,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
repo.load_index_text(Some(index), RepoPath::from_str("file2"))
|
||||
.await,
|
||||
Some("file2\n".to_string())
|
||||
);
|
||||
|
||||
smol::fs::write(repo_dir.path().join("file2"), "file2-changed\n")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
repo.status(Some(index), &[])
|
||||
.await
|
||||
.unwrap()
|
||||
.entries
|
||||
.as_ref(),
|
||||
vec![
|
||||
(RepoPath::from_str("file1"), FileStatus::Untracked),
|
||||
(
|
||||
RepoPath::from_str("file2"),
|
||||
FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Modified,
|
||||
index_status: StatusCode::Added,
|
||||
})
|
||||
)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
repo.load_index_text(Some(index), RepoPath::from_str("file1"))
|
||||
.await,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
repo.load_index_text(Some(index), RepoPath::from_str("file2"))
|
||||
.await,
|
||||
Some("file2\n".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_branches_parsing() {
|
||||
// suppress "help: octal escapes are not supported, `\0` is always null"
|
||||
|
||||
@@ -2259,7 +2259,7 @@ impl GitPanel {
|
||||
|
||||
let repo = repo.read(cx);
|
||||
|
||||
for entry in repo.status() {
|
||||
for entry in repo.cached_status() {
|
||||
let is_conflict = repo.has_conflict(&entry.repo_path);
|
||||
let is_new = entry.status.is_created();
|
||||
let staging = entry.status.staging();
|
||||
|
||||
@@ -26,7 +26,11 @@ use project::{
|
||||
git_store::{GitEvent, GitStore},
|
||||
Project, ProjectPath,
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, vertical_divider, KeyBinding, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
@@ -39,7 +43,47 @@ use workspace::{
|
||||
|
||||
actions!(git, [Diff, Add]);
|
||||
|
||||
pub trait DiffSource {
|
||||
// todo!("return a struct here")
|
||||
fn status(&self, cx: &App) -> Vec<(ProjectPath, FileStatus, bool)>;
|
||||
fn open_uncommitted_diff(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<BufferDiff>>>;
|
||||
// todo!("add an observe method")
|
||||
}
|
||||
|
||||
pub struct ProjectDiffSource(Entity<Project>);
|
||||
|
||||
impl DiffSource for ProjectDiffSource {
|
||||
fn status(&self, cx: &App) -> Vec<(ProjectPath, FileStatus, bool)> {
|
||||
let mut result = Vec::new();
|
||||
if let Some(git_repo) = self.0.read(cx).git_store().read(cx).active_repository() {
|
||||
let git_repo = git_repo.read(cx);
|
||||
for entry in git_repo.cached_status() {
|
||||
if let Some(project_path) = git_repo.repo_path_to_project_path(&entry.repo_path, cx)
|
||||
{
|
||||
let has_conflict = git_repo.has_conflict(&entry.repo_path);
|
||||
result.push((project_path, entry.status, has_conflict));
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn open_uncommitted_diff(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<BufferDiff>>> {
|
||||
self.0
|
||||
.update(cx, |project, cx| project.open_uncommitted_diff(buffer, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProjectDiff {
|
||||
source: Arc<dyn DiffSource>,
|
||||
project: Entity<Project>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
@@ -102,8 +146,16 @@ impl ProjectDiff {
|
||||
existing
|
||||
} else {
|
||||
let workspace_handle = cx.entity();
|
||||
let project_diff =
|
||||
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx));
|
||||
let source = Arc::new(ProjectDiffSource(workspace.project().clone()));
|
||||
let project_diff = cx.new(|cx| {
|
||||
Self::new(
|
||||
source,
|
||||
workspace.project().clone(),
|
||||
workspace_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(project_diff.clone()),
|
||||
None,
|
||||
@@ -127,6 +179,7 @@ impl ProjectDiff {
|
||||
}
|
||||
|
||||
fn new(
|
||||
source: Arc<dyn DiffSource>,
|
||||
project: Entity<Project>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
@@ -171,6 +224,7 @@ impl ProjectDiff {
|
||||
*send.borrow_mut() = ();
|
||||
|
||||
Self {
|
||||
source,
|
||||
project,
|
||||
git_store: git_store.clone(),
|
||||
workspace: workspace.downgrade(),
|
||||
@@ -328,56 +382,53 @@ impl ProjectDiff {
|
||||
}
|
||||
|
||||
fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> {
|
||||
let Some(repo) = self.git_store.read(cx).active_repository() else {
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.clear(cx);
|
||||
});
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||
|
||||
let mut result = vec![];
|
||||
repo.update(cx, |repo, cx| {
|
||||
for entry in repo.status() {
|
||||
if !entry.status.has_changes() {
|
||||
continue;
|
||||
}
|
||||
let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let namespace = if repo.has_conflict(&entry.repo_path) {
|
||||
CONFLICT_NAMESPACE
|
||||
} else if entry.status.is_created() {
|
||||
NEW_NAMESPACE
|
||||
} else {
|
||||
TRACKED_NAMESPACE
|
||||
};
|
||||
let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone());
|
||||
|
||||
previous_paths.remove(&path_key);
|
||||
let load_buffer = self
|
||||
.project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
|
||||
let project = self.project.clone();
|
||||
result.push(cx.spawn(async move |_, cx| {
|
||||
let buffer = load_buffer.await?;
|
||||
let changes = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
Ok(DiffBuffer {
|
||||
path_key,
|
||||
buffer,
|
||||
diff: changes,
|
||||
file_status: entry.status,
|
||||
})
|
||||
}));
|
||||
for (project_path, status, has_conflict) in self.source.status(cx) {
|
||||
if !status.has_changes() {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
let Some(worktree) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let full_path =
|
||||
Arc::from(Path::new(worktree.read(cx).root_name()).join(&project_path.path));
|
||||
|
||||
let namespace = if has_conflict {
|
||||
CONFLICT_NAMESPACE
|
||||
} else if status.is_created() {
|
||||
NEW_NAMESPACE
|
||||
} else {
|
||||
TRACKED_NAMESPACE
|
||||
};
|
||||
let path_key = PathKey::namespaced(namespace, full_path);
|
||||
|
||||
previous_paths.remove(&path_key);
|
||||
let load_buffer = self
|
||||
.project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
|
||||
let source = self.source.clone();
|
||||
result.push(cx.spawn(async move |_, cx| {
|
||||
let buffer = load_buffer.await?;
|
||||
let changes = cx
|
||||
.update(|cx| source.open_uncommitted_diff(buffer.clone(), cx))?
|
||||
.await?;
|
||||
Ok(DiffBuffer {
|
||||
path_key,
|
||||
buffer,
|
||||
diff: changes,
|
||||
file_status: status,
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
for path in previous_paths {
|
||||
multibuffer.remove_excerpts_for_path(path, cx);
|
||||
@@ -586,7 +637,15 @@ impl Item for ProjectDiff {
|
||||
Self: Sized,
|
||||
{
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
Some(cx.new(|cx| ProjectDiff::new(self.project.clone(), workspace, window, cx)))
|
||||
Some(cx.new(|cx| {
|
||||
ProjectDiff::new(
|
||||
self.source.clone(),
|
||||
self.project.clone(),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
@@ -744,7 +803,7 @@ impl SerializableItem for ProjectDiff {
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_project: Entity<Project>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
_item_id: workspace::ItemId,
|
||||
@@ -754,7 +813,16 @@ impl SerializableItem for ProjectDiff {
|
||||
window.spawn(cx, async move |cx| {
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let workspace_handle = cx.entity();
|
||||
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx))
|
||||
let diff = Arc::new(ProjectDiffSource(project));
|
||||
cx.new(|cx| {
|
||||
Self::new(
|
||||
diff,
|
||||
workspace.project().clone(),
|
||||
workspace_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1338,8 +1406,9 @@ mod tests {
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let source = Arc::new(ProjectDiffSource(project.clone()));
|
||||
let diff = cx.new_window_entity(|window, cx| {
|
||||
ProjectDiff::new(project.clone(), workspace, window, cx)
|
||||
ProjectDiff::new(source, project.clone(), workspace, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1392,8 +1461,9 @@ mod tests {
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let source = Arc::new(ProjectDiffSource(project.clone()));
|
||||
let diff = cx.new_window_entity(|window, cx| {
|
||||
ProjectDiff::new(project.clone(), workspace, window, cx)
|
||||
ProjectDiff::new(source, project.clone(), workspace, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1465,6 +1535,7 @@ mod tests {
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let source = Arc::new(ProjectDiffSource(project.clone()));
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/project/foo"), cx)
|
||||
@@ -1475,7 +1546,7 @@ mod tests {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
|
||||
});
|
||||
let diff = cx.new_window_entity(|window, cx| {
|
||||
ProjectDiff::new(project.clone(), workspace, window, cx)
|
||||
ProjectDiff::new(source, project.clone(), workspace, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ use git::{
|
||||
blame::Blame,
|
||||
parse_git_remote_url,
|
||||
repository::{
|
||||
Branch, CommitDetails, DiffType, GitRepository, GitRepositoryCheckpoint, PushOptions,
|
||||
Remote, RemoteCommandOutput, RepoPath, ResetMode,
|
||||
Branch, CommitDetails, DiffType, GitIndex, GitRepository, GitRepositoryCheckpoint,
|
||||
PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode,
|
||||
},
|
||||
status::FileStatus,
|
||||
status::{FileStatus, GitStatus},
|
||||
BuildPermalinkParams, GitHostingProviderRegistry,
|
||||
};
|
||||
use gpui::{
|
||||
@@ -146,6 +146,21 @@ pub struct GitStoreCheckpoint {
|
||||
checkpoints_by_work_dir_abs_path: HashMap<PathBuf, GitRepositoryCheckpoint>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GitStoreDiff {
|
||||
diffs_by_work_dir_abs_path: HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GitStoreIndex {
|
||||
indices_by_work_dir_abs_path: HashMap<PathBuf, GitIndex>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GitStoreStatus {
|
||||
statuses_by_work_dir_abs_path: HashMap<PathBuf, GitStatus>,
|
||||
}
|
||||
|
||||
pub struct Repository {
|
||||
pub repository_entry: RepositoryEntry,
|
||||
pub merge_message: Option<String>,
|
||||
@@ -651,8 +666,8 @@ impl GitStore {
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for (dot_git_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path {
|
||||
if let Some(repository) = repositories_by_work_dir_abs_path.get(&dot_git_abs_path) {
|
||||
for (work_dir_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path {
|
||||
if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) {
|
||||
let restore = repository.read(cx).restore_checkpoint(checkpoint);
|
||||
tasks.push(async move { restore.await? });
|
||||
}
|
||||
@@ -685,12 +700,13 @@ impl GitStore {
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for (dot_git_abs_path, left_checkpoint) in left.checkpoints_by_work_dir_abs_path {
|
||||
for (work_dir_abs_path, left_checkpoint) in left.checkpoints_by_work_dir_abs_path {
|
||||
if let Some(right_checkpoint) = right
|
||||
.checkpoints_by_work_dir_abs_path
|
||||
.remove(&dot_git_abs_path)
|
||||
.remove(&work_dir_abs_path)
|
||||
{
|
||||
if let Some(repository) = repositories_by_work_dir_abs_path.get(&dot_git_abs_path) {
|
||||
if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
|
||||
{
|
||||
let compare = repository
|
||||
.read(cx)
|
||||
.compare_checkpoints(left_checkpoint, right_checkpoint);
|
||||
@@ -738,6 +754,113 @@ impl GitStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn diff_checkpoints(
|
||||
&self,
|
||||
base_checkpoint: GitStoreCheckpoint,
|
||||
target_checkpoint: GitStoreCheckpoint,
|
||||
cx: &App,
|
||||
) -> Task<Result<GitStoreDiff>> {
|
||||
let repositories_by_work_dir_abs_path = self
|
||||
.repositories
|
||||
.values()
|
||||
.map(|repo| {
|
||||
(
|
||||
repo.read(cx)
|
||||
.repository_entry
|
||||
.work_directory_abs_path
|
||||
.clone(),
|
||||
repo,
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for (work_dir_abs_path, base_checkpoint) in base_checkpoint.checkpoints_by_work_dir_abs_path
|
||||
{
|
||||
if let Some(target_checkpoint) = target_checkpoint
|
||||
.checkpoints_by_work_dir_abs_path
|
||||
.get(&work_dir_abs_path)
|
||||
.cloned()
|
||||
{
|
||||
if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
|
||||
{
|
||||
let diff = repository
|
||||
.read(cx)
|
||||
.diff_checkpoints(base_checkpoint, target_checkpoint);
|
||||
tasks.push(async move {
|
||||
let diff = diff.await??;
|
||||
anyhow::Ok((work_dir_abs_path, diff))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let diffs_by_path = future::try_join_all(tasks).await?;
|
||||
Ok(GitStoreDiff {
|
||||
diffs_by_work_dir_abs_path: diffs_by_path.into_iter().collect(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_index(&self, cx: &App) -> Task<Result<GitStoreIndex>> {
|
||||
let mut indices = Vec::new();
|
||||
for repository in self.repositories.values() {
|
||||
let repository = repository.read(cx);
|
||||
let work_dir_abs_path = repository.repository_entry.work_directory_abs_path.clone();
|
||||
let index = repository.create_index().map(|index| index?);
|
||||
indices.push(async move {
|
||||
let index = index.await?;
|
||||
anyhow::Ok((work_dir_abs_path, index))
|
||||
});
|
||||
}
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let indices = future::try_join_all(indices).await?;
|
||||
Ok(GitStoreIndex {
|
||||
indices_by_work_dir_abs_path: indices.into_iter().collect(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply_diff(
|
||||
&self,
|
||||
mut index: GitStoreIndex,
|
||||
diff: GitStoreDiff,
|
||||
cx: &App,
|
||||
) -> Task<Result<()>> {
|
||||
let repositories_by_work_dir_abs_path = self
|
||||
.repositories
|
||||
.values()
|
||||
.map(|repo| {
|
||||
(
|
||||
repo.read(cx)
|
||||
.repository_entry
|
||||
.work_directory_abs_path
|
||||
.clone(),
|
||||
repo,
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for (work_dir_abs_path, diff) in diff.diffs_by_work_dir_abs_path {
|
||||
if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) {
|
||||
if let Some(branch) = index
|
||||
.indices_by_work_dir_abs_path
|
||||
.remove(&work_dir_abs_path)
|
||||
{
|
||||
let apply = repository.read(cx).apply_diff(branch, diff);
|
||||
tasks.push(async move { apply.await? });
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.background_spawn(async move {
|
||||
future::try_join_all(tasks).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Blames a buffer.
|
||||
pub fn blame_buffer(
|
||||
&self,
|
||||
@@ -1282,7 +1405,7 @@ impl GitStore {
|
||||
let index_text = if current_index_text.is_some() {
|
||||
local_repo
|
||||
.repo()
|
||||
.load_index_text(relative_path.clone())
|
||||
.load_index_text(None, relative_path.clone())
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
@@ -1397,6 +1520,62 @@ impl GitStore {
|
||||
Some(status.status)
|
||||
}
|
||||
|
||||
pub fn status(&self, index: Option<GitStoreIndex>, cx: &App) -> Task<Result<GitStoreStatus>> {
|
||||
let repositories_by_work_dir_abs_path = self
|
||||
.repositories
|
||||
.values()
|
||||
.map(|repo| {
|
||||
(
|
||||
repo.read(cx)
|
||||
.repository_entry
|
||||
.work_directory_abs_path
|
||||
.clone(),
|
||||
repo,
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
if let Some(index) = index {
|
||||
// When we have an index, just check the repositories that are part of it
|
||||
for (work_dir_abs_path, git_index) in index.indices_by_work_dir_abs_path {
|
||||
if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
|
||||
{
|
||||
let status = repository.read(cx).status(Some(git_index));
|
||||
tasks.push(
|
||||
async move {
|
||||
let status = status.await??;
|
||||
anyhow::Ok((work_dir_abs_path, status))
|
||||
}
|
||||
.boxed(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise, check all repositories
|
||||
for repository in self.repositories.values() {
|
||||
let repository = repository.read(cx);
|
||||
let work_dir_abs_path = repository.repository_entry.work_directory_abs_path.clone();
|
||||
let status = repository.status(None);
|
||||
tasks.push(
|
||||
async move {
|
||||
let status = status.await??;
|
||||
anyhow::Ok((work_dir_abs_path, status))
|
||||
}
|
||||
.boxed(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let statuses = future::try_join_all(tasks).await?;
|
||||
Ok(GitStoreStatus {
|
||||
statuses_by_work_dir_abs_path: statuses.into_iter().collect(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn repository_and_path_for_buffer_id(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
@@ -2642,10 +2821,19 @@ impl Repository {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
|
||||
pub fn cached_status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
|
||||
self.repository_entry.status()
|
||||
}
|
||||
|
||||
pub fn status(&self, index: Option<GitIndex>) -> oneshot::Receiver<Result<GitStatus>> {
|
||||
self.send_job(move |repo, _cx| async move {
|
||||
match repo {
|
||||
RepositoryState::Local(git_repository) => git_repository.status(index, &[]).await,
|
||||
RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_conflict(&self, path: &RepoPath) -> bool {
|
||||
self.repository_entry
|
||||
.current_merge_conflicts
|
||||
@@ -3533,6 +3721,43 @@ impl Repository {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn diff_checkpoints(
|
||||
&self,
|
||||
base_checkpoint: GitRepositoryCheckpoint,
|
||||
target_checkpoint: GitRepositoryCheckpoint,
|
||||
) -> oneshot::Receiver<Result<String>> {
|
||||
self.send_job(move |repo, _cx| async move {
|
||||
match repo {
|
||||
RepositoryState::Local(git_repository) => {
|
||||
git_repository
|
||||
.diff_checkpoints(base_checkpoint, target_checkpoint)
|
||||
.await
|
||||
}
|
||||
RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_index(&self) -> oneshot::Receiver<Result<GitIndex>> {
|
||||
self.send_job(move |repo, _cx| async move {
|
||||
match repo {
|
||||
RepositoryState::Local(git_repository) => git_repository.create_index().await,
|
||||
RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply_diff(&self, index: GitIndex, diff: String) -> oneshot::Receiver<Result<()>> {
|
||||
self.send_job(move |repo, _cx| async move {
|
||||
match repo {
|
||||
RepositoryState::Local(git_repository) => {
|
||||
git_repository.apply_diff(index, diff).await
|
||||
}
|
||||
RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_permalink_in_rust_registry_src(
|
||||
|
||||
@@ -1041,7 +1041,10 @@ impl Worktree {
|
||||
if let Some(git_repo) =
|
||||
snapshot.git_repositories.get(&repo.work_directory_id)
|
||||
{
|
||||
return Ok(git_repo.repo_ptr.load_index_text(repo_path).await);
|
||||
return Ok(git_repo
|
||||
.repo_ptr
|
||||
.load_index_text(None, repo_path)
|
||||
.await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user