Closes #47907 Implements the four git checkpoint operations (`create`, `restore`, `compare`, `diff`) that had been stubbed out for remote repositories, and related test infrastructure. Testing steps: 1. Open a project with a `.devcontainer` configuration and connect to the Dev Container 2. Open an Agent thread and ask the agent to make a code change 3. After the agent completes, verify the "Restore from checkpoint" button appears (previously missing in Dev Container sessions) 4. Click "Restore from checkpoint" and confirm the file reverts to its prior state Release Notes: - Added support for git checkpoint operations in remote/Dev Container sessions, restoring the "Restore from checkpoint" button in Agent threads. --------- Co-authored-by: KyleBarton <kjb@initialcapacity.io>
7575 lines
284 KiB
Rust
7575 lines
284 KiB
Rust
pub mod branch_diff;
|
|
mod conflict_set;
|
|
pub mod git_traversal;
|
|
pub mod pending_op;
|
|
|
|
use crate::{
|
|
ProjectEnvironment, ProjectItem, ProjectPath,
|
|
buffer_store::{BufferStore, BufferStoreEvent},
|
|
trusted_worktrees::{
|
|
PathTrust, TrustedWorktrees, TrustedWorktreesEvent, TrustedWorktreesStore,
|
|
},
|
|
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
|
};
|
|
use anyhow::{Context as _, Result, anyhow, bail};
|
|
use askpass::{AskPassDelegate, EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
|
|
use buffer_diff::{BufferDiff, BufferDiffEvent};
|
|
use client::ProjectId;
|
|
use collections::HashMap;
|
|
pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate};
|
|
use fs::Fs;
|
|
use futures::{
|
|
FutureExt, StreamExt,
|
|
channel::{
|
|
mpsc,
|
|
oneshot::{self, Canceled},
|
|
},
|
|
future::{self, BoxFuture, Shared},
|
|
stream::FuturesOrdered,
|
|
};
|
|
use git::{
|
|
BuildPermalinkParams, GitHostingProviderRegistry, Oid, RunHook,
|
|
blame::Blame,
|
|
parse_git_remote_url,
|
|
repository::{
|
|
Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, DiffType, FetchOptions,
|
|
GitRepository, GitRepositoryCheckpoint, GraphCommitData, InitialGraphCommitData, LogOrder,
|
|
LogSource, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode, SearchCommitArgs,
|
|
UpstreamTrackingStatus, Worktree as GitWorktree,
|
|
},
|
|
stash::{GitStash, StashEntry},
|
|
status::{
|
|
self, DiffStat, DiffTreeType, FileStatus, GitSummary, StatusCode, TrackedStatus, TreeDiff,
|
|
TreeDiffStatus, UnmergedStatus, UnmergedStatusCode,
|
|
},
|
|
};
|
|
use gpui::{
|
|
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
|
|
WeakEntity,
|
|
};
|
|
use language::{
|
|
Buffer, BufferEvent, Language, LanguageRegistry,
|
|
proto::{deserialize_version, serialize_version},
|
|
};
|
|
use parking_lot::Mutex;
|
|
use pending_op::{PendingOp, PendingOpId, PendingOps, PendingOpsSummary};
|
|
use postage::stream::Stream as _;
|
|
use rpc::{
|
|
AnyProtoClient, TypedEnvelope,
|
|
proto::{self, git_reset, split_repository_update},
|
|
};
|
|
use serde::Deserialize;
|
|
use settings::WorktreeId;
|
|
use smol::future::yield_now;
|
|
use std::{
|
|
cmp::Ordering,
|
|
collections::{BTreeSet, HashSet, VecDeque, hash_map::Entry},
|
|
future::Future,
|
|
mem,
|
|
ops::Range,
|
|
path::{Path, PathBuf},
|
|
str::FromStr,
|
|
sync::{
|
|
Arc,
|
|
atomic::{self, AtomicU64},
|
|
},
|
|
time::Instant,
|
|
};
|
|
use sum_tree::{Edit, SumTree, TreeMap};
|
|
use task::Shell;
|
|
use text::{Bias, BufferId};
|
|
use util::{
|
|
ResultExt, debug_panic,
|
|
paths::{PathStyle, SanitizedPath},
|
|
post_inc,
|
|
rel_path::RelPath,
|
|
};
|
|
use worktree::{
|
|
File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId,
|
|
UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree,
|
|
};
|
|
use zeroize::Zeroize;
|
|
|
|
pub struct GitStore {
|
|
state: GitStoreState,
|
|
buffer_store: Entity<BufferStore>,
|
|
worktree_store: Entity<WorktreeStore>,
|
|
repositories: HashMap<RepositoryId, Entity<Repository>>,
|
|
worktree_ids: HashMap<RepositoryId, HashSet<WorktreeId>>,
|
|
active_repo_id: Option<RepositoryId>,
|
|
#[allow(clippy::type_complexity)]
|
|
loading_diffs:
|
|
HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
|
|
diffs: HashMap<BufferId, Entity<BufferGitState>>,
|
|
shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct SharedDiffs {
|
|
unstaged: Option<Entity<BufferDiff>>,
|
|
uncommitted: Option<Entity<BufferDiff>>,
|
|
}
|
|
|
|
struct BufferGitState {
|
|
unstaged_diff: Option<WeakEntity<BufferDiff>>,
|
|
uncommitted_diff: Option<WeakEntity<BufferDiff>>,
|
|
oid_diffs: HashMap<Option<git::Oid>, WeakEntity<BufferDiff>>,
|
|
conflict_set: Option<WeakEntity<ConflictSet>>,
|
|
recalculate_diff_task: Option<Task<Result<()>>>,
|
|
reparse_conflict_markers_task: Option<Task<Result<()>>>,
|
|
language: Option<Arc<Language>>,
|
|
language_registry: Option<Arc<LanguageRegistry>>,
|
|
conflict_updated_futures: Vec<oneshot::Sender<()>>,
|
|
recalculating_tx: postage::watch::Sender<bool>,
|
|
|
|
/// These operation counts are used to ensure that head and index text
|
|
/// values read from the git repository are up-to-date with any hunk staging
|
|
/// operations that have been performed on the BufferDiff.
|
|
///
|
|
/// The operation count is incremented immediately when the user initiates a
|
|
/// hunk stage/unstage operation. Then, upon finishing writing the new index
|
|
/// text do disk, the `operation count as of write` is updated to reflect
|
|
/// the operation count that prompted the write.
|
|
hunk_staging_operation_count: usize,
|
|
hunk_staging_operation_count_as_of_write: usize,
|
|
|
|
head_text: Option<Arc<str>>,
|
|
index_text: Option<Arc<str>>,
|
|
oid_texts: HashMap<git::Oid, Arc<str>>,
|
|
head_changed: bool,
|
|
index_changed: bool,
|
|
language_changed: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum DiffBasesChange {
|
|
SetIndex(Option<String>),
|
|
SetHead(Option<String>),
|
|
SetEach {
|
|
index: Option<String>,
|
|
head: Option<String>,
|
|
},
|
|
SetBoth(Option<String>),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
enum DiffKind {
|
|
Unstaged,
|
|
Uncommitted,
|
|
SinceOid(Option<git::Oid>),
|
|
}
|
|
|
|
enum GitStoreState {
|
|
Local {
|
|
next_repository_id: Arc<AtomicU64>,
|
|
downstream: Option<LocalDownstreamState>,
|
|
project_environment: Entity<ProjectEnvironment>,
|
|
fs: Arc<dyn Fs>,
|
|
},
|
|
Remote {
|
|
upstream_client: AnyProtoClient,
|
|
upstream_project_id: u64,
|
|
downstream: Option<(AnyProtoClient, ProjectId)>,
|
|
},
|
|
}
|
|
|
|
enum DownstreamUpdate {
|
|
UpdateRepository(RepositorySnapshot),
|
|
RemoveRepository(RepositoryId),
|
|
}
|
|
|
|
struct LocalDownstreamState {
|
|
client: AnyProtoClient,
|
|
project_id: ProjectId,
|
|
updates_tx: mpsc::UnboundedSender<DownstreamUpdate>,
|
|
_task: Task<Result<()>>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct GitStoreCheckpoint {
|
|
checkpoints_by_work_dir_abs_path: HashMap<Arc<Path>, GitRepositoryCheckpoint>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct StatusEntry {
|
|
pub repo_path: RepoPath,
|
|
pub status: FileStatus,
|
|
pub diff_stat: Option<DiffStat>,
|
|
}
|
|
|
|
impl StatusEntry {
|
|
fn to_proto(&self) -> proto::StatusEntry {
|
|
let simple_status = match self.status {
|
|
FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32,
|
|
FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32,
|
|
FileStatus::Tracked(TrackedStatus {
|
|
index_status,
|
|
worktree_status,
|
|
}) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified {
|
|
worktree_status
|
|
} else {
|
|
index_status
|
|
}),
|
|
};
|
|
|
|
proto::StatusEntry {
|
|
repo_path: self.repo_path.to_proto(),
|
|
simple_status,
|
|
status: Some(status_to_proto(self.status)),
|
|
diff_stat_added: self.diff_stat.map(|ds| ds.added),
|
|
diff_stat_deleted: self.diff_stat.map(|ds| ds.deleted),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<proto::StatusEntry> for StatusEntry {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
|
|
let repo_path = RepoPath::from_proto(&value.repo_path).context("invalid repo path")?;
|
|
let status = status_from_proto(value.simple_status, value.status)?;
|
|
let diff_stat = match (value.diff_stat_added, value.diff_stat_deleted) {
|
|
(Some(added), Some(deleted)) => Some(DiffStat { added, deleted }),
|
|
_ => None,
|
|
};
|
|
Ok(Self {
|
|
repo_path,
|
|
status,
|
|
diff_stat,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl sum_tree::Item for StatusEntry {
|
|
type Summary = PathSummary<GitSummary>;
|
|
|
|
fn summary(&self, _: <Self::Summary as sum_tree::Summary>::Context<'_>) -> Self::Summary {
|
|
PathSummary {
|
|
max_path: self.repo_path.as_ref().clone(),
|
|
item_summary: self.status.summary(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl sum_tree::KeyedItem for StatusEntry {
|
|
type Key = PathKey;
|
|
|
|
fn key(&self) -> Self::Key {
|
|
PathKey(self.repo_path.as_ref().clone())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub struct RepositoryId(pub u64);
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct MergeDetails {
|
|
pub merge_heads_by_conflicted_path: TreeMap<RepoPath, Vec<Option<SharedString>>>,
|
|
pub message: Option<SharedString>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub enum CommitDataState {
|
|
Loading,
|
|
Loaded(Arc<GraphCommitData>),
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RepositorySnapshot {
|
|
pub id: RepositoryId,
|
|
pub statuses_by_path: SumTree<StatusEntry>,
|
|
pub work_directory_abs_path: Arc<Path>,
|
|
/// The working directory of the original repository. For a normal
|
|
/// checkout this equals `work_directory_abs_path`. For a git worktree
|
|
/// checkout, this is the original repo's working directory — used to
|
|
/// anchor new worktree creation so they don't nest.
|
|
pub original_repo_abs_path: Arc<Path>,
|
|
pub path_style: PathStyle,
|
|
pub branch: Option<Branch>,
|
|
pub head_commit: Option<CommitDetails>,
|
|
pub scan_id: u64,
|
|
pub merge: MergeDetails,
|
|
pub remote_origin_url: Option<String>,
|
|
pub remote_upstream_url: Option<String>,
|
|
pub stash_entries: GitStash,
|
|
pub linked_worktrees: Arc<[GitWorktree]>,
|
|
}
|
|
|
|
type JobId = u64;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct JobInfo {
|
|
pub start: Instant,
|
|
pub message: SharedString,
|
|
}
|
|
|
|
struct GraphCommitDataHandler {
|
|
_task: Task<()>,
|
|
commit_data_request: smol::channel::Sender<Oid>,
|
|
}
|
|
|
|
enum GraphCommitHandlerState {
|
|
Starting,
|
|
Open(GraphCommitDataHandler),
|
|
Closed,
|
|
}
|
|
|
|
pub struct InitialGitGraphData {
|
|
fetch_task: Task<()>,
|
|
pub error: Option<SharedString>,
|
|
pub commit_data: Vec<Arc<InitialGraphCommitData>>,
|
|
pub commit_oid_to_index: HashMap<Oid, usize>,
|
|
}
|
|
|
|
pub struct GraphDataResponse<'a> {
|
|
pub commits: &'a [Arc<InitialGraphCommitData>],
|
|
pub is_loading: bool,
|
|
pub error: Option<SharedString>,
|
|
}
|
|
|
|
pub struct Repository {
|
|
this: WeakEntity<Self>,
|
|
snapshot: RepositorySnapshot,
|
|
commit_message_buffer: Option<Entity<Buffer>>,
|
|
git_store: WeakEntity<GitStore>,
|
|
// For a local repository, holds paths that have had worktree events since the last status scan completed,
|
|
// and that should be examined during the next status scan.
|
|
paths_needing_status_update: Vec<Vec<RepoPath>>,
|
|
job_sender: mpsc::UnboundedSender<GitJob>,
|
|
active_jobs: HashMap<JobId, JobInfo>,
|
|
pending_ops: SumTree<PendingOps>,
|
|
job_id: JobId,
|
|
askpass_delegates: Arc<Mutex<HashMap<u64, AskPassDelegate>>>,
|
|
latest_askpass_id: u64,
|
|
repository_state: Shared<Task<Result<RepositoryState, String>>>,
|
|
initial_graph_data: HashMap<(LogSource, LogOrder), InitialGitGraphData>,
|
|
graph_commit_data_handler: GraphCommitHandlerState,
|
|
commit_data: HashMap<Oid, CommitDataState>,
|
|
}
|
|
|
|
impl std::ops::Deref for Repository {
|
|
type Target = RepositorySnapshot;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.snapshot
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct LocalRepositoryState {
|
|
pub fs: Arc<dyn Fs>,
|
|
pub backend: Arc<dyn GitRepository>,
|
|
pub environment: Arc<HashMap<String, String>>,
|
|
}
|
|
|
|
impl LocalRepositoryState {
|
|
async fn new(
|
|
work_directory_abs_path: Arc<Path>,
|
|
dot_git_abs_path: Arc<Path>,
|
|
project_environment: WeakEntity<ProjectEnvironment>,
|
|
fs: Arc<dyn Fs>,
|
|
is_trusted: bool,
|
|
cx: &mut AsyncApp,
|
|
) -> anyhow::Result<Self> {
|
|
let environment = project_environment
|
|
.update(cx, |project_environment, cx| {
|
|
project_environment.local_directory_environment(&Shell::System, work_directory_abs_path.clone(), cx)
|
|
})?
|
|
.await
|
|
.unwrap_or_else(|| {
|
|
log::error!("failed to get working directory environment for repository {work_directory_abs_path:?}");
|
|
HashMap::default()
|
|
});
|
|
let search_paths = environment.get("PATH").map(|val| val.to_owned());
|
|
let backend = cx
|
|
.background_spawn({
|
|
let fs = fs.clone();
|
|
async move {
|
|
let system_git_binary_path = search_paths
|
|
.and_then(|search_paths| {
|
|
which::which_in("git", Some(search_paths), &work_directory_abs_path)
|
|
.ok()
|
|
})
|
|
.or_else(|| which::which("git").ok());
|
|
fs.open_repo(&dot_git_abs_path, system_git_binary_path.as_deref())
|
|
.with_context(|| format!("opening repository at {dot_git_abs_path:?}"))
|
|
}
|
|
})
|
|
.await?;
|
|
backend.set_trusted(is_trusted);
|
|
Ok(LocalRepositoryState {
|
|
backend,
|
|
environment: Arc::new(environment),
|
|
fs,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct RemoteRepositoryState {
|
|
pub project_id: ProjectId,
|
|
pub client: AnyProtoClient,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub enum RepositoryState {
|
|
Local(LocalRepositoryState),
|
|
Remote(RemoteRepositoryState),
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum GitGraphEvent {
|
|
CountUpdated(usize),
|
|
FullyLoaded,
|
|
LoadingError,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum RepositoryEvent {
|
|
StatusesChanged,
|
|
BranchChanged,
|
|
StashEntriesChanged,
|
|
GitWorktreeListChanged,
|
|
PendingOpsChanged { pending_ops: SumTree<PendingOps> },
|
|
GraphEvent((LogSource, LogOrder), GitGraphEvent),
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct JobsUpdated;
|
|
|
|
#[derive(Debug)]
|
|
pub enum GitStoreEvent {
|
|
ActiveRepositoryChanged(Option<RepositoryId>),
|
|
/// Bool is true when the repository that's updated is the active repository
|
|
RepositoryUpdated(RepositoryId, RepositoryEvent, bool),
|
|
RepositoryAdded,
|
|
RepositoryRemoved(RepositoryId),
|
|
IndexWriteError(anyhow::Error),
|
|
JobsUpdated,
|
|
ConflictsUpdated,
|
|
}
|
|
|
|
impl EventEmitter<RepositoryEvent> for Repository {}
|
|
impl EventEmitter<JobsUpdated> for Repository {}
|
|
impl EventEmitter<GitStoreEvent> for GitStore {}
|
|
|
|
pub struct GitJob {
|
|
job: Box<dyn FnOnce(RepositoryState, &mut AsyncApp) -> Task<()>>,
|
|
key: Option<GitJobKey>,
|
|
}
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
enum GitJobKey {
|
|
WriteIndex(Vec<RepoPath>),
|
|
ReloadBufferDiffBases,
|
|
RefreshStatuses,
|
|
ReloadGitState,
|
|
}
|
|
|
|
impl GitStore {
|
|
pub fn local(
|
|
worktree_store: &Entity<WorktreeStore>,
|
|
buffer_store: Entity<BufferStore>,
|
|
environment: Entity<ProjectEnvironment>,
|
|
fs: Arc<dyn Fs>,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
Self::new(
|
|
worktree_store.clone(),
|
|
buffer_store,
|
|
GitStoreState::Local {
|
|
next_repository_id: Arc::new(AtomicU64::new(1)),
|
|
downstream: None,
|
|
project_environment: environment,
|
|
fs,
|
|
},
|
|
cx,
|
|
)
|
|
}
|
|
|
|
pub fn remote(
|
|
worktree_store: &Entity<WorktreeStore>,
|
|
buffer_store: Entity<BufferStore>,
|
|
upstream_client: AnyProtoClient,
|
|
project_id: u64,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
Self::new(
|
|
worktree_store.clone(),
|
|
buffer_store,
|
|
GitStoreState::Remote {
|
|
upstream_client,
|
|
upstream_project_id: project_id,
|
|
downstream: None,
|
|
},
|
|
cx,
|
|
)
|
|
}
|
|
|
|
fn new(
|
|
worktree_store: Entity<WorktreeStore>,
|
|
buffer_store: Entity<BufferStore>,
|
|
state: GitStoreState,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let mut _subscriptions = vec![
|
|
cx.subscribe(&worktree_store, Self::on_worktree_store_event),
|
|
cx.subscribe(&buffer_store, Self::on_buffer_store_event),
|
|
];
|
|
|
|
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
|
_subscriptions.push(cx.subscribe(&trusted_worktrees, Self::on_trusted_worktrees_event));
|
|
}
|
|
|
|
GitStore {
|
|
state,
|
|
buffer_store,
|
|
worktree_store,
|
|
repositories: HashMap::default(),
|
|
worktree_ids: HashMap::default(),
|
|
active_repo_id: None,
|
|
_subscriptions,
|
|
loading_diffs: HashMap::default(),
|
|
shared_diffs: HashMap::default(),
|
|
diffs: HashMap::default(),
|
|
}
|
|
}
|
|
|
|
pub fn init(client: &AnyProtoClient) {
|
|
client.add_entity_request_handler(Self::handle_get_remotes);
|
|
client.add_entity_request_handler(Self::handle_get_branches);
|
|
client.add_entity_request_handler(Self::handle_get_default_branch);
|
|
client.add_entity_request_handler(Self::handle_change_branch);
|
|
client.add_entity_request_handler(Self::handle_create_branch);
|
|
client.add_entity_request_handler(Self::handle_rename_branch);
|
|
client.add_entity_request_handler(Self::handle_create_remote);
|
|
client.add_entity_request_handler(Self::handle_remove_remote);
|
|
client.add_entity_request_handler(Self::handle_delete_branch);
|
|
client.add_entity_request_handler(Self::handle_git_init);
|
|
client.add_entity_request_handler(Self::handle_push);
|
|
client.add_entity_request_handler(Self::handle_pull);
|
|
client.add_entity_request_handler(Self::handle_fetch);
|
|
client.add_entity_request_handler(Self::handle_stage);
|
|
client.add_entity_request_handler(Self::handle_unstage);
|
|
client.add_entity_request_handler(Self::handle_stash);
|
|
client.add_entity_request_handler(Self::handle_stash_pop);
|
|
client.add_entity_request_handler(Self::handle_stash_apply);
|
|
client.add_entity_request_handler(Self::handle_stash_drop);
|
|
client.add_entity_request_handler(Self::handle_commit);
|
|
client.add_entity_request_handler(Self::handle_run_hook);
|
|
client.add_entity_request_handler(Self::handle_reset);
|
|
client.add_entity_request_handler(Self::handle_show);
|
|
client.add_entity_request_handler(Self::handle_create_checkpoint);
|
|
client.add_entity_request_handler(Self::handle_restore_checkpoint);
|
|
client.add_entity_request_handler(Self::handle_compare_checkpoints);
|
|
client.add_entity_request_handler(Self::handle_diff_checkpoints);
|
|
client.add_entity_request_handler(Self::handle_load_commit_diff);
|
|
client.add_entity_request_handler(Self::handle_file_history);
|
|
client.add_entity_request_handler(Self::handle_checkout_files);
|
|
client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
|
|
client.add_entity_request_handler(Self::handle_set_index_text);
|
|
client.add_entity_request_handler(Self::handle_askpass);
|
|
client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
|
|
client.add_entity_request_handler(Self::handle_git_diff);
|
|
client.add_entity_request_handler(Self::handle_tree_diff);
|
|
client.add_entity_request_handler(Self::handle_get_blob_content);
|
|
client.add_entity_request_handler(Self::handle_open_unstaged_diff);
|
|
client.add_entity_request_handler(Self::handle_open_uncommitted_diff);
|
|
client.add_entity_message_handler(Self::handle_update_diff_bases);
|
|
client.add_entity_request_handler(Self::handle_get_permalink_to_line);
|
|
client.add_entity_request_handler(Self::handle_blame_buffer);
|
|
client.add_entity_message_handler(Self::handle_update_repository);
|
|
client.add_entity_message_handler(Self::handle_remove_repository);
|
|
client.add_entity_request_handler(Self::handle_git_clone);
|
|
client.add_entity_request_handler(Self::handle_get_worktrees);
|
|
client.add_entity_request_handler(Self::handle_create_worktree);
|
|
client.add_entity_request_handler(Self::handle_remove_worktree);
|
|
client.add_entity_request_handler(Self::handle_rename_worktree);
|
|
}
|
|
|
|
pub fn is_local(&self) -> bool {
|
|
matches!(self.state, GitStoreState::Local { .. })
|
|
}
|
|
pub fn set_active_repo_for_path(&mut self, project_path: &ProjectPath, cx: &mut Context<Self>) {
|
|
if let Some((repo, _)) = self.repository_and_path_for_project_path(project_path, cx) {
|
|
let id = repo.read(cx).id;
|
|
if self.active_repo_id != Some(id) {
|
|
self.active_repo_id = Some(id);
|
|
cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
|
|
match &mut self.state {
|
|
GitStoreState::Remote {
|
|
downstream: downstream_client,
|
|
..
|
|
} => {
|
|
for repo in self.repositories.values() {
|
|
let update = repo.read(cx).snapshot.initial_update(project_id);
|
|
for update in split_repository_update(update) {
|
|
client.send(update).log_err();
|
|
}
|
|
}
|
|
*downstream_client = Some((client, ProjectId(project_id)));
|
|
}
|
|
GitStoreState::Local {
|
|
downstream: downstream_client,
|
|
..
|
|
} => {
|
|
let mut snapshots = HashMap::default();
|
|
let (updates_tx, mut updates_rx) = mpsc::unbounded();
|
|
for repo in self.repositories.values() {
|
|
updates_tx
|
|
.unbounded_send(DownstreamUpdate::UpdateRepository(
|
|
repo.read(cx).snapshot.clone(),
|
|
))
|
|
.ok();
|
|
}
|
|
*downstream_client = Some(LocalDownstreamState {
|
|
client: client.clone(),
|
|
project_id: ProjectId(project_id),
|
|
updates_tx,
|
|
_task: cx.spawn(async move |this, cx| {
|
|
cx.background_spawn(async move {
|
|
while let Some(update) = updates_rx.next().await {
|
|
match update {
|
|
DownstreamUpdate::UpdateRepository(snapshot) => {
|
|
if let Some(old_snapshot) = snapshots.get_mut(&snapshot.id)
|
|
{
|
|
let update =
|
|
snapshot.build_update(old_snapshot, project_id);
|
|
*old_snapshot = snapshot;
|
|
for update in split_repository_update(update) {
|
|
client.send(update)?;
|
|
}
|
|
} else {
|
|
let update = snapshot.initial_update(project_id);
|
|
for update in split_repository_update(update) {
|
|
client.send(update)?;
|
|
}
|
|
snapshots.insert(snapshot.id, snapshot);
|
|
}
|
|
}
|
|
DownstreamUpdate::RemoveRepository(id) => {
|
|
client.send(proto::RemoveRepository {
|
|
project_id,
|
|
id: id.to_proto(),
|
|
})?;
|
|
}
|
|
}
|
|
}
|
|
anyhow::Ok(())
|
|
})
|
|
.await
|
|
.ok();
|
|
this.update(cx, |this, _| {
|
|
if let GitStoreState::Local {
|
|
downstream: downstream_client,
|
|
..
|
|
} = &mut this.state
|
|
{
|
|
downstream_client.take();
|
|
} else {
|
|
unreachable!("unshared called on remote store");
|
|
}
|
|
})
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn unshared(&mut self, _cx: &mut Context<Self>) {
|
|
match &mut self.state {
|
|
GitStoreState::Local {
|
|
downstream: downstream_client,
|
|
..
|
|
} => {
|
|
downstream_client.take();
|
|
}
|
|
GitStoreState::Remote {
|
|
downstream: downstream_client,
|
|
..
|
|
} => {
|
|
downstream_client.take();
|
|
}
|
|
}
|
|
self.shared_diffs.clear();
|
|
}
|
|
|
|
pub(crate) fn forget_shared_diffs_for(&mut self, peer_id: &proto::PeerId) {
|
|
self.shared_diffs.remove(peer_id);
|
|
}
|
|
|
|
pub fn active_repository(&self) -> Option<Entity<Repository>> {
|
|
self.active_repo_id
|
|
.as_ref()
|
|
.map(|id| self.repositories[id].clone())
|
|
}
|
|
|
|
pub fn open_unstaged_diff(
|
|
&mut self,
|
|
buffer: Entity<Buffer>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<Entity<BufferDiff>>> {
|
|
let buffer_id = buffer.read(cx).remote_id();
|
|
if let Some(diff_state) = self.diffs.get(&buffer_id)
|
|
&& let Some(unstaged_diff) = diff_state
|
|
.read(cx)
|
|
.unstaged_diff
|
|
.as_ref()
|
|
.and_then(|weak| weak.upgrade())
|
|
{
|
|
if let Some(task) =
|
|
diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
|
|
{
|
|
return cx.background_executor().spawn(async move {
|
|
task.await;
|
|
Ok(unstaged_diff)
|
|
});
|
|
}
|
|
return Task::ready(Ok(unstaged_diff));
|
|
}
|
|
|
|
let Some((repo, repo_path)) =
|
|
self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
|
|
else {
|
|
return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
|
|
};
|
|
|
|
let task = self
|
|
.loading_diffs
|
|
.entry((buffer_id, DiffKind::Unstaged))
|
|
.or_insert_with(|| {
|
|
let staged_text = repo.update(cx, |repo, cx| {
|
|
repo.load_staged_text(buffer_id, repo_path, cx)
|
|
});
|
|
cx.spawn(async move |this, cx| {
|
|
Self::open_diff_internal(
|
|
this,
|
|
DiffKind::Unstaged,
|
|
staged_text.await.map(DiffBasesChange::SetIndex),
|
|
buffer,
|
|
cx,
|
|
)
|
|
.await
|
|
.map_err(Arc::new)
|
|
})
|
|
.shared()
|
|
})
|
|
.clone();
|
|
|
|
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
|
|
}
|
|
|
|
pub fn open_diff_since(
|
|
&mut self,
|
|
oid: Option<git::Oid>,
|
|
buffer: Entity<Buffer>,
|
|
repo: Entity<Repository>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<Entity<BufferDiff>>> {
|
|
let buffer_id = buffer.read(cx).remote_id();
|
|
|
|
if let Some(diff_state) = self.diffs.get(&buffer_id)
|
|
&& let Some(oid_diff) = diff_state.read(cx).oid_diff(oid)
|
|
{
|
|
if let Some(task) =
|
|
diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
|
|
{
|
|
return cx.background_executor().spawn(async move {
|
|
task.await;
|
|
Ok(oid_diff)
|
|
});
|
|
}
|
|
return Task::ready(Ok(oid_diff));
|
|
}
|
|
|
|
let diff_kind = DiffKind::SinceOid(oid);
|
|
if let Some(task) = self.loading_diffs.get(&(buffer_id, diff_kind)) {
|
|
let task = task.clone();
|
|
return cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) });
|
|
}
|
|
|
|
let task = cx
|
|
.spawn(async move |this, cx| {
|
|
let result: Result<Entity<BufferDiff>> = async {
|
|
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
|
let language_registry =
|
|
buffer.update(cx, |buffer, _| buffer.language_registry());
|
|
let content: Option<Arc<str>> = match oid {
|
|
None => None,
|
|
Some(oid) => Some(
|
|
repo.update(cx, |repo, cx| repo.load_blob_content(oid, cx))
|
|
.await?
|
|
.into(),
|
|
),
|
|
};
|
|
let buffer_diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, cx));
|
|
|
|
buffer_diff
|
|
.update(cx, |buffer_diff, cx| {
|
|
buffer_diff.language_changed(
|
|
buffer_snapshot.language().cloned(),
|
|
language_registry,
|
|
cx,
|
|
);
|
|
buffer_diff.set_base_text(
|
|
content.clone(),
|
|
buffer_snapshot.language().cloned(),
|
|
buffer_snapshot.text,
|
|
cx,
|
|
)
|
|
})
|
|
.await?;
|
|
let unstaged_diff = this
|
|
.update(cx, |this, cx| this.open_unstaged_diff(buffer.clone(), cx))?
|
|
.await?;
|
|
buffer_diff.update(cx, |buffer_diff, _| {
|
|
buffer_diff.set_secondary_diff(unstaged_diff);
|
|
});
|
|
|
|
this.update(cx, |this, cx| {
|
|
cx.subscribe(&buffer_diff, Self::on_buffer_diff_event)
|
|
.detach();
|
|
|
|
this.loading_diffs.remove(&(buffer_id, diff_kind));
|
|
|
|
let git_store = cx.weak_entity();
|
|
let diff_state = this
|
|
.diffs
|
|
.entry(buffer_id)
|
|
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
|
|
|
|
diff_state.update(cx, |state, _| {
|
|
if let Some(oid) = oid {
|
|
if let Some(content) = content {
|
|
state.oid_texts.insert(oid, content);
|
|
}
|
|
}
|
|
state.oid_diffs.insert(oid, buffer_diff.downgrade());
|
|
});
|
|
})?;
|
|
|
|
Ok(buffer_diff)
|
|
}
|
|
.await;
|
|
result.map_err(Arc::new)
|
|
})
|
|
.shared();
|
|
|
|
self.loading_diffs
|
|
.insert((buffer_id, diff_kind), task.clone());
|
|
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
|
|
}
|
|
|
|
#[ztracing::instrument(skip_all)]
|
|
pub fn open_uncommitted_diff(
|
|
&mut self,
|
|
buffer: Entity<Buffer>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<Entity<BufferDiff>>> {
|
|
let buffer_id = buffer.read(cx).remote_id();
|
|
|
|
if let Some(diff_state) = self.diffs.get(&buffer_id)
|
|
&& let Some(uncommitted_diff) = diff_state
|
|
.read(cx)
|
|
.uncommitted_diff
|
|
.as_ref()
|
|
.and_then(|weak| weak.upgrade())
|
|
{
|
|
if let Some(task) =
|
|
diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
|
|
{
|
|
return cx.background_executor().spawn(async move {
|
|
task.await;
|
|
Ok(uncommitted_diff)
|
|
});
|
|
}
|
|
return Task::ready(Ok(uncommitted_diff));
|
|
}
|
|
|
|
let Some((repo, repo_path)) =
|
|
self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
|
|
else {
|
|
return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
|
|
};
|
|
|
|
let task = self
|
|
.loading_diffs
|
|
.entry((buffer_id, DiffKind::Uncommitted))
|
|
.or_insert_with(|| {
|
|
let changes = repo.update(cx, |repo, cx| {
|
|
repo.load_committed_text(buffer_id, repo_path, cx)
|
|
});
|
|
|
|
// todo(lw): hot foreground spawn
|
|
cx.spawn(async move |this, cx| {
|
|
Self::open_diff_internal(this, DiffKind::Uncommitted, changes.await, buffer, cx)
|
|
.await
|
|
.map_err(Arc::new)
|
|
})
|
|
.shared()
|
|
})
|
|
.clone();
|
|
|
|
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
|
|
}
|
|
|
|
#[ztracing::instrument(skip_all)]
|
|
async fn open_diff_internal(
|
|
this: WeakEntity<Self>,
|
|
kind: DiffKind,
|
|
texts: Result<DiffBasesChange>,
|
|
buffer_entity: Entity<Buffer>,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<Entity<BufferDiff>> {
|
|
let diff_bases_change = match texts {
|
|
Err(e) => {
|
|
this.update(cx, |this, cx| {
|
|
let buffer = buffer_entity.read(cx);
|
|
let buffer_id = buffer.remote_id();
|
|
this.loading_diffs.remove(&(buffer_id, kind));
|
|
})?;
|
|
return Err(e);
|
|
}
|
|
Ok(change) => change,
|
|
};
|
|
|
|
this.update(cx, |this, cx| {
|
|
let buffer = buffer_entity.read(cx);
|
|
let buffer_id = buffer.remote_id();
|
|
let language = buffer.language().cloned();
|
|
let language_registry = buffer.language_registry();
|
|
let text_snapshot = buffer.text_snapshot();
|
|
this.loading_diffs.remove(&(buffer_id, kind));
|
|
|
|
let git_store = cx.weak_entity();
|
|
let diff_state = this
|
|
.diffs
|
|
.entry(buffer_id)
|
|
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
|
|
|
|
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
|
|
|
cx.subscribe(&diff, Self::on_buffer_diff_event).detach();
|
|
diff_state.update(cx, |diff_state, cx| {
|
|
diff_state.language_changed = true;
|
|
diff_state.language = language;
|
|
diff_state.language_registry = language_registry;
|
|
|
|
match kind {
|
|
DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()),
|
|
DiffKind::Uncommitted => {
|
|
let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() {
|
|
diff
|
|
} else {
|
|
let unstaged_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
|
diff_state.unstaged_diff = Some(unstaged_diff.downgrade());
|
|
unstaged_diff
|
|
};
|
|
|
|
diff.update(cx, |diff, _| diff.set_secondary_diff(unstaged_diff));
|
|
diff_state.uncommitted_diff = Some(diff.downgrade())
|
|
}
|
|
DiffKind::SinceOid(_) => {
|
|
unreachable!("open_diff_internal is not used for OID diffs")
|
|
}
|
|
}
|
|
|
|
diff_state.diff_bases_changed(text_snapshot, Some(diff_bases_change), cx);
|
|
let rx = diff_state.wait_for_recalculation();
|
|
|
|
anyhow::Ok(async move {
|
|
if let Some(rx) = rx {
|
|
rx.await;
|
|
}
|
|
Ok(diff)
|
|
})
|
|
})
|
|
})??
|
|
.await
|
|
}
|
|
|
|
pub fn get_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Option<Entity<BufferDiff>> {
|
|
let diff_state = self.diffs.get(&buffer_id)?;
|
|
diff_state.read(cx).unstaged_diff.as_ref()?.upgrade()
|
|
}
|
|
|
|
pub fn get_uncommitted_diff(
|
|
&self,
|
|
buffer_id: BufferId,
|
|
cx: &App,
|
|
) -> Option<Entity<BufferDiff>> {
|
|
let diff_state = self.diffs.get(&buffer_id)?;
|
|
diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
|
|
}
|
|
|
|
pub fn get_diff_since_oid(
|
|
&self,
|
|
buffer_id: BufferId,
|
|
oid: Option<git::Oid>,
|
|
cx: &App,
|
|
) -> Option<Entity<BufferDiff>> {
|
|
let diff_state = self.diffs.get(&buffer_id)?;
|
|
diff_state.read(cx).oid_diff(oid)
|
|
}
|
|
|
|
pub fn open_conflict_set(
|
|
&mut self,
|
|
buffer: Entity<Buffer>,
|
|
cx: &mut Context<Self>,
|
|
) -> Entity<ConflictSet> {
|
|
log::debug!("open conflict set");
|
|
let buffer_id = buffer.read(cx).remote_id();
|
|
|
|
if let Some(git_state) = self.diffs.get(&buffer_id)
|
|
&& let Some(conflict_set) = git_state
|
|
.read(cx)
|
|
.conflict_set
|
|
.as_ref()
|
|
.and_then(|weak| weak.upgrade())
|
|
{
|
|
let conflict_set = conflict_set;
|
|
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
|
|
|
git_state.update(cx, |state, cx| {
|
|
let _ = state.reparse_conflict_markers(buffer_snapshot, cx);
|
|
});
|
|
|
|
return conflict_set;
|
|
}
|
|
|
|
let is_unmerged = self
|
|
.repository_and_path_for_buffer_id(buffer_id, cx)
|
|
.is_some_and(|(repo, path)| repo.read(cx).snapshot.has_conflict(&path));
|
|
let git_store = cx.weak_entity();
|
|
let buffer_git_state = self
|
|
.diffs
|
|
.entry(buffer_id)
|
|
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
|
|
let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx));
|
|
|
|
self._subscriptions
|
|
.push(cx.subscribe(&conflict_set, |_, _, _, cx| {
|
|
cx.emit(GitStoreEvent::ConflictsUpdated);
|
|
}));
|
|
|
|
buffer_git_state.update(cx, |state, cx| {
|
|
state.conflict_set = Some(conflict_set.downgrade());
|
|
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
|
let _ = state.reparse_conflict_markers(buffer_snapshot, cx);
|
|
});
|
|
|
|
conflict_set
|
|
}
|
|
|
|
pub fn project_path_git_status(
|
|
&self,
|
|
project_path: &ProjectPath,
|
|
cx: &App,
|
|
) -> Option<FileStatus> {
|
|
let (repo, repo_path) = self.repository_and_path_for_project_path(project_path, cx)?;
|
|
Some(repo.read(cx).status_for_path(&repo_path)?.status)
|
|
}
|
|
|
|
pub fn checkpoint(&self, cx: &mut App) -> Task<Result<GitStoreCheckpoint>> {
|
|
let mut work_directory_abs_paths = Vec::new();
|
|
let mut checkpoints = Vec::new();
|
|
for repository in self.repositories.values() {
|
|
repository.update(cx, |repository, _| {
|
|
work_directory_abs_paths.push(repository.snapshot.work_directory_abs_path.clone());
|
|
checkpoints.push(repository.checkpoint().map(|checkpoint| checkpoint?));
|
|
});
|
|
}
|
|
|
|
cx.background_executor().spawn(async move {
|
|
let checkpoints = future::try_join_all(checkpoints).await?;
|
|
Ok(GitStoreCheckpoint {
|
|
checkpoints_by_work_dir_abs_path: work_directory_abs_paths
|
|
.into_iter()
|
|
.zip(checkpoints)
|
|
.collect(),
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn restore_checkpoint(
|
|
&self,
|
|
checkpoint: GitStoreCheckpoint,
|
|
cx: &mut App,
|
|
) -> Task<Result<()>> {
|
|
let repositories_by_work_dir_abs_path = self
|
|
.repositories
|
|
.values()
|
|
.map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
|
|
.collect::<HashMap<_, _>>();
|
|
|
|
let mut tasks = Vec::new();
|
|
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.update(cx, |repository, _| {
|
|
repository.restore_checkpoint(checkpoint)
|
|
});
|
|
tasks.push(async move { restore.await? });
|
|
}
|
|
}
|
|
cx.background_spawn(async move {
|
|
future::try_join_all(tasks).await?;
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
/// Compares two checkpoints, returning true if they are equal.
|
|
pub fn compare_checkpoints(
|
|
&self,
|
|
left: GitStoreCheckpoint,
|
|
mut right: GitStoreCheckpoint,
|
|
cx: &mut App,
|
|
) -> Task<Result<bool>> {
|
|
let repositories_by_work_dir_abs_path = self
|
|
.repositories
|
|
.values()
|
|
.map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
|
|
.collect::<HashMap<_, _>>();
|
|
|
|
let mut tasks = Vec::new();
|
|
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(&work_dir_abs_path)
|
|
{
|
|
if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
|
|
{
|
|
let compare = repository.update(cx, |repository, _| {
|
|
repository.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>,
|
|
version: Option<clock::Global>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<Option<Blame>>> {
|
|
let buffer = buffer.read(cx);
|
|
let Some((repo, repo_path)) =
|
|
self.repository_and_path_for_buffer_id(buffer.remote_id(), cx)
|
|
else {
|
|
return Task::ready(Err(anyhow!("failed to find a git repository for buffer")));
|
|
};
|
|
let content = match &version {
|
|
Some(version) => buffer.rope_for_version(version),
|
|
None => buffer.as_rope().clone(),
|
|
};
|
|
let line_ending = buffer.line_ending();
|
|
let version = version.unwrap_or(buffer.version());
|
|
let buffer_id = buffer.remote_id();
|
|
|
|
let repo = repo.downgrade();
|
|
cx.spawn(async move |_, cx| {
|
|
let repository_state = repo
|
|
.update(cx, |repo, _| repo.repository_state.clone())?
|
|
.await
|
|
.map_err(|err| anyhow::anyhow!(err))?;
|
|
match repository_state {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => backend
|
|
.blame(repo_path.clone(), content, line_ending)
|
|
.await
|
|
.with_context(|| format!("Failed to blame {:?}", repo_path.as_ref()))
|
|
.map(Some),
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::BlameBuffer {
|
|
project_id: project_id.to_proto(),
|
|
buffer_id: buffer_id.into(),
|
|
version: serialize_version(&version),
|
|
})
|
|
.await?;
|
|
Ok(deserialize_blame_buffer_response(response))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn file_history(
|
|
&self,
|
|
repo: &Entity<Repository>,
|
|
path: RepoPath,
|
|
cx: &mut App,
|
|
) -> Task<Result<git::repository::FileHistory>> {
|
|
let rx = repo.update(cx, |repo, _| repo.file_history(path));
|
|
|
|
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
|
}
|
|
|
|
pub fn file_history_paginated(
|
|
&self,
|
|
repo: &Entity<Repository>,
|
|
path: RepoPath,
|
|
skip: usize,
|
|
limit: Option<usize>,
|
|
cx: &mut App,
|
|
) -> Task<Result<git::repository::FileHistory>> {
|
|
let rx = repo.update(cx, |repo, _| repo.file_history_paginated(path, skip, limit));
|
|
|
|
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
|
}
|
|
|
|
pub fn get_permalink_to_line(
|
|
&self,
|
|
buffer: &Entity<Buffer>,
|
|
selection: Range<u32>,
|
|
cx: &mut App,
|
|
) -> Task<Result<url::Url>> {
|
|
let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
|
|
return Task::ready(Err(anyhow!("buffer has no file")));
|
|
};
|
|
|
|
let Some((repo, repo_path)) = self.repository_and_path_for_project_path(
|
|
&(file.worktree.read(cx).id(), file.path.clone()).into(),
|
|
cx,
|
|
) else {
|
|
// If we're not in a Git repo, check whether this is a Rust source
|
|
// file in the Cargo registry (presumably opened with go-to-definition
|
|
// from a normal Rust file). If so, we can put together a permalink
|
|
// using crate metadata.
|
|
if buffer
|
|
.read(cx)
|
|
.language()
|
|
.is_none_or(|lang| lang.name() != "Rust")
|
|
{
|
|
return Task::ready(Err(anyhow!("no permalink available")));
|
|
}
|
|
let file_path = file.worktree.read(cx).absolutize(&file.path);
|
|
return cx.spawn(async move |cx| {
|
|
let provider_registry = cx.update(GitHostingProviderRegistry::default_global);
|
|
get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
|
|
.context("no permalink available")
|
|
});
|
|
};
|
|
|
|
let buffer_id = buffer.read(cx).remote_id();
|
|
let branch = repo.read(cx).branch.clone();
|
|
let remote = branch
|
|
.as_ref()
|
|
.and_then(|b| b.upstream.as_ref())
|
|
.and_then(|b| b.remote_name())
|
|
.unwrap_or("origin")
|
|
.to_string();
|
|
|
|
let rx = repo.update(cx, |repo, _| {
|
|
repo.send_job(None, move |state, cx| async move {
|
|
match state {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
let origin_url = backend
|
|
.remote_url(&remote)
|
|
.await
|
|
.with_context(|| format!("remote \"{remote}\" not found"))?;
|
|
|
|
let sha = backend.head_sha().await.context("reading HEAD SHA")?;
|
|
|
|
let provider_registry =
|
|
cx.update(GitHostingProviderRegistry::default_global);
|
|
|
|
let (provider, remote) =
|
|
parse_git_remote_url(provider_registry, &origin_url)
|
|
.context("parsing Git remote URL")?;
|
|
|
|
Ok(provider.build_permalink(
|
|
remote,
|
|
BuildPermalinkParams::new(&sha, &repo_path, Some(selection)),
|
|
))
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::GetPermalinkToLine {
|
|
project_id: project_id.to_proto(),
|
|
buffer_id: buffer_id.into(),
|
|
selection: Some(proto::Range {
|
|
start: selection.start as u64,
|
|
end: selection.end as u64,
|
|
}),
|
|
})
|
|
.await?;
|
|
|
|
url::Url::parse(&response.permalink).context("failed to parse permalink")
|
|
}
|
|
}
|
|
})
|
|
});
|
|
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
|
}
|
|
|
|
fn downstream_client(&self) -> Option<(AnyProtoClient, ProjectId)> {
|
|
match &self.state {
|
|
GitStoreState::Local {
|
|
downstream: downstream_client,
|
|
..
|
|
} => downstream_client
|
|
.as_ref()
|
|
.map(|state| (state.client.clone(), state.project_id)),
|
|
GitStoreState::Remote {
|
|
downstream: downstream_client,
|
|
..
|
|
} => downstream_client.clone(),
|
|
}
|
|
}
|
|
|
|
fn upstream_client(&self) -> Option<AnyProtoClient> {
|
|
match &self.state {
|
|
GitStoreState::Local { .. } => None,
|
|
GitStoreState::Remote {
|
|
upstream_client, ..
|
|
} => Some(upstream_client.clone()),
|
|
}
|
|
}
|
|
|
|
fn on_worktree_store_event(
|
|
&mut self,
|
|
worktree_store: Entity<WorktreeStore>,
|
|
event: &WorktreeStoreEvent,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let GitStoreState::Local {
|
|
project_environment,
|
|
downstream,
|
|
next_repository_id,
|
|
fs,
|
|
} = &self.state
|
|
else {
|
|
return;
|
|
};
|
|
|
|
match event {
|
|
WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, updated_entries) => {
|
|
if let Some(worktree) = self
|
|
.worktree_store
|
|
.read(cx)
|
|
.worktree_for_id(*worktree_id, cx)
|
|
{
|
|
let paths_by_git_repo =
|
|
self.process_updated_entries(&worktree, updated_entries, cx);
|
|
let downstream = downstream
|
|
.as_ref()
|
|
.map(|downstream| downstream.updates_tx.clone());
|
|
cx.spawn(async move |_, cx| {
|
|
let paths_by_git_repo = paths_by_git_repo.await;
|
|
for (repo, paths) in paths_by_git_repo {
|
|
repo.update(cx, |repo, cx| {
|
|
repo.paths_changed(paths, downstream.clone(), cx);
|
|
});
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
}
|
|
WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => {
|
|
let Some(worktree) = worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
|
|
else {
|
|
return;
|
|
};
|
|
if !worktree.read(cx).is_visible() {
|
|
log::debug!(
|
|
"not adding repositories for local worktree {:?} because it's not visible",
|
|
worktree.read(cx).abs_path()
|
|
);
|
|
return;
|
|
}
|
|
self.update_repositories_from_worktree(
|
|
*worktree_id,
|
|
project_environment.clone(),
|
|
next_repository_id.clone(),
|
|
downstream
|
|
.as_ref()
|
|
.map(|downstream| downstream.updates_tx.clone()),
|
|
changed_repos.clone(),
|
|
fs.clone(),
|
|
cx,
|
|
);
|
|
self.local_worktree_git_repos_changed(worktree, changed_repos, cx);
|
|
}
|
|
WorktreeStoreEvent::WorktreeRemoved(_entity_id, worktree_id) => {
|
|
let repos_without_worktree: Vec<RepositoryId> = self
|
|
.worktree_ids
|
|
.iter_mut()
|
|
.filter_map(|(repo_id, worktree_ids)| {
|
|
worktree_ids.remove(worktree_id);
|
|
if worktree_ids.is_empty() {
|
|
Some(*repo_id)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
let is_active_repo_removed = repos_without_worktree
|
|
.iter()
|
|
.any(|repo_id| self.active_repo_id == Some(*repo_id));
|
|
|
|
for repo_id in repos_without_worktree {
|
|
self.repositories.remove(&repo_id);
|
|
self.worktree_ids.remove(&repo_id);
|
|
if let Some(updates_tx) =
|
|
downstream.as_ref().map(|downstream| &downstream.updates_tx)
|
|
{
|
|
updates_tx
|
|
.unbounded_send(DownstreamUpdate::RemoveRepository(repo_id))
|
|
.ok();
|
|
}
|
|
}
|
|
|
|
if is_active_repo_removed {
|
|
if let Some((&repo_id, _)) = self.repositories.iter().next() {
|
|
self.active_repo_id = Some(repo_id);
|
|
cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(repo_id)));
|
|
} else {
|
|
self.active_repo_id = None;
|
|
cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
fn on_repository_event(
|
|
&mut self,
|
|
repo: Entity<Repository>,
|
|
event: &RepositoryEvent,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let id = repo.read(cx).id;
|
|
let repo_snapshot = repo.read(cx).snapshot.clone();
|
|
for (buffer_id, diff) in self.diffs.iter() {
|
|
if let Some((buffer_repo, repo_path)) =
|
|
self.repository_and_path_for_buffer_id(*buffer_id, cx)
|
|
&& buffer_repo == repo
|
|
{
|
|
diff.update(cx, |diff, cx| {
|
|
if let Some(conflict_set) = &diff.conflict_set {
|
|
let conflict_status_changed =
|
|
conflict_set.update(cx, |conflict_set, cx| {
|
|
let has_conflict = repo_snapshot.has_conflict(&repo_path);
|
|
conflict_set.set_has_conflict(has_conflict, cx)
|
|
})?;
|
|
if conflict_status_changed {
|
|
let buffer_store = self.buffer_store.read(cx);
|
|
if let Some(buffer) = buffer_store.get(*buffer_id) {
|
|
let _ = diff
|
|
.reparse_conflict_markers(buffer.read(cx).text_snapshot(), cx);
|
|
}
|
|
}
|
|
}
|
|
anyhow::Ok(())
|
|
})
|
|
.ok();
|
|
}
|
|
}
|
|
cx.emit(GitStoreEvent::RepositoryUpdated(
|
|
id,
|
|
event.clone(),
|
|
self.active_repo_id == Some(id),
|
|
))
|
|
}
|
|
|
|
fn on_jobs_updated(&mut self, _: Entity<Repository>, _: &JobsUpdated, cx: &mut Context<Self>) {
|
|
cx.emit(GitStoreEvent::JobsUpdated)
|
|
}
|
|
|
|
/// Update our list of repositories and schedule git scans in response to a notification from a worktree,
|
|
fn update_repositories_from_worktree(
|
|
&mut self,
|
|
worktree_id: WorktreeId,
|
|
project_environment: Entity<ProjectEnvironment>,
|
|
next_repository_id: Arc<AtomicU64>,
|
|
updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
|
|
updated_git_repositories: UpdatedGitRepositoriesSet,
|
|
fs: Arc<dyn Fs>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let mut removed_ids = Vec::new();
|
|
for update in updated_git_repositories.iter() {
|
|
if let Some((id, existing)) = self.repositories.iter().find(|(_, repo)| {
|
|
let existing_work_directory_abs_path =
|
|
repo.read(cx).work_directory_abs_path.clone();
|
|
Some(&existing_work_directory_abs_path)
|
|
== update.old_work_directory_abs_path.as_ref()
|
|
|| Some(&existing_work_directory_abs_path)
|
|
== update.new_work_directory_abs_path.as_ref()
|
|
}) {
|
|
let repo_id = *id;
|
|
if let Some(new_work_directory_abs_path) =
|
|
update.new_work_directory_abs_path.clone()
|
|
{
|
|
self.worktree_ids
|
|
.entry(repo_id)
|
|
.or_insert_with(HashSet::new)
|
|
.insert(worktree_id);
|
|
existing.update(cx, |existing, cx| {
|
|
existing.snapshot.work_directory_abs_path = new_work_directory_abs_path;
|
|
existing.schedule_scan(updates_tx.clone(), cx);
|
|
});
|
|
} else {
|
|
if let Some(worktree_ids) = self.worktree_ids.get_mut(&repo_id) {
|
|
worktree_ids.remove(&worktree_id);
|
|
if worktree_ids.is_empty() {
|
|
removed_ids.push(repo_id);
|
|
}
|
|
}
|
|
}
|
|
} else if let UpdatedGitRepository {
|
|
new_work_directory_abs_path: Some(work_directory_abs_path),
|
|
dot_git_abs_path: Some(dot_git_abs_path),
|
|
repository_dir_abs_path: Some(repository_dir_abs_path),
|
|
common_dir_abs_path: Some(common_dir_abs_path),
|
|
..
|
|
} = update
|
|
{
|
|
let original_repo_abs_path: Arc<Path> = git::repository::original_repo_path(
|
|
work_directory_abs_path,
|
|
common_dir_abs_path,
|
|
repository_dir_abs_path,
|
|
)
|
|
.into();
|
|
let id = RepositoryId(next_repository_id.fetch_add(1, atomic::Ordering::Release));
|
|
let is_trusted = TrustedWorktrees::try_get_global(cx)
|
|
.map(|trusted_worktrees| {
|
|
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
|
trusted_worktrees.can_trust(&self.worktree_store, worktree_id, cx)
|
|
})
|
|
})
|
|
.unwrap_or(false);
|
|
let git_store = cx.weak_entity();
|
|
let repo = cx.new(|cx| {
|
|
let mut repo = Repository::local(
|
|
id,
|
|
work_directory_abs_path.clone(),
|
|
original_repo_abs_path.clone(),
|
|
dot_git_abs_path.clone(),
|
|
project_environment.downgrade(),
|
|
fs.clone(),
|
|
is_trusted,
|
|
git_store,
|
|
cx,
|
|
);
|
|
if let Some(updates_tx) = updates_tx.as_ref() {
|
|
// trigger an empty `UpdateRepository` to ensure remote active_repo_id is set correctly
|
|
updates_tx
|
|
.unbounded_send(DownstreamUpdate::UpdateRepository(repo.snapshot()))
|
|
.ok();
|
|
}
|
|
repo.schedule_scan(updates_tx.clone(), cx);
|
|
repo
|
|
});
|
|
self._subscriptions
|
|
.push(cx.subscribe(&repo, Self::on_repository_event));
|
|
self._subscriptions
|
|
.push(cx.subscribe(&repo, Self::on_jobs_updated));
|
|
self.repositories.insert(id, repo);
|
|
self.worktree_ids.insert(id, HashSet::from([worktree_id]));
|
|
cx.emit(GitStoreEvent::RepositoryAdded);
|
|
self.active_repo_id.get_or_insert_with(|| {
|
|
cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
|
|
id
|
|
});
|
|
}
|
|
}
|
|
|
|
for id in removed_ids {
|
|
if self.active_repo_id == Some(id) {
|
|
self.active_repo_id = None;
|
|
cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
|
|
}
|
|
self.repositories.remove(&id);
|
|
if let Some(updates_tx) = updates_tx.as_ref() {
|
|
updates_tx
|
|
.unbounded_send(DownstreamUpdate::RemoveRepository(id))
|
|
.ok();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn on_trusted_worktrees_event(
|
|
&mut self,
|
|
_: Entity<TrustedWorktreesStore>,
|
|
event: &TrustedWorktreesEvent,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if !matches!(self.state, GitStoreState::Local { .. }) {
|
|
return;
|
|
}
|
|
|
|
let (is_trusted, event_paths) = match event {
|
|
TrustedWorktreesEvent::Trusted(_, trusted_paths) => (true, trusted_paths),
|
|
TrustedWorktreesEvent::Restricted(_, restricted_paths) => (false, restricted_paths),
|
|
};
|
|
|
|
for (repo_id, worktree_ids) in &self.worktree_ids {
|
|
if worktree_ids
|
|
.iter()
|
|
.any(|worktree_id| event_paths.contains(&PathTrust::Worktree(*worktree_id)))
|
|
{
|
|
if let Some(repo) = self.repositories.get(repo_id) {
|
|
let repository_state = repo.read(cx).repository_state.clone();
|
|
cx.background_spawn(async move {
|
|
if let Ok(RepositoryState::Local(state)) = repository_state.await {
|
|
state.backend.set_trusted(is_trusted);
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn on_buffer_store_event(
|
|
&mut self,
|
|
_: Entity<BufferStore>,
|
|
event: &BufferStoreEvent,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match event {
|
|
BufferStoreEvent::BufferAdded(buffer) => {
|
|
cx.subscribe(buffer, |this, buffer, event, cx| {
|
|
if let BufferEvent::LanguageChanged(_) = event {
|
|
let buffer_id = buffer.read(cx).remote_id();
|
|
if let Some(diff_state) = this.diffs.get(&buffer_id) {
|
|
diff_state.update(cx, |diff_state, cx| {
|
|
diff_state.buffer_language_changed(buffer, cx);
|
|
});
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id) => {
|
|
if let Some(diffs) = self.shared_diffs.get_mut(peer_id) {
|
|
diffs.remove(buffer_id);
|
|
}
|
|
}
|
|
BufferStoreEvent::BufferDropped(buffer_id) => {
|
|
self.diffs.remove(buffer_id);
|
|
for diffs in self.shared_diffs.values_mut() {
|
|
diffs.remove(buffer_id);
|
|
}
|
|
}
|
|
BufferStoreEvent::BufferChangedFilePath { buffer, .. } => {
|
|
// Whenever a buffer's file path changes, it's possible that the
|
|
// new path is actually a path that is being tracked by a git
|
|
// repository. In that case, we'll want to update the buffer's
|
|
// `BufferDiffState`, in case it already has one.
|
|
let buffer_id = buffer.read(cx).remote_id();
|
|
let diff_state = self.diffs.get(&buffer_id);
|
|
let repo = self.repository_and_path_for_buffer_id(buffer_id, cx);
|
|
|
|
if let Some(diff_state) = diff_state
|
|
&& let Some((repo, repo_path)) = repo
|
|
{
|
|
let buffer = buffer.clone();
|
|
let diff_state = diff_state.clone();
|
|
|
|
cx.spawn(async move |_git_store, cx| {
|
|
async {
|
|
let diff_bases_change = repo
|
|
.update(cx, |repo, cx| {
|
|
repo.load_committed_text(buffer_id, repo_path, cx)
|
|
})
|
|
.await?;
|
|
|
|
diff_state.update(cx, |diff_state, cx| {
|
|
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
|
diff_state.diff_bases_changed(
|
|
buffer_snapshot,
|
|
Some(diff_bases_change),
|
|
cx,
|
|
);
|
|
});
|
|
anyhow::Ok(())
|
|
}
|
|
.await
|
|
.log_err();
|
|
})
|
|
.detach();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn recalculate_buffer_diffs(
|
|
&mut self,
|
|
buffers: Vec<Entity<Buffer>>,
|
|
cx: &mut Context<Self>,
|
|
) -> impl Future<Output = ()> + use<> {
|
|
let mut futures = Vec::new();
|
|
for buffer in buffers {
|
|
if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
|
|
let buffer = buffer.read(cx).text_snapshot();
|
|
diff_state.update(cx, |diff_state, cx| {
|
|
diff_state.recalculate_diffs(buffer.clone(), cx);
|
|
futures.extend(diff_state.wait_for_recalculation().map(FutureExt::boxed));
|
|
});
|
|
futures.push(diff_state.update(cx, |diff_state, cx| {
|
|
diff_state
|
|
.reparse_conflict_markers(buffer, cx)
|
|
.map(|_| {})
|
|
.boxed()
|
|
}));
|
|
}
|
|
}
|
|
async move {
|
|
futures::future::join_all(futures).await;
|
|
}
|
|
}
|
|
|
|
fn on_buffer_diff_event(
|
|
&mut self,
|
|
diff: Entity<buffer_diff::BufferDiff>,
|
|
event: &BufferDiffEvent,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
|
|
let buffer_id = diff.read(cx).buffer_id;
|
|
if let Some(diff_state) = self.diffs.get(&buffer_id) {
|
|
let hunk_staging_operation_count = diff_state.update(cx, |diff_state, _| {
|
|
diff_state.hunk_staging_operation_count += 1;
|
|
diff_state.hunk_staging_operation_count
|
|
});
|
|
if let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) {
|
|
let recv = repo.update(cx, |repo, cx| {
|
|
log::debug!("hunks changed for {}", path.as_unix_str());
|
|
repo.spawn_set_index_text_job(
|
|
path,
|
|
new_index_text.as_ref().map(|rope| rope.to_string()),
|
|
Some(hunk_staging_operation_count),
|
|
cx,
|
|
)
|
|
});
|
|
let diff = diff.downgrade();
|
|
cx.spawn(async move |this, cx| {
|
|
if let Ok(Err(error)) = cx.background_spawn(recv).await {
|
|
diff.update(cx, |diff, cx| {
|
|
diff.clear_pending_hunks(cx);
|
|
})
|
|
.ok();
|
|
this.update(cx, |_, cx| cx.emit(GitStoreEvent::IndexWriteError(error)))
|
|
.ok();
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn local_worktree_git_repos_changed(
|
|
&mut self,
|
|
worktree: Entity<Worktree>,
|
|
changed_repos: &UpdatedGitRepositoriesSet,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
log::debug!("local worktree repos changed");
|
|
debug_assert!(worktree.read(cx).is_local());
|
|
|
|
for repository in self.repositories.values() {
|
|
repository.update(cx, |repository, cx| {
|
|
let repo_abs_path = &repository.work_directory_abs_path;
|
|
if changed_repos.iter().any(|update| {
|
|
update.old_work_directory_abs_path.as_ref() == Some(repo_abs_path)
|
|
|| update.new_work_directory_abs_path.as_ref() == Some(repo_abs_path)
|
|
}) {
|
|
repository.reload_buffer_diff_bases(cx);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
pub fn repositories(&self) -> &HashMap<RepositoryId, Entity<Repository>> {
|
|
&self.repositories
|
|
}
|
|
|
|
/// Returns the original (main) repository working directory for the given worktree.
|
|
/// For normal checkouts this equals the worktree's own path; for linked
|
|
/// worktrees it points back to the original repo.
|
|
pub fn original_repo_path_for_worktree(
|
|
&self,
|
|
worktree_id: WorktreeId,
|
|
cx: &App,
|
|
) -> Option<Arc<Path>> {
|
|
self.active_repo_id
|
|
.iter()
|
|
.chain(self.worktree_ids.keys())
|
|
.find(|repo_id| {
|
|
self.worktree_ids
|
|
.get(repo_id)
|
|
.is_some_and(|ids| ids.contains(&worktree_id))
|
|
})
|
|
.and_then(|repo_id| self.repositories.get(repo_id))
|
|
.map(|repo| repo.read(cx).snapshot().original_repo_abs_path)
|
|
}
|
|
|
|
pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
|
|
let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?;
|
|
let status = repo.read(cx).snapshot.status_for_path(&path)?;
|
|
Some(status.status)
|
|
}
|
|
|
|
pub fn repository_and_path_for_buffer_id(
|
|
&self,
|
|
buffer_id: BufferId,
|
|
cx: &App,
|
|
) -> Option<(Entity<Repository>, RepoPath)> {
|
|
let buffer = self.buffer_store.read(cx).get(buffer_id)?;
|
|
let project_path = buffer.read(cx).project_path(cx)?;
|
|
self.repository_and_path_for_project_path(&project_path, cx)
|
|
}
|
|
|
|
pub fn repository_and_path_for_project_path(
|
|
&self,
|
|
path: &ProjectPath,
|
|
cx: &App,
|
|
) -> Option<(Entity<Repository>, RepoPath)> {
|
|
let abs_path = self.worktree_store.read(cx).absolutize(path, cx)?;
|
|
self.repositories
|
|
.values()
|
|
.filter_map(|repo| {
|
|
let repo_path = repo.read(cx).abs_path_to_repo_path(&abs_path)?;
|
|
Some((repo.clone(), repo_path))
|
|
})
|
|
.max_by_key(|(repo, _)| repo.read(cx).work_directory_abs_path.clone())
|
|
}
|
|
|
|
pub fn git_init(
|
|
&self,
|
|
path: Arc<Path>,
|
|
fallback_branch_name: String,
|
|
cx: &App,
|
|
) -> Task<Result<()>> {
|
|
match &self.state {
|
|
GitStoreState::Local { fs, .. } => {
|
|
let fs = fs.clone();
|
|
cx.background_executor()
|
|
.spawn(async move { fs.git_init(&path, fallback_branch_name).await })
|
|
}
|
|
GitStoreState::Remote {
|
|
upstream_client,
|
|
upstream_project_id: project_id,
|
|
..
|
|
} => {
|
|
let client = upstream_client.clone();
|
|
let project_id = *project_id;
|
|
cx.background_executor().spawn(async move {
|
|
client
|
|
.request(proto::GitInit {
|
|
project_id: project_id,
|
|
abs_path: path.to_string_lossy().into_owned(),
|
|
fallback_branch_name,
|
|
})
|
|
.await?;
|
|
Ok(())
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn git_clone(
|
|
&self,
|
|
repo: String,
|
|
path: impl Into<Arc<std::path::Path>>,
|
|
cx: &App,
|
|
) -> Task<Result<()>> {
|
|
let path = path.into();
|
|
match &self.state {
|
|
GitStoreState::Local { fs, .. } => {
|
|
let fs = fs.clone();
|
|
cx.background_executor()
|
|
.spawn(async move { fs.git_clone(&repo, &path).await })
|
|
}
|
|
GitStoreState::Remote {
|
|
upstream_client,
|
|
upstream_project_id,
|
|
..
|
|
} => {
|
|
if upstream_client.is_via_collab() {
|
|
return Task::ready(Err(anyhow!(
|
|
"Git Clone isn't supported for project guests"
|
|
)));
|
|
}
|
|
let request = upstream_client.request(proto::GitClone {
|
|
project_id: *upstream_project_id,
|
|
abs_path: path.to_string_lossy().into_owned(),
|
|
remote_repo: repo,
|
|
});
|
|
|
|
cx.background_spawn(async move {
|
|
let result = request.await?;
|
|
|
|
match result.success {
|
|
true => Ok(()),
|
|
false => Err(anyhow!("Git Clone failed")),
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn handle_update_repository(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::UpdateRepository>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<()> {
|
|
this.update(&mut cx, |this, cx| {
|
|
let path_style = this.worktree_store.read(cx).path_style();
|
|
let mut update = envelope.payload;
|
|
|
|
let id = RepositoryId::from_proto(update.id);
|
|
let client = this.upstream_client().context("no upstream client")?;
|
|
|
|
let original_repo_abs_path: Option<Arc<Path>> = update
|
|
.original_repo_abs_path
|
|
.as_deref()
|
|
.map(|p| Path::new(p).into());
|
|
|
|
let mut repo_subscription = None;
|
|
let repo = this.repositories.entry(id).or_insert_with(|| {
|
|
let git_store = cx.weak_entity();
|
|
let repo = cx.new(|cx| {
|
|
Repository::remote(
|
|
id,
|
|
Path::new(&update.abs_path).into(),
|
|
original_repo_abs_path.clone(),
|
|
path_style,
|
|
ProjectId(update.project_id),
|
|
client,
|
|
git_store,
|
|
cx,
|
|
)
|
|
});
|
|
repo_subscription = Some(cx.subscribe(&repo, Self::on_repository_event));
|
|
cx.emit(GitStoreEvent::RepositoryAdded);
|
|
repo
|
|
});
|
|
this._subscriptions.extend(repo_subscription);
|
|
|
|
repo.update(cx, {
|
|
let update = update.clone();
|
|
|repo, cx| repo.apply_remote_update(update, cx)
|
|
})?;
|
|
|
|
this.active_repo_id.get_or_insert_with(|| {
|
|
cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
|
|
id
|
|
});
|
|
|
|
if let Some((client, project_id)) = this.downstream_client() {
|
|
update.project_id = project_id.to_proto();
|
|
client.send(update).log_err();
|
|
}
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
async fn handle_remove_repository(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::RemoveRepository>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<()> {
|
|
this.update(&mut cx, |this, cx| {
|
|
let mut update = envelope.payload;
|
|
let id = RepositoryId::from_proto(update.id);
|
|
this.repositories.remove(&id);
|
|
if let Some((client, project_id)) = this.downstream_client() {
|
|
update.project_id = project_id.to_proto();
|
|
client.send(update).log_err();
|
|
}
|
|
if this.active_repo_id == Some(id) {
|
|
this.active_repo_id = None;
|
|
cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
|
|
}
|
|
cx.emit(GitStoreEvent::RepositoryRemoved(id));
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_git_init(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitInit>,
|
|
cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let path: Arc<Path> = PathBuf::from(envelope.payload.abs_path).into();
|
|
let name = envelope.payload.fallback_branch_name;
|
|
cx.update(|cx| this.read(cx).git_init(path, name, cx))
|
|
.await?;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_git_clone(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitClone>,
|
|
cx: AsyncApp,
|
|
) -> Result<proto::GitCloneResponse> {
|
|
let path: Arc<Path> = PathBuf::from(envelope.payload.abs_path).into();
|
|
let repo_name = envelope.payload.remote_repo;
|
|
let result = cx
|
|
.update(|cx| this.read(cx).git_clone(repo_name, path, cx))
|
|
.await;
|
|
|
|
Ok(proto::GitCloneResponse {
|
|
success: result.is_ok(),
|
|
})
|
|
}
|
|
|
|
async fn handle_fetch(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::Fetch>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::RemoteMessageResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let fetch_options = FetchOptions::from_proto(envelope.payload.remote);
|
|
let askpass_id = envelope.payload.askpass_id;
|
|
|
|
let askpass = make_remote_delegate(
|
|
this,
|
|
envelope.payload.project_id,
|
|
repository_id,
|
|
askpass_id,
|
|
&mut cx,
|
|
);
|
|
|
|
let remote_output = repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.fetch(fetch_options, askpass, cx)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::RemoteMessageResponse {
|
|
stdout: remote_output.stdout,
|
|
stderr: remote_output.stderr,
|
|
})
|
|
}
|
|
|
|
async fn handle_push(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::Push>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::RemoteMessageResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let askpass_id = envelope.payload.askpass_id;
|
|
let askpass = make_remote_delegate(
|
|
this,
|
|
envelope.payload.project_id,
|
|
repository_id,
|
|
askpass_id,
|
|
&mut cx,
|
|
);
|
|
|
|
let options = envelope
|
|
.payload
|
|
.options
|
|
.as_ref()
|
|
.map(|_| match envelope.payload.options() {
|
|
proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream,
|
|
proto::push::PushOptions::Force => git::repository::PushOptions::Force,
|
|
});
|
|
|
|
let branch_name = envelope.payload.branch_name.into();
|
|
let remote_branch_name = envelope.payload.remote_branch_name.into();
|
|
let remote_name = envelope.payload.remote_name.into();
|
|
|
|
let remote_output = repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.push(
|
|
branch_name,
|
|
remote_branch_name,
|
|
remote_name,
|
|
options,
|
|
askpass,
|
|
cx,
|
|
)
|
|
})
|
|
.await??;
|
|
Ok(proto::RemoteMessageResponse {
|
|
stdout: remote_output.stdout,
|
|
stderr: remote_output.stderr,
|
|
})
|
|
}
|
|
|
|
async fn handle_pull(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::Pull>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::RemoteMessageResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let askpass_id = envelope.payload.askpass_id;
|
|
let askpass = make_remote_delegate(
|
|
this,
|
|
envelope.payload.project_id,
|
|
repository_id,
|
|
askpass_id,
|
|
&mut cx,
|
|
);
|
|
|
|
let branch_name = envelope.payload.branch_name.map(|name| name.into());
|
|
let remote_name = envelope.payload.remote_name.into();
|
|
let rebase = envelope.payload.rebase;
|
|
|
|
let remote_message = repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.pull(branch_name, remote_name, rebase, askpass, cx)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::RemoteMessageResponse {
|
|
stdout: remote_message.stdout,
|
|
stderr: remote_message.stderr,
|
|
})
|
|
}
|
|
|
|
async fn handle_stage(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::Stage>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let entries = envelope
|
|
.payload
|
|
.paths
|
|
.into_iter()
|
|
.map(|path| RepoPath::new(&path))
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.stage_entries(entries, cx)
|
|
})
|
|
.await?;
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_unstage(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::Unstage>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let entries = envelope
|
|
.payload
|
|
.paths
|
|
.into_iter()
|
|
.map(|path| RepoPath::new(&path))
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.unstage_entries(entries, cx)
|
|
})
|
|
.await?;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_stash(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::Stash>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let entries = envelope
|
|
.payload
|
|
.paths
|
|
.into_iter()
|
|
.map(|path| RepoPath::new(&path))
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.stash_entries(entries, cx)
|
|
})
|
|
.await?;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_stash_pop(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::StashPop>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let stash_index = envelope.payload.stash_index.map(|i| i as usize);
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.stash_pop(stash_index, cx)
|
|
})
|
|
.await?;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_stash_apply(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::StashApply>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let stash_index = envelope.payload.stash_index.map(|i| i as usize);
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.stash_apply(stash_index, cx)
|
|
})
|
|
.await?;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_stash_drop(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::StashDrop>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let stash_index = envelope.payload.stash_index.map(|i| i as usize);
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.stash_drop(stash_index, cx)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_set_index_text(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::SetIndexText>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let repo_path = RepoPath::from_proto(&envelope.payload.path)?;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.spawn_set_index_text_job(
|
|
repo_path,
|
|
envelope.payload.text,
|
|
None,
|
|
cx,
|
|
)
|
|
})
|
|
.await??;
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_run_hook(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::RunGitHook>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let hook = RunHook::from_proto(envelope.payload.hook).context("invalid hook")?;
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.run_hook(hook, cx)
|
|
})
|
|
.await??;
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_commit(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::Commit>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let askpass_id = envelope.payload.askpass_id;
|
|
|
|
let askpass = make_remote_delegate(
|
|
this,
|
|
envelope.payload.project_id,
|
|
repository_id,
|
|
askpass_id,
|
|
&mut cx,
|
|
);
|
|
|
|
let message = SharedString::from(envelope.payload.message);
|
|
let name = envelope.payload.name.map(SharedString::from);
|
|
let email = envelope.payload.email.map(SharedString::from);
|
|
let options = envelope.payload.options.unwrap_or_default();
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.commit(
|
|
message,
|
|
name.zip(email),
|
|
CommitOptions {
|
|
amend: options.amend,
|
|
signoff: options.signoff,
|
|
},
|
|
askpass,
|
|
cx,
|
|
)
|
|
})
|
|
.await??;
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_get_remotes(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GetRemotes>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GetRemotesResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let branch_name = envelope.payload.branch_name;
|
|
let is_push = envelope.payload.is_push;
|
|
|
|
let remotes = repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.get_remotes(branch_name, is_push)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::GetRemotesResponse {
|
|
remotes: remotes
|
|
.into_iter()
|
|
.map(|remotes| proto::get_remotes_response::Remote {
|
|
name: remotes.name.to_string(),
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
})
|
|
}
|
|
|
|
async fn handle_get_worktrees(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitGetWorktrees>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GitWorktreesResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let worktrees = repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.worktrees()
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::GitWorktreesResponse {
|
|
worktrees: worktrees
|
|
.into_iter()
|
|
.map(|worktree| worktree_to_proto(&worktree))
|
|
.collect::<Vec<_>>(),
|
|
})
|
|
}
|
|
|
|
async fn handle_create_worktree(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitCreateWorktree>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let directory = PathBuf::from(envelope.payload.directory);
|
|
let name = envelope.payload.name;
|
|
let commit = envelope.payload.commit;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.create_worktree(name, directory, commit)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_remove_worktree(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitRemoveWorktree>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let path = PathBuf::from(envelope.payload.path);
|
|
let force = envelope.payload.force;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.remove_worktree(path, force)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_rename_worktree(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitRenameWorktree>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let old_path = PathBuf::from(envelope.payload.old_path);
|
|
let new_path = PathBuf::from(envelope.payload.new_path);
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.rename_worktree(old_path, new_path)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_get_branches(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitGetBranches>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GitBranchesResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let branches = repository_handle
|
|
.update(&mut cx, |repository_handle, _| repository_handle.branches())
|
|
.await??;
|
|
|
|
Ok(proto::GitBranchesResponse {
|
|
branches: branches
|
|
.into_iter()
|
|
.map(|branch| branch_to_proto(&branch))
|
|
.collect::<Vec<_>>(),
|
|
})
|
|
}
|
|
async fn handle_get_default_branch(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GetDefaultBranch>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GetDefaultBranchResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let branch = repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.default_branch(false)
|
|
})
|
|
.await??
|
|
.map(Into::into);
|
|
|
|
Ok(proto::GetDefaultBranchResponse { branch })
|
|
}
|
|
async fn handle_create_branch(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitCreateBranch>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let branch_name = envelope.payload.branch_name;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.create_branch(branch_name, None)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_change_branch(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitChangeBranch>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let branch_name = envelope.payload.branch_name;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.change_branch(branch_name)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_rename_branch(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitRenameBranch>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let branch = envelope.payload.branch;
|
|
let new_name = envelope.payload.new_name;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.rename_branch(branch, new_name)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_create_remote(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitCreateRemote>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let remote_name = envelope.payload.remote_name;
|
|
let remote_url = envelope.payload.remote_url;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.create_remote(remote_name, remote_url)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_delete_branch(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitDeleteBranch>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let is_remote = envelope.payload.is_remote;
|
|
let branch_name = envelope.payload.branch_name;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.delete_branch(is_remote, branch_name)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_remove_remote(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitRemoveRemote>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let remote_name = envelope.payload.remote_name;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.remove_remote(remote_name)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_show(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitShow>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GitCommitDetails> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let commit = repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.show(envelope.payload.commit)
|
|
})
|
|
.await??;
|
|
Ok(proto::GitCommitDetails {
|
|
sha: commit.sha.into(),
|
|
message: commit.message.into(),
|
|
commit_timestamp: commit.commit_timestamp,
|
|
author_email: commit.author_email.into(),
|
|
author_name: commit.author_name.into(),
|
|
})
|
|
}
|
|
|
|
async fn handle_create_checkpoint(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitCreateCheckpoint>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GitCreateCheckpointResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let checkpoint = repository_handle
|
|
.update(&mut cx, |repository, _| repository.checkpoint())
|
|
.await??;
|
|
|
|
Ok(proto::GitCreateCheckpointResponse {
|
|
commit_sha: checkpoint.commit_sha.as_bytes().to_vec(),
|
|
})
|
|
}
|
|
|
|
async fn handle_restore_checkpoint(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitRestoreCheckpoint>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let checkpoint = GitRepositoryCheckpoint {
|
|
commit_sha: Oid::from_bytes(&envelope.payload.commit_sha)?,
|
|
};
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository, _| {
|
|
repository.restore_checkpoint(checkpoint)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_compare_checkpoints(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitCompareCheckpoints>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GitCompareCheckpointsResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let left = GitRepositoryCheckpoint {
|
|
commit_sha: Oid::from_bytes(&envelope.payload.left_commit_sha)?,
|
|
};
|
|
let right = GitRepositoryCheckpoint {
|
|
commit_sha: Oid::from_bytes(&envelope.payload.right_commit_sha)?,
|
|
};
|
|
|
|
let equal = repository_handle
|
|
.update(&mut cx, |repository, _| {
|
|
repository.compare_checkpoints(left, right)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::GitCompareCheckpointsResponse { equal })
|
|
}
|
|
|
|
async fn handle_diff_checkpoints(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitDiffCheckpoints>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GitDiffCheckpointsResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let base = GitRepositoryCheckpoint {
|
|
commit_sha: Oid::from_bytes(&envelope.payload.base_commit_sha)?,
|
|
};
|
|
let target = GitRepositoryCheckpoint {
|
|
commit_sha: Oid::from_bytes(&envelope.payload.target_commit_sha)?,
|
|
};
|
|
|
|
let diff = repository_handle
|
|
.update(&mut cx, |repository, _| {
|
|
repository.diff_checkpoints(base, target)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::GitDiffCheckpointsResponse { diff })
|
|
}
|
|
|
|
async fn handle_load_commit_diff(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::LoadCommitDiff>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::LoadCommitDiffResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let commit_diff = repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.load_commit_diff(envelope.payload.commit)
|
|
})
|
|
.await??;
|
|
Ok(proto::LoadCommitDiffResponse {
|
|
files: commit_diff
|
|
.files
|
|
.into_iter()
|
|
.map(|file| proto::CommitFile {
|
|
path: file.path.to_proto(),
|
|
old_text: file.old_text,
|
|
new_text: file.new_text,
|
|
is_binary: file.is_binary,
|
|
})
|
|
.collect(),
|
|
})
|
|
}
|
|
|
|
async fn handle_file_history(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitFileHistory>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GitFileHistoryResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let path = RepoPath::from_proto(&envelope.payload.path)?;
|
|
let skip = envelope.payload.skip as usize;
|
|
let limit = envelope.payload.limit.map(|l| l as usize);
|
|
|
|
let file_history = repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.file_history_paginated(path, skip, limit)
|
|
})
|
|
.await??;
|
|
|
|
Ok(proto::GitFileHistoryResponse {
|
|
entries: file_history
|
|
.entries
|
|
.into_iter()
|
|
.map(|entry| proto::FileHistoryEntry {
|
|
sha: entry.sha.to_string(),
|
|
subject: entry.subject.to_string(),
|
|
message: entry.message.to_string(),
|
|
commit_timestamp: entry.commit_timestamp,
|
|
author_name: entry.author_name.to_string(),
|
|
author_email: entry.author_email.to_string(),
|
|
})
|
|
.collect(),
|
|
path: file_history.path.to_proto(),
|
|
})
|
|
}
|
|
|
|
async fn handle_reset(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitReset>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let mode = match envelope.payload.mode() {
|
|
git_reset::ResetMode::Soft => ResetMode::Soft,
|
|
git_reset::ResetMode::Mixed => ResetMode::Mixed,
|
|
};
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.reset(envelope.payload.commit, mode, cx)
|
|
})
|
|
.await??;
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_checkout_files(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitCheckoutFiles>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let paths = envelope
|
|
.payload
|
|
.paths
|
|
.iter()
|
|
.map(|s| RepoPath::from_proto(s))
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.checkout_files(&envelope.payload.commit, paths, cx)
|
|
})
|
|
.await?;
|
|
Ok(proto::Ack {})
|
|
}
|
|
|
|
async fn handle_open_commit_message_buffer(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::OpenBufferResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let buffer = repository
|
|
.update(&mut cx, |repository, cx| {
|
|
repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
|
|
})
|
|
.await?;
|
|
|
|
let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id());
|
|
this.update(&mut cx, |this, cx| {
|
|
this.buffer_store.update(cx, |buffer_store, cx| {
|
|
buffer_store
|
|
.create_buffer_for_peer(
|
|
&buffer,
|
|
envelope.original_sender_id.unwrap_or(envelope.sender_id),
|
|
cx,
|
|
)
|
|
.detach_and_log_err(cx);
|
|
})
|
|
});
|
|
|
|
Ok(proto::OpenBufferResponse {
|
|
buffer_id: buffer_id.to_proto(),
|
|
})
|
|
}
|
|
|
|
async fn handle_askpass(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::AskPassRequest>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::AskPassResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let delegates = cx.update(|cx| repository.read(cx).askpass_delegates.clone());
|
|
let Some(mut askpass) = delegates.lock().remove(&envelope.payload.askpass_id) else {
|
|
debug_panic!("no askpass found");
|
|
anyhow::bail!("no askpass found");
|
|
};
|
|
|
|
let response = askpass
|
|
.ask_password(envelope.payload.prompt)
|
|
.await
|
|
.ok_or_else(|| anyhow::anyhow!("askpass cancelled"))?;
|
|
|
|
delegates
|
|
.lock()
|
|
.insert(envelope.payload.askpass_id, askpass);
|
|
|
|
// In fact, we don't quite know what we're doing here, as we're sending askpass password unencrypted, but..
|
|
Ok(proto::AskPassResponse {
|
|
response: response.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)?,
|
|
})
|
|
}
|
|
|
|
async fn handle_check_for_pushed_commits(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::CheckForPushedCommits>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::CheckForPushedCommitsResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
|
|
let branches = repository_handle
|
|
.update(&mut cx, |repository_handle, _| {
|
|
repository_handle.check_for_pushed_commits()
|
|
})
|
|
.await??;
|
|
Ok(proto::CheckForPushedCommitsResponse {
|
|
pushed_to: branches
|
|
.into_iter()
|
|
.map(|commit| commit.to_string())
|
|
.collect(),
|
|
})
|
|
}
|
|
|
|
async fn handle_git_diff(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GitDiff>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GitDiffResponse> {
|
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
|
let diff_type = match envelope.payload.diff_type() {
|
|
proto::git_diff::DiffType::HeadToIndex => DiffType::HeadToIndex,
|
|
proto::git_diff::DiffType::HeadToWorktree => DiffType::HeadToWorktree,
|
|
proto::git_diff::DiffType::MergeBase => {
|
|
let base_ref = envelope
|
|
.payload
|
|
.merge_base_ref
|
|
.ok_or_else(|| anyhow!("merge_base_ref is required for MergeBase diff type"))?;
|
|
DiffType::MergeBase {
|
|
base_ref: base_ref.into(),
|
|
}
|
|
}
|
|
};
|
|
|
|
let mut diff = repository_handle
|
|
.update(&mut cx, |repository_handle, cx| {
|
|
repository_handle.diff(diff_type, cx)
|
|
})
|
|
.await??;
|
|
const ONE_MB: usize = 1_000_000;
|
|
if diff.len() > ONE_MB {
|
|
diff = diff.chars().take(ONE_MB).collect()
|
|
}
|
|
|
|
Ok(proto::GitDiffResponse { diff })
|
|
}
|
|
|
|
async fn handle_tree_diff(
|
|
this: Entity<Self>,
|
|
request: TypedEnvelope<proto::GetTreeDiff>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GetTreeDiffResponse> {
|
|
let repository_id = RepositoryId(request.payload.repository_id);
|
|
let diff_type = if request.payload.is_merge {
|
|
DiffTreeType::MergeBase {
|
|
base: request.payload.base.into(),
|
|
head: request.payload.head.into(),
|
|
}
|
|
} else {
|
|
DiffTreeType::Since {
|
|
base: request.payload.base.into(),
|
|
head: request.payload.head.into(),
|
|
}
|
|
};
|
|
|
|
let diff = this
|
|
.update(&mut cx, |this, cx| {
|
|
let repository = this.repositories().get(&repository_id)?;
|
|
Some(repository.update(cx, |repo, cx| repo.diff_tree(diff_type, cx)))
|
|
})
|
|
.context("missing repository")?
|
|
.await??;
|
|
|
|
Ok(proto::GetTreeDiffResponse {
|
|
entries: diff
|
|
.entries
|
|
.into_iter()
|
|
.map(|(path, status)| proto::TreeDiffStatus {
|
|
path: path.as_ref().to_proto(),
|
|
status: match status {
|
|
TreeDiffStatus::Added {} => proto::tree_diff_status::Status::Added.into(),
|
|
TreeDiffStatus::Modified { .. } => {
|
|
proto::tree_diff_status::Status::Modified.into()
|
|
}
|
|
TreeDiffStatus::Deleted { .. } => {
|
|
proto::tree_diff_status::Status::Deleted.into()
|
|
}
|
|
},
|
|
oid: match status {
|
|
TreeDiffStatus::Deleted { old } | TreeDiffStatus::Modified { old } => {
|
|
Some(old.to_string())
|
|
}
|
|
TreeDiffStatus::Added => None,
|
|
},
|
|
})
|
|
.collect(),
|
|
})
|
|
}
|
|
|
|
async fn handle_get_blob_content(
|
|
this: Entity<Self>,
|
|
request: TypedEnvelope<proto::GetBlobContent>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GetBlobContentResponse> {
|
|
let oid = git::Oid::from_str(&request.payload.oid)?;
|
|
let repository_id = RepositoryId(request.payload.repository_id);
|
|
let content = this
|
|
.update(&mut cx, |this, cx| {
|
|
let repository = this.repositories().get(&repository_id)?;
|
|
Some(repository.update(cx, |repo, cx| repo.load_blob_content(oid, cx)))
|
|
})
|
|
.context("missing repository")?
|
|
.await?;
|
|
Ok(proto::GetBlobContentResponse { content })
|
|
}
|
|
|
|
async fn handle_open_unstaged_diff(
|
|
this: Entity<Self>,
|
|
request: TypedEnvelope<proto::OpenUnstagedDiff>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::OpenUnstagedDiffResponse> {
|
|
let buffer_id = BufferId::new(request.payload.buffer_id)?;
|
|
let diff = this
|
|
.update(&mut cx, |this, cx| {
|
|
let buffer = this.buffer_store.read(cx).get(buffer_id)?;
|
|
Some(this.open_unstaged_diff(buffer, cx))
|
|
})
|
|
.context("missing buffer")?
|
|
.await?;
|
|
this.update(&mut cx, |this, _| {
|
|
let shared_diffs = this
|
|
.shared_diffs
|
|
.entry(request.original_sender_id.unwrap_or(request.sender_id))
|
|
.or_default();
|
|
shared_diffs.entry(buffer_id).or_default().unstaged = Some(diff.clone());
|
|
});
|
|
let staged_text = diff.read_with(&cx, |diff, cx| diff.base_text_string(cx));
|
|
Ok(proto::OpenUnstagedDiffResponse { staged_text })
|
|
}
|
|
|
|
async fn handle_open_uncommitted_diff(
|
|
this: Entity<Self>,
|
|
request: TypedEnvelope<proto::OpenUncommittedDiff>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::OpenUncommittedDiffResponse> {
|
|
let buffer_id = BufferId::new(request.payload.buffer_id)?;
|
|
let diff = this
|
|
.update(&mut cx, |this, cx| {
|
|
let buffer = this.buffer_store.read(cx).get(buffer_id)?;
|
|
Some(this.open_uncommitted_diff(buffer, cx))
|
|
})
|
|
.context("missing buffer")?
|
|
.await?;
|
|
this.update(&mut cx, |this, _| {
|
|
let shared_diffs = this
|
|
.shared_diffs
|
|
.entry(request.original_sender_id.unwrap_or(request.sender_id))
|
|
.or_default();
|
|
shared_diffs.entry(buffer_id).or_default().uncommitted = Some(diff.clone());
|
|
});
|
|
Ok(diff.read_with(&cx, |diff, cx| {
|
|
use proto::open_uncommitted_diff_response::Mode;
|
|
|
|
let unstaged_diff = diff.secondary_diff();
|
|
let index_snapshot = unstaged_diff.and_then(|diff| {
|
|
let diff = diff.read(cx);
|
|
diff.base_text_exists().then(|| diff.base_text(cx))
|
|
});
|
|
|
|
let mode;
|
|
let staged_text;
|
|
let committed_text;
|
|
if diff.base_text_exists() {
|
|
let committed_snapshot = diff.base_text(cx);
|
|
committed_text = Some(committed_snapshot.text());
|
|
if let Some(index_text) = index_snapshot {
|
|
if index_text.remote_id() == committed_snapshot.remote_id() {
|
|
mode = Mode::IndexMatchesHead;
|
|
staged_text = None;
|
|
} else {
|
|
mode = Mode::IndexAndHead;
|
|
staged_text = Some(index_text.text());
|
|
}
|
|
} else {
|
|
mode = Mode::IndexAndHead;
|
|
staged_text = None;
|
|
}
|
|
} else {
|
|
mode = Mode::IndexAndHead;
|
|
committed_text = None;
|
|
staged_text = index_snapshot.as_ref().map(|buffer| buffer.text());
|
|
}
|
|
|
|
proto::OpenUncommittedDiffResponse {
|
|
committed_text,
|
|
staged_text,
|
|
mode: mode.into(),
|
|
}
|
|
}))
|
|
}
|
|
|
|
async fn handle_update_diff_bases(
|
|
this: Entity<Self>,
|
|
request: TypedEnvelope<proto::UpdateDiffBases>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<()> {
|
|
let buffer_id = BufferId::new(request.payload.buffer_id)?;
|
|
this.update(&mut cx, |this, cx| {
|
|
if let Some(diff_state) = this.diffs.get_mut(&buffer_id)
|
|
&& let Some(buffer) = this.buffer_store.read(cx).get(buffer_id)
|
|
{
|
|
let buffer = buffer.read(cx).text_snapshot();
|
|
diff_state.update(cx, |diff_state, cx| {
|
|
diff_state.handle_base_texts_updated(buffer, request.payload, cx);
|
|
})
|
|
}
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_blame_buffer(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::BlameBuffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::BlameBufferResponse> {
|
|
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
|
|
let version = deserialize_version(&envelope.payload.version);
|
|
let buffer = this.read_with(&cx, |this, cx| {
|
|
this.buffer_store.read(cx).get_existing(buffer_id)
|
|
})?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(version.clone())
|
|
})
|
|
.await?;
|
|
let blame = this
|
|
.update(&mut cx, |this, cx| {
|
|
this.blame_buffer(&buffer, Some(version), cx)
|
|
})
|
|
.await?;
|
|
Ok(serialize_blame_buffer_response(blame))
|
|
}
|
|
|
|
async fn handle_get_permalink_to_line(
|
|
this: Entity<Self>,
|
|
envelope: TypedEnvelope<proto::GetPermalinkToLine>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::GetPermalinkToLineResponse> {
|
|
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
|
|
// let version = deserialize_version(&envelope.payload.version);
|
|
let selection = {
|
|
let proto_selection = envelope
|
|
.payload
|
|
.selection
|
|
.context("no selection to get permalink for defined")?;
|
|
proto_selection.start as u32..proto_selection.end as u32
|
|
};
|
|
let buffer = this.read_with(&cx, |this, cx| {
|
|
this.buffer_store.read(cx).get_existing(buffer_id)
|
|
})?;
|
|
let permalink = this
|
|
.update(&mut cx, |this, cx| {
|
|
this.get_permalink_to_line(&buffer, selection, cx)
|
|
})
|
|
.await?;
|
|
Ok(proto::GetPermalinkToLineResponse {
|
|
permalink: permalink.to_string(),
|
|
})
|
|
}
|
|
|
|
fn repository_for_request(
|
|
this: &Entity<Self>,
|
|
id: RepositoryId,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<Entity<Repository>> {
|
|
this.read_with(cx, |this, _| {
|
|
this.repositories
|
|
.get(&id)
|
|
.context("missing repository handle")
|
|
.cloned()
|
|
})
|
|
}
|
|
|
|
pub fn repo_snapshots(&self, cx: &App) -> HashMap<RepositoryId, RepositorySnapshot> {
|
|
self.repositories
|
|
.iter()
|
|
.map(|(id, repo)| (*id, repo.read(cx).snapshot.clone()))
|
|
.collect()
|
|
}
|
|
|
|
fn process_updated_entries(
|
|
&self,
|
|
worktree: &Entity<Worktree>,
|
|
updated_entries: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
|
|
cx: &mut App,
|
|
) -> Task<HashMap<Entity<Repository>, Vec<RepoPath>>> {
|
|
let path_style = worktree.read(cx).path_style();
|
|
let mut repo_paths = self
|
|
.repositories
|
|
.values()
|
|
.map(|repo| (repo.read(cx).work_directory_abs_path.clone(), repo.clone()))
|
|
.collect::<Vec<_>>();
|
|
let mut entries: Vec<_> = updated_entries
|
|
.iter()
|
|
.map(|(path, _, _)| path.clone())
|
|
.collect();
|
|
entries.sort();
|
|
let worktree = worktree.read(cx);
|
|
|
|
let entries = entries
|
|
.into_iter()
|
|
.map(|path| worktree.absolutize(&path))
|
|
.collect::<Arc<[_]>>();
|
|
|
|
let executor = cx.background_executor().clone();
|
|
cx.background_executor().spawn(async move {
|
|
repo_paths.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0));
|
|
let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default();
|
|
let mut tasks = FuturesOrdered::new();
|
|
for (repo_path, repo) in repo_paths.into_iter().rev() {
|
|
let entries = entries.clone();
|
|
let task = executor.spawn(async move {
|
|
// Find all repository paths that belong to this repo
|
|
let mut ix = entries.partition_point(|path| path < &*repo_path);
|
|
if ix == entries.len() {
|
|
return None;
|
|
};
|
|
|
|
let mut paths = Vec::new();
|
|
// All paths prefixed by a given repo will constitute a continuous range.
|
|
while let Some(path) = entries.get(ix)
|
|
&& let Some(repo_path) = RepositorySnapshot::abs_path_to_repo_path_inner(
|
|
&repo_path, path, path_style,
|
|
)
|
|
{
|
|
paths.push((repo_path, ix));
|
|
ix += 1;
|
|
}
|
|
if paths.is_empty() {
|
|
None
|
|
} else {
|
|
Some((repo, paths))
|
|
}
|
|
});
|
|
tasks.push_back(task);
|
|
}
|
|
|
|
// Now, let's filter out the "duplicate" entries that were processed by multiple distinct repos.
|
|
let mut path_was_used = vec![false; entries.len()];
|
|
let tasks = tasks.collect::<Vec<_>>().await;
|
|
// Process tasks from the back: iterating backwards allows us to see more-specific paths first.
|
|
// We always want to assign a path to it's innermost repository.
|
|
for t in tasks {
|
|
let Some((repo, paths)) = t else {
|
|
continue;
|
|
};
|
|
let entry = paths_by_git_repo.entry(repo).or_default();
|
|
for (repo_path, ix) in paths {
|
|
if path_was_used[ix] {
|
|
continue;
|
|
}
|
|
path_was_used[ix] = true;
|
|
entry.push(repo_path);
|
|
}
|
|
}
|
|
|
|
paths_by_git_repo
|
|
})
|
|
}
|
|
}
|
|
|
|
impl BufferGitState {
|
|
fn new(_git_store: WeakEntity<GitStore>) -> Self {
|
|
Self {
|
|
unstaged_diff: Default::default(),
|
|
uncommitted_diff: Default::default(),
|
|
oid_diffs: Default::default(),
|
|
recalculate_diff_task: Default::default(),
|
|
language: Default::default(),
|
|
language_registry: Default::default(),
|
|
recalculating_tx: postage::watch::channel_with(false).0,
|
|
hunk_staging_operation_count: 0,
|
|
hunk_staging_operation_count_as_of_write: 0,
|
|
head_text: Default::default(),
|
|
index_text: Default::default(),
|
|
oid_texts: Default::default(),
|
|
head_changed: Default::default(),
|
|
index_changed: Default::default(),
|
|
language_changed: Default::default(),
|
|
conflict_updated_futures: Default::default(),
|
|
conflict_set: Default::default(),
|
|
reparse_conflict_markers_task: Default::default(),
|
|
}
|
|
}
|
|
|
|
#[ztracing::instrument(skip_all)]
|
|
fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
|
self.language = buffer.read(cx).language().cloned();
|
|
self.language_changed = true;
|
|
let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
|
|
}
|
|
|
|
fn reparse_conflict_markers(
|
|
&mut self,
|
|
buffer: text::BufferSnapshot,
|
|
cx: &mut Context<Self>,
|
|
) -> oneshot::Receiver<()> {
|
|
let (tx, rx) = oneshot::channel();
|
|
|
|
let Some(conflict_set) = self
|
|
.conflict_set
|
|
.as_ref()
|
|
.and_then(|conflict_set| conflict_set.upgrade())
|
|
else {
|
|
return rx;
|
|
};
|
|
|
|
let old_snapshot = conflict_set.read_with(cx, |conflict_set, _| {
|
|
if conflict_set.has_conflict {
|
|
Some(conflict_set.snapshot())
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
if let Some(old_snapshot) = old_snapshot {
|
|
self.conflict_updated_futures.push(tx);
|
|
self.reparse_conflict_markers_task = Some(cx.spawn(async move |this, cx| {
|
|
let (snapshot, changed_range) = cx
|
|
.background_spawn(async move {
|
|
let new_snapshot = ConflictSet::parse(&buffer);
|
|
let changed_range = old_snapshot.compare(&new_snapshot, &buffer);
|
|
(new_snapshot, changed_range)
|
|
})
|
|
.await;
|
|
this.update(cx, |this, cx| {
|
|
if let Some(conflict_set) = &this.conflict_set {
|
|
conflict_set
|
|
.update(cx, |conflict_set, cx| {
|
|
conflict_set.set_snapshot(snapshot, changed_range, cx);
|
|
})
|
|
.ok();
|
|
}
|
|
let futures = std::mem::take(&mut this.conflict_updated_futures);
|
|
for tx in futures {
|
|
tx.send(()).ok();
|
|
}
|
|
})
|
|
}))
|
|
}
|
|
|
|
rx
|
|
}
|
|
|
|
fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
|
|
self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
|
|
}
|
|
|
|
fn uncommitted_diff(&self) -> Option<Entity<BufferDiff>> {
|
|
self.uncommitted_diff.as_ref().and_then(|set| set.upgrade())
|
|
}
|
|
|
|
fn oid_diff(&self, oid: Option<git::Oid>) -> Option<Entity<BufferDiff>> {
|
|
self.oid_diffs.get(&oid).and_then(|weak| weak.upgrade())
|
|
}
|
|
|
|
fn handle_base_texts_updated(
|
|
&mut self,
|
|
buffer: text::BufferSnapshot,
|
|
message: proto::UpdateDiffBases,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
use proto::update_diff_bases::Mode;
|
|
|
|
let Some(mode) = Mode::from_i32(message.mode) else {
|
|
return;
|
|
};
|
|
|
|
let diff_bases_change = match mode {
|
|
Mode::HeadOnly => DiffBasesChange::SetHead(message.committed_text),
|
|
Mode::IndexOnly => DiffBasesChange::SetIndex(message.staged_text),
|
|
Mode::IndexMatchesHead => DiffBasesChange::SetBoth(message.committed_text),
|
|
Mode::IndexAndHead => DiffBasesChange::SetEach {
|
|
index: message.staged_text,
|
|
head: message.committed_text,
|
|
},
|
|
};
|
|
|
|
self.diff_bases_changed(buffer, Some(diff_bases_change), cx);
|
|
}
|
|
|
|
pub fn wait_for_recalculation(&mut self) -> Option<impl Future<Output = ()> + use<>> {
|
|
if *self.recalculating_tx.borrow() {
|
|
let mut rx = self.recalculating_tx.subscribe();
|
|
Some(async move {
|
|
loop {
|
|
let is_recalculating = rx.recv().await;
|
|
if is_recalculating != Some(true) {
|
|
break;
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn diff_bases_changed(
|
|
&mut self,
|
|
buffer: text::BufferSnapshot,
|
|
diff_bases_change: Option<DiffBasesChange>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match diff_bases_change {
|
|
Some(DiffBasesChange::SetIndex(index)) => {
|
|
self.index_text = index.map(|mut index| {
|
|
text::LineEnding::normalize(&mut index);
|
|
Arc::from(index.as_str())
|
|
});
|
|
self.index_changed = true;
|
|
}
|
|
Some(DiffBasesChange::SetHead(head)) => {
|
|
self.head_text = head.map(|mut head| {
|
|
text::LineEnding::normalize(&mut head);
|
|
Arc::from(head.as_str())
|
|
});
|
|
self.head_changed = true;
|
|
}
|
|
Some(DiffBasesChange::SetBoth(text)) => {
|
|
let text = text.map(|mut text| {
|
|
text::LineEnding::normalize(&mut text);
|
|
Arc::from(text.as_str())
|
|
});
|
|
self.head_text = text.clone();
|
|
self.index_text = text;
|
|
self.head_changed = true;
|
|
self.index_changed = true;
|
|
}
|
|
Some(DiffBasesChange::SetEach { index, head }) => {
|
|
self.index_text = index.map(|mut index| {
|
|
text::LineEnding::normalize(&mut index);
|
|
Arc::from(index.as_str())
|
|
});
|
|
self.index_changed = true;
|
|
self.head_text = head.map(|mut head| {
|
|
text::LineEnding::normalize(&mut head);
|
|
Arc::from(head.as_str())
|
|
});
|
|
self.head_changed = true;
|
|
}
|
|
None => {}
|
|
}
|
|
|
|
self.recalculate_diffs(buffer, cx)
|
|
}
|
|
|
|
#[ztracing::instrument(skip_all)]
|
|
fn recalculate_diffs(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
|
|
*self.recalculating_tx.borrow_mut() = true;
|
|
|
|
let language = self.language.clone();
|
|
let language_registry = self.language_registry.clone();
|
|
let unstaged_diff = self.unstaged_diff();
|
|
let uncommitted_diff = self.uncommitted_diff();
|
|
let head = self.head_text.clone();
|
|
let index = self.index_text.clone();
|
|
let index_changed = self.index_changed;
|
|
let head_changed = self.head_changed;
|
|
let language_changed = self.language_changed;
|
|
let prev_hunk_staging_operation_count = self.hunk_staging_operation_count_as_of_write;
|
|
let index_matches_head = match (self.index_text.as_ref(), self.head_text.as_ref()) {
|
|
(Some(index), Some(head)) => Arc::ptr_eq(index, head),
|
|
(None, None) => true,
|
|
_ => false,
|
|
};
|
|
|
|
let oid_diffs: Vec<(Option<git::Oid>, Entity<BufferDiff>, Option<Arc<str>>)> = self
|
|
.oid_diffs
|
|
.iter()
|
|
.filter_map(|(oid, weak)| {
|
|
let base_text = oid.and_then(|oid| self.oid_texts.get(&oid).cloned());
|
|
weak.upgrade().map(|diff| (*oid, diff, base_text))
|
|
})
|
|
.collect();
|
|
|
|
self.oid_diffs.retain(|oid, weak| {
|
|
let alive = weak.upgrade().is_some();
|
|
if !alive {
|
|
if let Some(oid) = oid {
|
|
self.oid_texts.remove(oid);
|
|
}
|
|
}
|
|
alive
|
|
});
|
|
self.recalculate_diff_task = Some(cx.spawn(async move |this, cx| {
|
|
log::debug!(
|
|
"start recalculating diffs for buffer {}",
|
|
buffer.remote_id()
|
|
);
|
|
|
|
let mut new_unstaged_diff = None;
|
|
if let Some(unstaged_diff) = &unstaged_diff {
|
|
new_unstaged_diff = Some(
|
|
cx.update(|cx| {
|
|
unstaged_diff.read(cx).update_diff(
|
|
buffer.clone(),
|
|
index,
|
|
index_changed.then_some(false),
|
|
language.clone(),
|
|
cx,
|
|
)
|
|
})
|
|
.await,
|
|
);
|
|
}
|
|
|
|
// Dropping BufferDiff can be expensive, so yield back to the event loop
|
|
// for a bit
|
|
yield_now().await;
|
|
|
|
let mut new_uncommitted_diff = None;
|
|
if let Some(uncommitted_diff) = &uncommitted_diff {
|
|
new_uncommitted_diff = if index_matches_head {
|
|
new_unstaged_diff.clone()
|
|
} else {
|
|
Some(
|
|
cx.update(|cx| {
|
|
uncommitted_diff.read(cx).update_diff(
|
|
buffer.clone(),
|
|
head,
|
|
head_changed.then_some(true),
|
|
language.clone(),
|
|
cx,
|
|
)
|
|
})
|
|
.await,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Dropping BufferDiff can be expensive, so yield back to the event loop
|
|
// for a bit
|
|
yield_now().await;
|
|
|
|
let cancel = this.update(cx, |this, _| {
|
|
// This checks whether all pending stage/unstage operations
|
|
// have quiesced (i.e. both the corresponding write and the
|
|
// read of that write have completed). If not, then we cancel
|
|
// this recalculation attempt to avoid invalidating pending
|
|
// state too quickly; another recalculation will come along
|
|
// later and clear the pending state once the state of the index has settled.
|
|
if this.hunk_staging_operation_count > prev_hunk_staging_operation_count {
|
|
*this.recalculating_tx.borrow_mut() = false;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
})?;
|
|
if cancel {
|
|
log::debug!(
|
|
concat!(
|
|
"aborting recalculating diffs for buffer {}",
|
|
"due to subsequent hunk operations",
|
|
),
|
|
buffer.remote_id()
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
|
|
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
|
|
{
|
|
let task = unstaged_diff.update(cx, |diff, cx| {
|
|
// For git index buffer we skip assigning the language as we do not really need to perform any syntax highlighting on
|
|
// it. As a result, by skipping it we are potentially shaving off a lot of RSS plus we get a snappier feel for large diff
|
|
// view multibuffers.
|
|
diff.set_snapshot(new_unstaged_diff, &buffer, cx)
|
|
});
|
|
Some(task.await)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
yield_now().await;
|
|
|
|
if let Some((uncommitted_diff, new_uncommitted_diff)) =
|
|
uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
|
|
{
|
|
uncommitted_diff
|
|
.update(cx, |diff, cx| {
|
|
if language_changed {
|
|
diff.language_changed(language.clone(), language_registry, cx);
|
|
}
|
|
diff.set_snapshot_with_secondary(
|
|
new_uncommitted_diff,
|
|
&buffer,
|
|
unstaged_changed_range.flatten(),
|
|
true,
|
|
cx,
|
|
)
|
|
})
|
|
.await;
|
|
}
|
|
|
|
yield_now().await;
|
|
|
|
for (oid, oid_diff, base_text) in oid_diffs {
|
|
let new_oid_diff = cx
|
|
.update(|cx| {
|
|
oid_diff.read(cx).update_diff(
|
|
buffer.clone(),
|
|
base_text,
|
|
None,
|
|
language.clone(),
|
|
cx,
|
|
)
|
|
})
|
|
.await;
|
|
|
|
oid_diff
|
|
.update(cx, |diff, cx| diff.set_snapshot(new_oid_diff, &buffer, cx))
|
|
.await;
|
|
|
|
log::debug!(
|
|
"finished recalculating oid diff for buffer {} oid {:?}",
|
|
buffer.remote_id(),
|
|
oid
|
|
);
|
|
|
|
yield_now().await;
|
|
}
|
|
|
|
log::debug!(
|
|
"finished recalculating diffs for buffer {}",
|
|
buffer.remote_id()
|
|
);
|
|
|
|
if let Some(this) = this.upgrade() {
|
|
this.update(cx, |this, _| {
|
|
this.index_changed = false;
|
|
this.head_changed = false;
|
|
this.language_changed = false;
|
|
*this.recalculating_tx.borrow_mut() = false;
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}));
|
|
}
|
|
}
|
|
|
|
fn make_remote_delegate(
|
|
this: Entity<GitStore>,
|
|
project_id: u64,
|
|
repository_id: RepositoryId,
|
|
askpass_id: u64,
|
|
cx: &mut AsyncApp,
|
|
) -> AskPassDelegate {
|
|
AskPassDelegate::new(cx, move |prompt, tx, cx| {
|
|
this.update(cx, |this, cx| {
|
|
let Some((client, _)) = this.downstream_client() else {
|
|
return;
|
|
};
|
|
let response = client.request(proto::AskPassRequest {
|
|
project_id,
|
|
repository_id: repository_id.to_proto(),
|
|
askpass_id,
|
|
prompt,
|
|
});
|
|
cx.spawn(async move |_, _| {
|
|
let mut response = response.await?.response;
|
|
tx.send(EncryptedPassword::try_from(response.as_ref())?)
|
|
.ok();
|
|
response.zeroize();
|
|
anyhow::Ok(())
|
|
})
|
|
.detach_and_log_err(cx);
|
|
});
|
|
})
|
|
}
|
|
|
|
impl RepositoryId {
|
|
pub fn to_proto(self) -> u64 {
|
|
self.0
|
|
}
|
|
|
|
pub fn from_proto(id: u64) -> Self {
|
|
RepositoryId(id)
|
|
}
|
|
}
|
|
|
|
impl RepositorySnapshot {
|
|
fn empty(
|
|
id: RepositoryId,
|
|
work_directory_abs_path: Arc<Path>,
|
|
original_repo_abs_path: Option<Arc<Path>>,
|
|
path_style: PathStyle,
|
|
) -> Self {
|
|
Self {
|
|
id,
|
|
statuses_by_path: Default::default(),
|
|
original_repo_abs_path: original_repo_abs_path
|
|
.unwrap_or_else(|| work_directory_abs_path.clone()),
|
|
work_directory_abs_path,
|
|
branch: None,
|
|
head_commit: None,
|
|
scan_id: 0,
|
|
merge: Default::default(),
|
|
remote_origin_url: None,
|
|
remote_upstream_url: None,
|
|
stash_entries: Default::default(),
|
|
linked_worktrees: Arc::from([]),
|
|
path_style,
|
|
}
|
|
}
|
|
|
|
fn initial_update(&self, project_id: u64) -> proto::UpdateRepository {
|
|
proto::UpdateRepository {
|
|
branch_summary: self.branch.as_ref().map(branch_to_proto),
|
|
head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto),
|
|
updated_statuses: self
|
|
.statuses_by_path
|
|
.iter()
|
|
.map(|entry| entry.to_proto())
|
|
.collect(),
|
|
removed_statuses: Default::default(),
|
|
current_merge_conflicts: self
|
|
.merge
|
|
.merge_heads_by_conflicted_path
|
|
.iter()
|
|
.map(|(repo_path, _)| repo_path.to_proto())
|
|
.collect(),
|
|
merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
|
|
project_id,
|
|
id: self.id.to_proto(),
|
|
abs_path: self.work_directory_abs_path.to_string_lossy().into_owned(),
|
|
entry_ids: vec![self.id.to_proto()],
|
|
scan_id: self.scan_id,
|
|
is_last_update: true,
|
|
stash_entries: self
|
|
.stash_entries
|
|
.entries
|
|
.iter()
|
|
.map(stash_to_proto)
|
|
.collect(),
|
|
remote_upstream_url: self.remote_upstream_url.clone(),
|
|
remote_origin_url: self.remote_origin_url.clone(),
|
|
original_repo_abs_path: Some(
|
|
self.original_repo_abs_path.to_string_lossy().into_owned(),
|
|
),
|
|
linked_worktrees: self
|
|
.linked_worktrees
|
|
.iter()
|
|
.map(worktree_to_proto)
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
fn build_update(&self, old: &Self, project_id: u64) -> proto::UpdateRepository {
|
|
let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();
|
|
let mut removed_statuses: Vec<String> = Vec::new();
|
|
|
|
let mut new_statuses = self.statuses_by_path.iter().peekable();
|
|
let mut old_statuses = old.statuses_by_path.iter().peekable();
|
|
|
|
let mut current_new_entry = new_statuses.next();
|
|
let mut current_old_entry = old_statuses.next();
|
|
loop {
|
|
match (current_new_entry, current_old_entry) {
|
|
(Some(new_entry), Some(old_entry)) => {
|
|
match new_entry.repo_path.cmp(&old_entry.repo_path) {
|
|
Ordering::Less => {
|
|
updated_statuses.push(new_entry.to_proto());
|
|
current_new_entry = new_statuses.next();
|
|
}
|
|
Ordering::Equal => {
|
|
if new_entry.status != old_entry.status
|
|
|| new_entry.diff_stat != old_entry.diff_stat
|
|
{
|
|
updated_statuses.push(new_entry.to_proto());
|
|
}
|
|
current_old_entry = old_statuses.next();
|
|
current_new_entry = new_statuses.next();
|
|
}
|
|
Ordering::Greater => {
|
|
removed_statuses.push(old_entry.repo_path.to_proto());
|
|
current_old_entry = old_statuses.next();
|
|
}
|
|
}
|
|
}
|
|
(None, Some(old_entry)) => {
|
|
removed_statuses.push(old_entry.repo_path.to_proto());
|
|
current_old_entry = old_statuses.next();
|
|
}
|
|
(Some(new_entry), None) => {
|
|
updated_statuses.push(new_entry.to_proto());
|
|
current_new_entry = new_statuses.next();
|
|
}
|
|
(None, None) => break,
|
|
}
|
|
}
|
|
|
|
proto::UpdateRepository {
|
|
branch_summary: self.branch.as_ref().map(branch_to_proto),
|
|
head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto),
|
|
updated_statuses,
|
|
removed_statuses,
|
|
current_merge_conflicts: self
|
|
.merge
|
|
.merge_heads_by_conflicted_path
|
|
.iter()
|
|
.map(|(path, _)| path.to_proto())
|
|
.collect(),
|
|
merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
|
|
project_id,
|
|
id: self.id.to_proto(),
|
|
abs_path: self.work_directory_abs_path.to_string_lossy().into_owned(),
|
|
entry_ids: vec![],
|
|
scan_id: self.scan_id,
|
|
is_last_update: true,
|
|
stash_entries: self
|
|
.stash_entries
|
|
.entries
|
|
.iter()
|
|
.map(stash_to_proto)
|
|
.collect(),
|
|
remote_upstream_url: self.remote_upstream_url.clone(),
|
|
remote_origin_url: self.remote_origin_url.clone(),
|
|
original_repo_abs_path: Some(
|
|
self.original_repo_abs_path.to_string_lossy().into_owned(),
|
|
),
|
|
linked_worktrees: self
|
|
.linked_worktrees
|
|
.iter()
|
|
.map(worktree_to_proto)
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
/// The main worktree is the original checkout that other worktrees were
|
|
/// created from.
|
|
///
|
|
/// For example, if you had both `~/code/zed` and `~/code/worktrees/zed-2`,
|
|
/// then `~/code/zed` is the main worktree and `~/code/worktrees/zed-2` is a linked worktree.
|
|
///
|
|
/// Submodules also return `true` here, since they are not linked worktrees.
|
|
pub fn is_main_worktree(&self) -> bool {
|
|
self.work_directory_abs_path == self.original_repo_abs_path
|
|
}
|
|
|
|
/// Returns true if this repository is a linked worktree, that is, one that
|
|
/// was created from another worktree.
|
|
///
|
|
/// Returns `false` for both the main worktree and submodules.
|
|
pub fn is_linked_worktree(&self) -> bool {
|
|
!self.is_main_worktree()
|
|
}
|
|
|
|
pub fn linked_worktrees(&self) -> &[GitWorktree] {
|
|
&self.linked_worktrees
|
|
}
|
|
|
|
pub fn status(&self) -> impl Iterator<Item = StatusEntry> + '_ {
|
|
self.statuses_by_path.iter().cloned()
|
|
}
|
|
|
|
pub fn status_summary(&self) -> GitSummary {
|
|
self.statuses_by_path.summary().item_summary
|
|
}
|
|
|
|
pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
|
|
self.statuses_by_path
|
|
.get(&PathKey(path.as_ref().clone()), ())
|
|
.cloned()
|
|
}
|
|
|
|
pub fn diff_stat_for_path(&self, path: &RepoPath) -> Option<DiffStat> {
|
|
self.statuses_by_path
|
|
.get(&PathKey(path.as_ref().clone()), ())
|
|
.and_then(|entry| entry.diff_stat)
|
|
}
|
|
|
|
pub fn abs_path_to_repo_path(&self, abs_path: &Path) -> Option<RepoPath> {
|
|
Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path, self.path_style)
|
|
}
|
|
|
|
fn repo_path_to_abs_path(&self, repo_path: &RepoPath) -> PathBuf {
|
|
self.path_style
|
|
.join(&self.work_directory_abs_path, repo_path.as_std_path())
|
|
.unwrap()
|
|
.into()
|
|
}
|
|
|
|
#[inline]
|
|
fn abs_path_to_repo_path_inner(
|
|
work_directory_abs_path: &Path,
|
|
abs_path: &Path,
|
|
path_style: PathStyle,
|
|
) -> Option<RepoPath> {
|
|
let rel_path = path_style.strip_prefix(abs_path, work_directory_abs_path)?;
|
|
Some(RepoPath::from_rel_path(&rel_path))
|
|
}
|
|
|
|
pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool {
|
|
self.merge
|
|
.merge_heads_by_conflicted_path
|
|
.contains_key(repo_path)
|
|
}
|
|
|
|
pub fn has_conflict(&self, repo_path: &RepoPath) -> bool {
|
|
let had_conflict_on_last_merge_head_change = self
|
|
.merge
|
|
.merge_heads_by_conflicted_path
|
|
.contains_key(repo_path);
|
|
let has_conflict_currently = self
|
|
.status_for_path(repo_path)
|
|
.is_some_and(|entry| entry.status.is_conflicted());
|
|
had_conflict_on_last_merge_head_change || has_conflict_currently
|
|
}
|
|
|
|
/// This is the name that will be displayed in the repository selector for this repository.
|
|
pub fn display_name(&self) -> SharedString {
|
|
self.work_directory_abs_path
|
|
.file_name()
|
|
.unwrap_or_default()
|
|
.to_string_lossy()
|
|
.to_string()
|
|
.into()
|
|
}
|
|
}
|
|
|
|
pub fn stash_to_proto(entry: &StashEntry) -> proto::StashEntry {
|
|
proto::StashEntry {
|
|
oid: entry.oid.as_bytes().to_vec(),
|
|
message: entry.message.clone(),
|
|
branch: entry.branch.clone(),
|
|
index: entry.index as u64,
|
|
timestamp: entry.timestamp,
|
|
}
|
|
}
|
|
|
|
pub fn proto_to_stash(entry: &proto::StashEntry) -> Result<StashEntry> {
|
|
Ok(StashEntry {
|
|
oid: Oid::from_bytes(&entry.oid)?,
|
|
message: entry.message.clone(),
|
|
index: entry.index as usize,
|
|
branch: entry.branch.clone(),
|
|
timestamp: entry.timestamp,
|
|
})
|
|
}
|
|
|
|
impl MergeDetails {
|
|
async fn update(
|
|
&mut self,
|
|
backend: &Arc<dyn GitRepository>,
|
|
current_conflicted_paths: Vec<RepoPath>,
|
|
) -> Result<bool> {
|
|
log::debug!("load merge details");
|
|
self.message = backend.merge_message().await.map(SharedString::from);
|
|
let heads = backend
|
|
.revparse_batch(vec![
|
|
"MERGE_HEAD".into(),
|
|
"CHERRY_PICK_HEAD".into(),
|
|
"REBASE_HEAD".into(),
|
|
"REVERT_HEAD".into(),
|
|
"APPLY_HEAD".into(),
|
|
])
|
|
.await
|
|
.log_err()
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.map(|opt| opt.map(SharedString::from))
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut conflicts_changed = false;
|
|
|
|
// Record the merge state for newly conflicted paths
|
|
for path in ¤t_conflicted_paths {
|
|
if self.merge_heads_by_conflicted_path.get(&path).is_none() {
|
|
conflicts_changed = true;
|
|
self.merge_heads_by_conflicted_path
|
|
.insert(path.clone(), heads.clone());
|
|
}
|
|
}
|
|
|
|
// Clear state for paths that are no longer conflicted and for which the merge heads have changed
|
|
self.merge_heads_by_conflicted_path
|
|
.retain(|path, old_merge_heads| {
|
|
let keep = current_conflicted_paths.contains(path)
|
|
|| (old_merge_heads == &heads
|
|
&& old_merge_heads.iter().any(|head| head.is_some()));
|
|
if !keep {
|
|
conflicts_changed = true;
|
|
}
|
|
keep
|
|
});
|
|
|
|
Ok(conflicts_changed)
|
|
}
|
|
}
|
|
|
|
impl Repository {
|
|
pub fn is_trusted(&self) -> bool {
|
|
match self.repository_state.peek() {
|
|
Some(Ok(RepositoryState::Local(state))) => state.backend.is_trusted(),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn snapshot(&self) -> RepositorySnapshot {
|
|
self.snapshot.clone()
|
|
}
|
|
|
|
pub fn pending_ops(&self) -> impl Iterator<Item = PendingOps> + '_ {
|
|
self.pending_ops.iter().cloned()
|
|
}
|
|
|
|
pub fn pending_ops_summary(&self) -> PathSummary<PendingOpsSummary> {
|
|
self.pending_ops.summary().clone()
|
|
}
|
|
|
|
pub fn pending_ops_for_path(&self, path: &RepoPath) -> Option<PendingOps> {
|
|
self.pending_ops
|
|
.get(&PathKey(path.as_ref().clone()), ())
|
|
.cloned()
|
|
}
|
|
|
|
fn local(
|
|
id: RepositoryId,
|
|
work_directory_abs_path: Arc<Path>,
|
|
original_repo_abs_path: Arc<Path>,
|
|
dot_git_abs_path: Arc<Path>,
|
|
project_environment: WeakEntity<ProjectEnvironment>,
|
|
fs: Arc<dyn Fs>,
|
|
is_trusted: bool,
|
|
git_store: WeakEntity<GitStore>,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let snapshot = RepositorySnapshot::empty(
|
|
id,
|
|
work_directory_abs_path.clone(),
|
|
Some(original_repo_abs_path),
|
|
PathStyle::local(),
|
|
);
|
|
let state = cx
|
|
.spawn(async move |_, cx| {
|
|
LocalRepositoryState::new(
|
|
work_directory_abs_path,
|
|
dot_git_abs_path,
|
|
project_environment,
|
|
fs,
|
|
is_trusted,
|
|
cx,
|
|
)
|
|
.await
|
|
.map_err(|err| err.to_string())
|
|
})
|
|
.shared();
|
|
let job_sender = Repository::spawn_local_git_worker(state.clone(), cx);
|
|
let state = cx
|
|
.spawn(async move |_, _| {
|
|
let state = state.await?;
|
|
Ok(RepositoryState::Local(state))
|
|
})
|
|
.shared();
|
|
|
|
cx.subscribe_self(move |this, event: &RepositoryEvent, _| match event {
|
|
RepositoryEvent::BranchChanged => {
|
|
if this.scan_id > 1 {
|
|
this.initial_graph_data.clear();
|
|
}
|
|
}
|
|
_ => {}
|
|
})
|
|
.detach();
|
|
|
|
Repository {
|
|
this: cx.weak_entity(),
|
|
git_store,
|
|
snapshot,
|
|
pending_ops: Default::default(),
|
|
repository_state: state,
|
|
commit_message_buffer: None,
|
|
askpass_delegates: Default::default(),
|
|
paths_needing_status_update: Default::default(),
|
|
latest_askpass_id: 0,
|
|
job_sender,
|
|
job_id: 0,
|
|
active_jobs: Default::default(),
|
|
initial_graph_data: Default::default(),
|
|
commit_data: Default::default(),
|
|
graph_commit_data_handler: GraphCommitHandlerState::Closed,
|
|
}
|
|
}
|
|
|
|
fn remote(
|
|
id: RepositoryId,
|
|
work_directory_abs_path: Arc<Path>,
|
|
original_repo_abs_path: Option<Arc<Path>>,
|
|
path_style: PathStyle,
|
|
project_id: ProjectId,
|
|
client: AnyProtoClient,
|
|
git_store: WeakEntity<GitStore>,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let snapshot = RepositorySnapshot::empty(
|
|
id,
|
|
work_directory_abs_path,
|
|
original_repo_abs_path,
|
|
path_style,
|
|
);
|
|
let repository_state = RemoteRepositoryState { project_id, client };
|
|
let job_sender = Self::spawn_remote_git_worker(repository_state.clone(), cx);
|
|
let repository_state = Task::ready(Ok(RepositoryState::Remote(repository_state))).shared();
|
|
Self {
|
|
this: cx.weak_entity(),
|
|
snapshot,
|
|
commit_message_buffer: None,
|
|
git_store,
|
|
pending_ops: Default::default(),
|
|
paths_needing_status_update: Default::default(),
|
|
job_sender,
|
|
repository_state,
|
|
askpass_delegates: Default::default(),
|
|
latest_askpass_id: 0,
|
|
active_jobs: Default::default(),
|
|
job_id: 0,
|
|
initial_graph_data: Default::default(),
|
|
commit_data: Default::default(),
|
|
graph_commit_data_handler: GraphCommitHandlerState::Closed,
|
|
}
|
|
}
|
|
|
|
pub fn git_store(&self) -> Option<Entity<GitStore>> {
|
|
self.git_store.upgrade()
|
|
}
|
|
|
|
fn reload_buffer_diff_bases(&mut self, cx: &mut Context<Self>) {
|
|
let this = cx.weak_entity();
|
|
let git_store = self.git_store.clone();
|
|
let _ = self.send_keyed_job(
|
|
Some(GitJobKey::ReloadBufferDiffBases),
|
|
None,
|
|
|state, mut cx| async move {
|
|
let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else {
|
|
log::error!("tried to recompute diffs for a non-local repository");
|
|
return Ok(());
|
|
};
|
|
|
|
let Some(this) = this.upgrade() else {
|
|
return Ok(());
|
|
};
|
|
|
|
let repo_diff_state_updates = this.update(&mut cx, |this, cx| {
|
|
git_store.update(cx, |git_store, cx| {
|
|
git_store
|
|
.diffs
|
|
.iter()
|
|
.filter_map(|(buffer_id, diff_state)| {
|
|
let buffer_store = git_store.buffer_store.read(cx);
|
|
let buffer = buffer_store.get(*buffer_id)?;
|
|
let file = File::from_dyn(buffer.read(cx).file())?;
|
|
let abs_path = file.worktree.read(cx).absolutize(&file.path);
|
|
let repo_path = this.abs_path_to_repo_path(&abs_path)?;
|
|
log::debug!(
|
|
"start reload diff bases for repo path {}",
|
|
repo_path.as_unix_str()
|
|
);
|
|
diff_state.update(cx, |diff_state, _| {
|
|
let has_unstaged_diff = diff_state
|
|
.unstaged_diff
|
|
.as_ref()
|
|
.is_some_and(|diff| diff.is_upgradable());
|
|
let has_uncommitted_diff = diff_state
|
|
.uncommitted_diff
|
|
.as_ref()
|
|
.is_some_and(|set| set.is_upgradable());
|
|
|
|
Some((
|
|
buffer,
|
|
repo_path,
|
|
has_unstaged_diff.then(|| diff_state.index_text.clone()),
|
|
has_uncommitted_diff.then(|| diff_state.head_text.clone()),
|
|
))
|
|
})
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
})?;
|
|
|
|
let buffer_diff_base_changes = cx
|
|
.background_spawn(async move {
|
|
let mut changes = Vec::new();
|
|
for (buffer, repo_path, current_index_text, current_head_text) in
|
|
&repo_diff_state_updates
|
|
{
|
|
let index_text = if current_index_text.is_some() {
|
|
backend.load_index_text(repo_path.clone()).await
|
|
} else {
|
|
None
|
|
};
|
|
let head_text = if current_head_text.is_some() {
|
|
backend.load_committed_text(repo_path.clone()).await
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let change =
|
|
match (current_index_text.as_ref(), current_head_text.as_ref()) {
|
|
(Some(current_index), Some(current_head)) => {
|
|
let index_changed =
|
|
index_text.as_deref() != current_index.as_deref();
|
|
let head_changed =
|
|
head_text.as_deref() != current_head.as_deref();
|
|
if index_changed && head_changed {
|
|
if index_text == head_text {
|
|
Some(DiffBasesChange::SetBoth(head_text))
|
|
} else {
|
|
Some(DiffBasesChange::SetEach {
|
|
index: index_text,
|
|
head: head_text,
|
|
})
|
|
}
|
|
} else if index_changed {
|
|
Some(DiffBasesChange::SetIndex(index_text))
|
|
} else if head_changed {
|
|
Some(DiffBasesChange::SetHead(head_text))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
(Some(current_index), None) => {
|
|
let index_changed =
|
|
index_text.as_deref() != current_index.as_deref();
|
|
index_changed
|
|
.then_some(DiffBasesChange::SetIndex(index_text))
|
|
}
|
|
(None, Some(current_head)) => {
|
|
let head_changed =
|
|
head_text.as_deref() != current_head.as_deref();
|
|
head_changed.then_some(DiffBasesChange::SetHead(head_text))
|
|
}
|
|
(None, None) => None,
|
|
};
|
|
|
|
changes.push((buffer.clone(), change))
|
|
}
|
|
changes
|
|
})
|
|
.await;
|
|
|
|
git_store.update(&mut cx, |git_store, cx| {
|
|
for (buffer, diff_bases_change) in buffer_diff_base_changes {
|
|
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
|
let buffer_id = buffer_snapshot.remote_id();
|
|
let Some(diff_state) = git_store.diffs.get(&buffer_id) else {
|
|
continue;
|
|
};
|
|
|
|
let downstream_client = git_store.downstream_client();
|
|
diff_state.update(cx, |diff_state, cx| {
|
|
use proto::update_diff_bases::Mode;
|
|
|
|
if let Some((diff_bases_change, (client, project_id))) =
|
|
diff_bases_change.clone().zip(downstream_client)
|
|
{
|
|
let (staged_text, committed_text, mode) = match diff_bases_change {
|
|
DiffBasesChange::SetIndex(index) => {
|
|
(index, None, Mode::IndexOnly)
|
|
}
|
|
DiffBasesChange::SetHead(head) => (None, head, Mode::HeadOnly),
|
|
DiffBasesChange::SetEach { index, head } => {
|
|
(index, head, Mode::IndexAndHead)
|
|
}
|
|
DiffBasesChange::SetBoth(text) => {
|
|
(None, text, Mode::IndexMatchesHead)
|
|
}
|
|
};
|
|
client
|
|
.send(proto::UpdateDiffBases {
|
|
project_id: project_id.to_proto(),
|
|
buffer_id: buffer_id.to_proto(),
|
|
staged_text,
|
|
committed_text,
|
|
mode: mode as i32,
|
|
})
|
|
.log_err();
|
|
}
|
|
|
|
diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx);
|
|
});
|
|
}
|
|
})
|
|
},
|
|
);
|
|
}
|
|
|
|
pub fn send_job<F, Fut, R>(
|
|
&mut self,
|
|
status: Option<SharedString>,
|
|
job: F,
|
|
) -> oneshot::Receiver<R>
|
|
where
|
|
F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static,
|
|
Fut: Future<Output = R> + 'static,
|
|
R: Send + 'static,
|
|
{
|
|
self.send_keyed_job(None, status, job)
|
|
}
|
|
|
|
fn send_keyed_job<F, Fut, R>(
|
|
&mut self,
|
|
key: Option<GitJobKey>,
|
|
status: Option<SharedString>,
|
|
job: F,
|
|
) -> oneshot::Receiver<R>
|
|
where
|
|
F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static,
|
|
Fut: Future<Output = R> + 'static,
|
|
R: Send + 'static,
|
|
{
|
|
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
|
let job_id = post_inc(&mut self.job_id);
|
|
let this = self.this.clone();
|
|
self.job_sender
|
|
.unbounded_send(GitJob {
|
|
key,
|
|
job: Box::new(move |state, cx: &mut AsyncApp| {
|
|
let job = job(state, cx.clone());
|
|
cx.spawn(async move |cx| {
|
|
if let Some(s) = status.clone() {
|
|
this.update(cx, |this, cx| {
|
|
this.active_jobs.insert(
|
|
job_id,
|
|
JobInfo {
|
|
start: Instant::now(),
|
|
message: s.clone(),
|
|
},
|
|
);
|
|
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
}
|
|
let result = job.await;
|
|
|
|
this.update(cx, |this, cx| {
|
|
this.active_jobs.remove(&job_id);
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
|
|
result_tx.send(result).ok();
|
|
})
|
|
}),
|
|
})
|
|
.ok();
|
|
result_rx
|
|
}
|
|
|
|
pub fn set_as_active_repository(&self, cx: &mut Context<Self>) {
|
|
let Some(git_store) = self.git_store.upgrade() else {
|
|
return;
|
|
};
|
|
let entity = cx.entity();
|
|
git_store.update(cx, |git_store, cx| {
|
|
let Some((&id, _)) = git_store
|
|
.repositories
|
|
.iter()
|
|
.find(|(_, handle)| *handle == &entity)
|
|
else {
|
|
return;
|
|
};
|
|
git_store.active_repo_id = Some(id);
|
|
cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
|
|
});
|
|
}
|
|
|
|
pub fn cached_status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
|
|
self.snapshot.status()
|
|
}
|
|
|
|
pub fn diff_stat_for_path(&self, path: &RepoPath) -> Option<DiffStat> {
|
|
self.snapshot.diff_stat_for_path(path)
|
|
}
|
|
|
|
pub fn cached_stash(&self) -> GitStash {
|
|
self.snapshot.stash_entries.clone()
|
|
}
|
|
|
|
pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option<ProjectPath> {
|
|
let git_store = self.git_store.upgrade()?;
|
|
let worktree_store = git_store.read(cx).worktree_store.read(cx);
|
|
let abs_path = self.snapshot.repo_path_to_abs_path(path);
|
|
let abs_path = SanitizedPath::new(&abs_path);
|
|
let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?;
|
|
Some(ProjectPath {
|
|
worktree_id: worktree.read(cx).id(),
|
|
path: relative_path,
|
|
})
|
|
}
|
|
|
|
pub fn project_path_to_repo_path(&self, path: &ProjectPath, cx: &App) -> Option<RepoPath> {
|
|
let git_store = self.git_store.upgrade()?;
|
|
let worktree_store = git_store.read(cx).worktree_store.read(cx);
|
|
let abs_path = worktree_store.absolutize(path, cx)?;
|
|
self.snapshot.abs_path_to_repo_path(&abs_path)
|
|
}
|
|
|
|
pub fn contains_sub_repo(&self, other: &Entity<Self>, cx: &App) -> bool {
|
|
other
|
|
.read(cx)
|
|
.snapshot
|
|
.work_directory_abs_path
|
|
.starts_with(&self.snapshot.work_directory_abs_path)
|
|
}
|
|
|
|
pub fn open_commit_buffer(
|
|
&mut self,
|
|
languages: Option<Arc<LanguageRegistry>>,
|
|
buffer_store: Entity<BufferStore>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<Entity<Buffer>>> {
|
|
let id = self.id;
|
|
if let Some(buffer) = self.commit_message_buffer.clone() {
|
|
return Task::ready(Ok(buffer));
|
|
}
|
|
let this = cx.weak_entity();
|
|
|
|
let rx = self.send_job(None, move |state, mut cx| async move {
|
|
let Some(this) = this.upgrade() else {
|
|
bail!("git store was dropped");
|
|
};
|
|
match state {
|
|
RepositoryState::Local(..) => {
|
|
this.update(&mut cx, |_, cx| {
|
|
Self::open_local_commit_buffer(languages, buffer_store, cx)
|
|
})
|
|
.await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let request = client.request(proto::OpenCommitMessageBuffer {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
});
|
|
let response = request.await.context("requesting to open commit buffer")?;
|
|
let buffer_id = BufferId::new(response.buffer_id)?;
|
|
let buffer = buffer_store
|
|
.update(&mut cx, |buffer_store, cx| {
|
|
buffer_store.wait_for_remote_buffer(buffer_id, cx)
|
|
})
|
|
.await?;
|
|
if let Some(language_registry) = languages {
|
|
let git_commit_language =
|
|
language_registry.language_for_name("Git Commit").await?;
|
|
buffer.update(&mut cx, |buffer, cx| {
|
|
buffer.set_language(Some(git_commit_language), cx);
|
|
});
|
|
}
|
|
this.update(&mut cx, |this, _| {
|
|
this.commit_message_buffer = Some(buffer.clone());
|
|
});
|
|
Ok(buffer)
|
|
}
|
|
}
|
|
});
|
|
|
|
cx.spawn(|_, _: &mut AsyncApp| async move { rx.await? })
|
|
}
|
|
|
|
fn open_local_commit_buffer(
|
|
language_registry: Option<Arc<LanguageRegistry>>,
|
|
buffer_store: Entity<BufferStore>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<Entity<Buffer>>> {
|
|
cx.spawn(async move |repository, cx| {
|
|
let git_commit_language = match language_registry {
|
|
Some(language_registry) => {
|
|
Some(language_registry.language_for_name("Git Commit").await?)
|
|
}
|
|
None => None,
|
|
};
|
|
let buffer = buffer_store
|
|
.update(cx, |buffer_store, cx| {
|
|
buffer_store.create_buffer(git_commit_language, false, cx)
|
|
})
|
|
.await?;
|
|
|
|
repository.update(cx, |repository, _| {
|
|
repository.commit_message_buffer = Some(buffer.clone());
|
|
})?;
|
|
Ok(buffer)
|
|
})
|
|
}
|
|
|
|
pub fn checkout_files(
|
|
&mut self,
|
|
commit: &str,
|
|
paths: Vec<RepoPath>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<()>> {
|
|
let commit = commit.to_string();
|
|
let id = self.id;
|
|
|
|
self.spawn_job_with_tracking(
|
|
paths.clone(),
|
|
pending_op::GitStatus::Reverted,
|
|
cx,
|
|
async move |this, cx| {
|
|
this.update(cx, |this, _cx| {
|
|
this.send_job(
|
|
Some(format!("git checkout {}", commit).into()),
|
|
move |git_repo, _| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => {
|
|
backend
|
|
.checkout_files(commit, paths, environment.clone())
|
|
.await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState {
|
|
project_id,
|
|
client,
|
|
}) => {
|
|
client
|
|
.request(proto::GitCheckoutFiles {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
commit,
|
|
paths: paths
|
|
.into_iter()
|
|
.map(|p| p.to_proto())
|
|
.collect(),
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)
|
|
})?
|
|
.await?
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn reset(
|
|
&mut self,
|
|
commit: String,
|
|
reset_mode: ResetMode,
|
|
_cx: &mut App,
|
|
) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
|
|
self.send_job(None, move |git_repo, _| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => backend.reset(commit, reset_mode, environment).await,
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitReset {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
commit,
|
|
mode: match reset_mode {
|
|
ResetMode::Soft => git_reset::ResetMode::Soft.into(),
|
|
ResetMode::Mixed => git_reset::ResetMode::Mixed.into(),
|
|
},
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn show(&mut self, commit: String) -> oneshot::Receiver<Result<CommitDetails>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |git_repo, _cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.show(commit).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let resp = client
|
|
.request(proto::GitShow {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
commit,
|
|
})
|
|
.await?;
|
|
|
|
Ok(CommitDetails {
|
|
sha: resp.sha.into(),
|
|
message: resp.message.into(),
|
|
commit_timestamp: resp.commit_timestamp,
|
|
author_email: resp.author_email.into(),
|
|
author_name: resp.author_name.into(),
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn load_commit_diff(&mut self, commit: String) -> oneshot::Receiver<Result<CommitDiff>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |git_repo, cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.load_commit(commit, cx).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState {
|
|
client, project_id, ..
|
|
}) => {
|
|
let response = client
|
|
.request(proto::LoadCommitDiff {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
commit,
|
|
})
|
|
.await?;
|
|
Ok(CommitDiff {
|
|
files: response
|
|
.files
|
|
.into_iter()
|
|
.map(|file| {
|
|
Ok(CommitFile {
|
|
path: RepoPath::from_proto(&file.path)?,
|
|
old_text: file.old_text,
|
|
new_text: file.new_text,
|
|
is_binary: file.is_binary,
|
|
})
|
|
})
|
|
.collect::<Result<Vec<_>>>()?,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn file_history(
|
|
&mut self,
|
|
path: RepoPath,
|
|
) -> oneshot::Receiver<Result<git::repository::FileHistory>> {
|
|
self.file_history_paginated(path, 0, None)
|
|
}
|
|
|
|
pub fn file_history_paginated(
|
|
&mut self,
|
|
path: RepoPath,
|
|
skip: usize,
|
|
limit: Option<usize>,
|
|
) -> oneshot::Receiver<Result<git::repository::FileHistory>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |git_repo, _cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.file_history_paginated(path, skip, limit).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
|
|
let response = client
|
|
.request(proto::GitFileHistory {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
path: path.to_proto(),
|
|
skip: skip as u64,
|
|
limit: limit.map(|l| l as u64),
|
|
})
|
|
.await?;
|
|
Ok(git::repository::FileHistory {
|
|
entries: response
|
|
.entries
|
|
.into_iter()
|
|
.map(|entry| git::repository::FileHistoryEntry {
|
|
sha: entry.sha.into(),
|
|
subject: entry.subject.into(),
|
|
message: entry.message.into(),
|
|
commit_timestamp: entry.commit_timestamp,
|
|
author_name: entry.author_name.into(),
|
|
author_email: entry.author_email.into(),
|
|
})
|
|
.collect(),
|
|
path: RepoPath::from_proto(&response.path)?,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn get_graph_data(
|
|
&self,
|
|
log_source: LogSource,
|
|
log_order: LogOrder,
|
|
) -> Option<&InitialGitGraphData> {
|
|
self.initial_graph_data.get(&(log_source, log_order))
|
|
}
|
|
|
|
pub fn search_commits(
|
|
&mut self,
|
|
log_source: LogSource,
|
|
search_args: SearchCommitArgs,
|
|
request_tx: smol::channel::Sender<Oid>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let repository_state = self.repository_state.clone();
|
|
|
|
cx.background_spawn(async move {
|
|
let repo_state = repository_state.await;
|
|
|
|
match repo_state {
|
|
Ok(RepositoryState::Local(LocalRepositoryState { backend, .. })) => {
|
|
backend
|
|
.search_commits(log_source, search_args, request_tx)
|
|
.await
|
|
.log_err();
|
|
}
|
|
Ok(RepositoryState::Remote(_)) => {}
|
|
Err(_) => {}
|
|
};
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub fn graph_data(
|
|
&mut self,
|
|
log_source: LogSource,
|
|
log_order: LogOrder,
|
|
range: Range<usize>,
|
|
cx: &mut Context<Self>,
|
|
) -> GraphDataResponse<'_> {
|
|
let initial_commit_data = self
|
|
.initial_graph_data
|
|
.entry((log_source.clone(), log_order))
|
|
.or_insert_with(|| {
|
|
let state = self.repository_state.clone();
|
|
let log_source = log_source.clone();
|
|
|
|
let fetch_task = cx.spawn(async move |repository, cx| {
|
|
let state = state.await;
|
|
let result = match state {
|
|
Ok(RepositoryState::Local(LocalRepositoryState { backend, .. })) => {
|
|
Self::local_git_graph_data(
|
|
repository.clone(),
|
|
backend,
|
|
log_source.clone(),
|
|
log_order,
|
|
cx,
|
|
)
|
|
.await
|
|
}
|
|
Ok(RepositoryState::Remote(_)) => {
|
|
Err("Git graph is not supported for collab yet".into())
|
|
}
|
|
Err(e) => Err(SharedString::from(e)),
|
|
};
|
|
|
|
if let Err(fetch_task_error) = result {
|
|
repository
|
|
.update(cx, |repository, _| {
|
|
if let Some(data) = repository
|
|
.initial_graph_data
|
|
.get_mut(&(log_source, log_order))
|
|
{
|
|
data.error = Some(fetch_task_error);
|
|
} else {
|
|
debug_panic!(
|
|
"This task would be dropped if this entry doesn't exist"
|
|
);
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
});
|
|
|
|
InitialGitGraphData {
|
|
fetch_task,
|
|
error: None,
|
|
commit_data: Vec::new(),
|
|
commit_oid_to_index: HashMap::default(),
|
|
}
|
|
});
|
|
|
|
let max_start = initial_commit_data.commit_data.len().saturating_sub(1);
|
|
let max_end = initial_commit_data.commit_data.len();
|
|
|
|
GraphDataResponse {
|
|
commits: &initial_commit_data.commit_data
|
|
[range.start.min(max_start)..range.end.min(max_end)],
|
|
is_loading: !initial_commit_data.fetch_task.is_ready(),
|
|
error: initial_commit_data.error.clone(),
|
|
}
|
|
}
|
|
|
|
async fn local_git_graph_data(
|
|
this: WeakEntity<Self>,
|
|
backend: Arc<dyn GitRepository>,
|
|
log_source: LogSource,
|
|
log_order: LogOrder,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<(), SharedString> {
|
|
let (request_tx, request_rx) =
|
|
smol::channel::unbounded::<Vec<Arc<InitialGraphCommitData>>>();
|
|
|
|
let task = cx.background_executor().spawn({
|
|
let log_source = log_source.clone();
|
|
async move {
|
|
backend
|
|
.initial_graph_data(log_source, log_order, request_tx)
|
|
.await
|
|
.map_err(|err| SharedString::from(err.to_string()))
|
|
}
|
|
});
|
|
|
|
let graph_data_key = (log_source, log_order);
|
|
|
|
while let Ok(initial_graph_commit_data) = request_rx.recv().await {
|
|
this.update(cx, |repository, cx| {
|
|
let graph_data = repository
|
|
.initial_graph_data
|
|
.entry(graph_data_key.clone())
|
|
.and_modify(|graph_data| {
|
|
for commit_data in initial_graph_commit_data {
|
|
graph_data
|
|
.commit_oid_to_index
|
|
.insert(commit_data.sha, graph_data.commit_data.len());
|
|
graph_data.commit_data.push(commit_data);
|
|
}
|
|
cx.emit(RepositoryEvent::GraphEvent(
|
|
graph_data_key.clone(),
|
|
GitGraphEvent::CountUpdated(graph_data.commit_data.len()),
|
|
));
|
|
});
|
|
|
|
match &graph_data {
|
|
Entry::Occupied(_) => {}
|
|
Entry::Vacant(_) => {
|
|
debug_panic!("This task should be dropped if data doesn't exist");
|
|
}
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
|
|
task.await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn fetch_commit_data(&mut self, sha: Oid, cx: &mut Context<Self>) -> &CommitDataState {
|
|
if !self.commit_data.contains_key(&sha) {
|
|
match &self.graph_commit_data_handler {
|
|
GraphCommitHandlerState::Open(handler) => {
|
|
if handler.commit_data_request.try_send(sha).is_ok() {
|
|
let old_value = self.commit_data.insert(sha, CommitDataState::Loading);
|
|
debug_assert!(old_value.is_none(), "We should never overwrite commit data");
|
|
}
|
|
}
|
|
GraphCommitHandlerState::Closed => {
|
|
self.open_graph_commit_data_handler(cx);
|
|
}
|
|
GraphCommitHandlerState::Starting => {}
|
|
}
|
|
}
|
|
|
|
self.commit_data
|
|
.get(&sha)
|
|
.unwrap_or(&CommitDataState::Loading)
|
|
}
|
|
|
|
fn open_graph_commit_data_handler(&mut self, cx: &mut Context<Self>) {
|
|
self.graph_commit_data_handler = GraphCommitHandlerState::Starting;
|
|
|
|
let state = self.repository_state.clone();
|
|
let (result_tx, result_rx) = smol::channel::bounded::<(Oid, GraphCommitData)>(64);
|
|
let (request_tx, request_rx) = smol::channel::unbounded::<Oid>();
|
|
|
|
let foreground_task = cx.spawn(async move |this, cx| {
|
|
while let Ok((sha, commit_data)) = result_rx.recv().await {
|
|
let result = this.update(cx, |this, cx| {
|
|
let old_value = this
|
|
.commit_data
|
|
.insert(sha, CommitDataState::Loaded(Arc::new(commit_data)));
|
|
debug_assert!(
|
|
!matches!(old_value, Some(CommitDataState::Loaded(_))),
|
|
"We should never overwrite commit data"
|
|
);
|
|
|
|
cx.notify();
|
|
});
|
|
if result.is_err() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.update(cx, |this, _cx| {
|
|
this.graph_commit_data_handler = GraphCommitHandlerState::Closed;
|
|
})
|
|
.ok();
|
|
});
|
|
|
|
let request_tx_for_handler = request_tx;
|
|
let background_executor = cx.background_executor().clone();
|
|
|
|
cx.background_spawn(async move {
|
|
let backend = match state.await {
|
|
Ok(RepositoryState::Local(LocalRepositoryState { backend, .. })) => backend,
|
|
Ok(RepositoryState::Remote(_)) => {
|
|
log::error!("commit_data_reader not supported for remote repositories");
|
|
return;
|
|
}
|
|
Err(error) => {
|
|
log::error!("failed to get repository state: {error}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let reader = match backend.commit_data_reader() {
|
|
Ok(reader) => reader,
|
|
Err(error) => {
|
|
log::error!("failed to create commit data reader: {error:?}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
loop {
|
|
let timeout = background_executor.timer(std::time::Duration::from_secs(10));
|
|
|
|
futures::select_biased! {
|
|
sha = futures::FutureExt::fuse(request_rx.recv()) => {
|
|
let Ok(sha) = sha else {
|
|
break;
|
|
};
|
|
|
|
match reader.read(sha).await {
|
|
Ok(commit_data) => {
|
|
if result_tx.send((sha, commit_data)).await.is_err() {
|
|
break;
|
|
}
|
|
}
|
|
Err(error) => {
|
|
log::error!("failed to read commit data for {sha}: {error:?}");
|
|
}
|
|
}
|
|
}
|
|
_ = futures::FutureExt::fuse(timeout) => {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(result_tx);
|
|
})
|
|
.detach();
|
|
|
|
self.graph_commit_data_handler = GraphCommitHandlerState::Open(GraphCommitDataHandler {
|
|
_task: foreground_task,
|
|
commit_data_request: request_tx_for_handler,
|
|
});
|
|
}
|
|
|
|
fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
|
|
Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
|
|
}
|
|
|
|
fn save_buffers<'a>(
|
|
&self,
|
|
entries: impl IntoIterator<Item = &'a RepoPath>,
|
|
cx: &mut Context<Self>,
|
|
) -> Vec<Task<anyhow::Result<()>>> {
|
|
let mut save_futures = Vec::new();
|
|
if let Some(buffer_store) = self.buffer_store(cx) {
|
|
buffer_store.update(cx, |buffer_store, cx| {
|
|
for path in entries {
|
|
let Some(project_path) = self.repo_path_to_project_path(path, cx) else {
|
|
continue;
|
|
};
|
|
if let Some(buffer) = buffer_store.get_by_path(&project_path)
|
|
&& buffer
|
|
.read(cx)
|
|
.file()
|
|
.is_some_and(|file| file.disk_state().exists())
|
|
&& buffer.read(cx).has_unsaved_edits()
|
|
{
|
|
save_futures.push(buffer_store.save_buffer(buffer, cx));
|
|
}
|
|
}
|
|
})
|
|
}
|
|
save_futures
|
|
}
|
|
|
|
pub fn stage_entries(
|
|
&mut self,
|
|
entries: Vec<RepoPath>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<anyhow::Result<()>> {
|
|
self.stage_or_unstage_entries(true, entries, cx)
|
|
}
|
|
|
|
pub fn unstage_entries(
|
|
&mut self,
|
|
entries: Vec<RepoPath>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<anyhow::Result<()>> {
|
|
self.stage_or_unstage_entries(false, entries, cx)
|
|
}
|
|
|
|
fn stage_or_unstage_entries(
|
|
&mut self,
|
|
stage: bool,
|
|
entries: Vec<RepoPath>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<anyhow::Result<()>> {
|
|
if entries.is_empty() {
|
|
return Task::ready(Ok(()));
|
|
}
|
|
let Some(git_store) = self.git_store.upgrade() else {
|
|
return Task::ready(Ok(()));
|
|
};
|
|
let id = self.id;
|
|
let save_tasks = self.save_buffers(&entries, cx);
|
|
let paths = entries
|
|
.iter()
|
|
.map(|p| p.as_unix_str())
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
let status = if stage {
|
|
format!("git add {paths}")
|
|
} else {
|
|
format!("git reset {paths}")
|
|
};
|
|
let job_key = GitJobKey::WriteIndex(entries.clone());
|
|
|
|
self.spawn_job_with_tracking(
|
|
entries.clone(),
|
|
if stage {
|
|
pending_op::GitStatus::Staged
|
|
} else {
|
|
pending_op::GitStatus::Unstaged
|
|
},
|
|
cx,
|
|
async move |this, cx| {
|
|
for save_task in save_tasks {
|
|
save_task.await?;
|
|
}
|
|
|
|
this.update(cx, |this, cx| {
|
|
let weak_this = cx.weak_entity();
|
|
this.send_keyed_job(
|
|
Some(job_key),
|
|
Some(status.into()),
|
|
move |git_repo, mut cx| async move {
|
|
let hunk_staging_operation_counts = weak_this
|
|
.update(&mut cx, |this, cx| {
|
|
let mut hunk_staging_operation_counts = HashMap::default();
|
|
for path in &entries {
|
|
let Some(project_path) =
|
|
this.repo_path_to_project_path(path, cx)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(buffer) = git_store
|
|
.read(cx)
|
|
.buffer_store
|
|
.read(cx)
|
|
.get_by_path(&project_path)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(diff_state) = git_store
|
|
.read(cx)
|
|
.diffs
|
|
.get(&buffer.read(cx).remote_id())
|
|
.cloned()
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(uncommitted_diff) =
|
|
diff_state.read(cx).uncommitted_diff.as_ref().and_then(
|
|
|uncommitted_diff| uncommitted_diff.upgrade(),
|
|
)
|
|
else {
|
|
continue;
|
|
};
|
|
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
|
let file_exists = buffer
|
|
.read(cx)
|
|
.file()
|
|
.is_some_and(|file| file.disk_state().exists());
|
|
let hunk_staging_operation_count =
|
|
diff_state.update(cx, |diff_state, cx| {
|
|
uncommitted_diff.update(
|
|
cx,
|
|
|uncommitted_diff, cx| {
|
|
uncommitted_diff
|
|
.stage_or_unstage_all_hunks(
|
|
stage,
|
|
&buffer_snapshot,
|
|
file_exists,
|
|
cx,
|
|
);
|
|
},
|
|
);
|
|
|
|
diff_state.hunk_staging_operation_count += 1;
|
|
diff_state.hunk_staging_operation_count
|
|
});
|
|
hunk_staging_operation_counts.insert(
|
|
diff_state.downgrade(),
|
|
hunk_staging_operation_count,
|
|
);
|
|
}
|
|
hunk_staging_operation_counts
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let result = match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => {
|
|
if stage {
|
|
backend.stage_paths(entries, environment.clone()).await
|
|
} else {
|
|
backend.unstage_paths(entries, environment.clone()).await
|
|
}
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState {
|
|
project_id,
|
|
client,
|
|
}) => {
|
|
if stage {
|
|
client
|
|
.request(proto::Stage {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
paths: entries
|
|
.into_iter()
|
|
.map(|repo_path| repo_path.to_proto())
|
|
.collect(),
|
|
})
|
|
.await
|
|
.context("sending stage request")
|
|
.map(|_| ())
|
|
} else {
|
|
client
|
|
.request(proto::Unstage {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
paths: entries
|
|
.into_iter()
|
|
.map(|repo_path| repo_path.to_proto())
|
|
.collect(),
|
|
})
|
|
.await
|
|
.context("sending unstage request")
|
|
.map(|_| ())
|
|
}
|
|
}
|
|
};
|
|
|
|
for (diff_state, hunk_staging_operation_count) in
|
|
hunk_staging_operation_counts
|
|
{
|
|
diff_state
|
|
.update(&mut cx, |diff_state, cx| {
|
|
if result.is_ok() {
|
|
diff_state.hunk_staging_operation_count_as_of_write =
|
|
hunk_staging_operation_count;
|
|
} else if let Some(uncommitted_diff) =
|
|
&diff_state.uncommitted_diff
|
|
{
|
|
uncommitted_diff
|
|
.update(cx, |uncommitted_diff, cx| {
|
|
uncommitted_diff.clear_pending_hunks(cx);
|
|
})
|
|
.ok();
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
|
|
result
|
|
},
|
|
)
|
|
})?
|
|
.await?
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn stage_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
|
|
let snapshot = self.snapshot.clone();
|
|
let pending_ops = self.pending_ops.clone();
|
|
let to_stage = cx.background_spawn(async move {
|
|
snapshot
|
|
.status()
|
|
.filter_map(|entry| {
|
|
if let Some(ops) =
|
|
pending_ops.get(&PathKey(entry.repo_path.as_ref().clone()), ())
|
|
{
|
|
if ops.staging() || ops.staged() {
|
|
None
|
|
} else {
|
|
Some(entry.repo_path)
|
|
}
|
|
} else if entry.status.staging().is_fully_staged() {
|
|
None
|
|
} else {
|
|
Some(entry.repo_path)
|
|
}
|
|
})
|
|
.collect()
|
|
});
|
|
|
|
cx.spawn(async move |this, cx| {
|
|
let to_stage = to_stage.await;
|
|
this.update(cx, |this, cx| {
|
|
this.stage_or_unstage_entries(true, to_stage, cx)
|
|
})?
|
|
.await
|
|
})
|
|
}
|
|
|
|
pub fn unstage_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
|
|
let snapshot = self.snapshot.clone();
|
|
let pending_ops = self.pending_ops.clone();
|
|
let to_unstage = cx.background_spawn(async move {
|
|
snapshot
|
|
.status()
|
|
.filter_map(|entry| {
|
|
if let Some(ops) =
|
|
pending_ops.get(&PathKey(entry.repo_path.as_ref().clone()), ())
|
|
{
|
|
if !ops.staging() && !ops.staged() {
|
|
None
|
|
} else {
|
|
Some(entry.repo_path)
|
|
}
|
|
} else if entry.status.staging().is_fully_unstaged() {
|
|
None
|
|
} else {
|
|
Some(entry.repo_path)
|
|
}
|
|
})
|
|
.collect()
|
|
});
|
|
|
|
cx.spawn(async move |this, cx| {
|
|
let to_unstage = to_unstage.await;
|
|
this.update(cx, |this, cx| {
|
|
this.stage_or_unstage_entries(false, to_unstage, cx)
|
|
})?
|
|
.await
|
|
})
|
|
}
|
|
|
|
pub fn stash_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
|
|
let to_stash = self.cached_status().map(|entry| entry.repo_path).collect();
|
|
|
|
self.stash_entries(to_stash, cx)
|
|
}
|
|
|
|
pub fn stash_entries(
|
|
&mut self,
|
|
entries: Vec<RepoPath>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<anyhow::Result<()>> {
|
|
let id = self.id;
|
|
|
|
cx.spawn(async move |this, cx| {
|
|
this.update(cx, |this, _| {
|
|
this.send_job(None, move |git_repo, _cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => backend.stash_paths(entries, environment).await,
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::Stash {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
paths: entries
|
|
.into_iter()
|
|
.map(|repo_path| repo_path.to_proto())
|
|
.collect(),
|
|
})
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
}
|
|
})
|
|
})?
|
|
.await??;
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
pub fn stash_pop(
|
|
&mut self,
|
|
index: Option<usize>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<anyhow::Result<()>> {
|
|
let id = self.id;
|
|
cx.spawn(async move |this, cx| {
|
|
this.update(cx, |this, _| {
|
|
this.send_job(None, move |git_repo, _cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => backend.stash_pop(index, environment).await,
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::StashPop {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
stash_index: index.map(|i| i as u64),
|
|
})
|
|
.await
|
|
.context("sending stash pop request")?;
|
|
Ok(())
|
|
}
|
|
}
|
|
})
|
|
})?
|
|
.await??;
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
pub fn stash_apply(
|
|
&mut self,
|
|
index: Option<usize>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<anyhow::Result<()>> {
|
|
let id = self.id;
|
|
cx.spawn(async move |this, cx| {
|
|
this.update(cx, |this, _| {
|
|
this.send_job(None, move |git_repo, _cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => backend.stash_apply(index, environment).await,
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::StashApply {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
stash_index: index.map(|i| i as u64),
|
|
})
|
|
.await
|
|
.context("sending stash apply request")?;
|
|
Ok(())
|
|
}
|
|
}
|
|
})
|
|
})?
|
|
.await??;
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
pub fn stash_drop(
|
|
&mut self,
|
|
index: Option<usize>,
|
|
cx: &mut Context<Self>,
|
|
) -> oneshot::Receiver<anyhow::Result<()>> {
|
|
let id = self.id;
|
|
let updates_tx = self
|
|
.git_store()
|
|
.and_then(|git_store| match &git_store.read(cx).state {
|
|
GitStoreState::Local { downstream, .. } => downstream
|
|
.as_ref()
|
|
.map(|downstream| downstream.updates_tx.clone()),
|
|
_ => None,
|
|
});
|
|
let this = cx.weak_entity();
|
|
self.send_job(None, move |git_repo, mut cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => {
|
|
// TODO would be nice to not have to do this manually
|
|
let result = backend.stash_drop(index, environment).await;
|
|
if result.is_ok()
|
|
&& let Ok(stash_entries) = backend.stash_entries().await
|
|
{
|
|
let snapshot = this.update(&mut cx, |this, cx| {
|
|
this.snapshot.stash_entries = stash_entries;
|
|
cx.emit(RepositoryEvent::StashEntriesChanged);
|
|
this.snapshot.clone()
|
|
})?;
|
|
if let Some(updates_tx) = updates_tx {
|
|
updates_tx
|
|
.unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
|
|
.ok();
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::StashDrop {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
stash_index: index.map(|i| i as u64),
|
|
})
|
|
.await
|
|
.context("sending stash pop request")?;
|
|
Ok(())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn run_hook(&mut self, hook: RunHook, _cx: &mut App) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
self.send_job(
|
|
Some(format!("git hook {}", hook.as_str()).into()),
|
|
move |git_repo, _cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => backend.run_hook(hook, environment.clone()).await,
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::RunGitHook {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
hook: hook.to_proto(),
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn commit(
|
|
&mut self,
|
|
message: SharedString,
|
|
name_and_email: Option<(SharedString, SharedString)>,
|
|
options: CommitOptions,
|
|
askpass: AskPassDelegate,
|
|
cx: &mut App,
|
|
) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
let askpass_delegates = self.askpass_delegates.clone();
|
|
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
|
|
|
let rx = self.run_hook(RunHook::PreCommit, cx);
|
|
|
|
self.send_job(Some("git commit".into()), move |git_repo, _cx| async move {
|
|
rx.await??;
|
|
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => {
|
|
backend
|
|
.commit(message, name_and_email, options, askpass, environment)
|
|
.await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
askpass_delegates.lock().insert(askpass_id, askpass);
|
|
let _defer = util::defer(|| {
|
|
let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
|
|
debug_assert!(askpass_delegate.is_some());
|
|
});
|
|
let (name, email) = name_and_email.unzip();
|
|
client
|
|
.request(proto::Commit {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
message: String::from(message),
|
|
name: name.map(String::from),
|
|
email: email.map(String::from),
|
|
options: Some(proto::commit::CommitOptions {
|
|
amend: options.amend,
|
|
signoff: options.signoff,
|
|
}),
|
|
askpass_id,
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn fetch(
|
|
&mut self,
|
|
fetch_options: FetchOptions,
|
|
askpass: AskPassDelegate,
|
|
_cx: &mut App,
|
|
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
|
let askpass_delegates = self.askpass_delegates.clone();
|
|
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
|
let id = self.id;
|
|
|
|
self.send_job(Some("git fetch".into()), move |git_repo, cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => backend.fetch(fetch_options, askpass, environment, cx).await,
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
askpass_delegates.lock().insert(askpass_id, askpass);
|
|
let _defer = util::defer(|| {
|
|
let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
|
|
debug_assert!(askpass_delegate.is_some());
|
|
});
|
|
|
|
let response = client
|
|
.request(proto::Fetch {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
askpass_id,
|
|
remote: fetch_options.to_proto(),
|
|
})
|
|
.await?;
|
|
|
|
Ok(RemoteCommandOutput {
|
|
stdout: response.stdout,
|
|
stderr: response.stderr,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn push(
|
|
&mut self,
|
|
branch: SharedString,
|
|
remote_branch: SharedString,
|
|
remote: SharedString,
|
|
options: Option<PushOptions>,
|
|
askpass: AskPassDelegate,
|
|
cx: &mut Context<Self>,
|
|
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
|
let askpass_delegates = self.askpass_delegates.clone();
|
|
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
|
let id = self.id;
|
|
|
|
let args = options
|
|
.map(|option| match option {
|
|
PushOptions::SetUpstream => " --set-upstream",
|
|
PushOptions::Force => " --force-with-lease",
|
|
})
|
|
.unwrap_or("");
|
|
|
|
let updates_tx = self
|
|
.git_store()
|
|
.and_then(|git_store| match &git_store.read(cx).state {
|
|
GitStoreState::Local { downstream, .. } => downstream
|
|
.as_ref()
|
|
.map(|downstream| downstream.updates_tx.clone()),
|
|
_ => None,
|
|
});
|
|
|
|
let this = cx.weak_entity();
|
|
self.send_job(
|
|
Some(format!("git push {} {} {}:{}", args, remote, branch, remote_branch).into()),
|
|
move |git_repo, mut cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => {
|
|
let result = backend
|
|
.push(
|
|
branch.to_string(),
|
|
remote_branch.to_string(),
|
|
remote.to_string(),
|
|
options,
|
|
askpass,
|
|
environment.clone(),
|
|
cx.clone(),
|
|
)
|
|
.await;
|
|
// TODO would be nice to not have to do this manually
|
|
if result.is_ok() {
|
|
let branches = backend.branches().await?;
|
|
let branch = branches.into_iter().find(|branch| branch.is_head);
|
|
log::info!("head branch after scan is {branch:?}");
|
|
let snapshot = this.update(&mut cx, |this, cx| {
|
|
this.snapshot.branch = branch;
|
|
cx.emit(RepositoryEvent::BranchChanged);
|
|
this.snapshot.clone()
|
|
})?;
|
|
if let Some(updates_tx) = updates_tx {
|
|
updates_tx
|
|
.unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
|
|
.ok();
|
|
}
|
|
}
|
|
result
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
askpass_delegates.lock().insert(askpass_id, askpass);
|
|
let _defer = util::defer(|| {
|
|
let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
|
|
debug_assert!(askpass_delegate.is_some());
|
|
});
|
|
let response = client
|
|
.request(proto::Push {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
askpass_id,
|
|
branch_name: branch.to_string(),
|
|
remote_branch_name: remote_branch.to_string(),
|
|
remote_name: remote.to_string(),
|
|
options: options.map(|options| match options {
|
|
PushOptions::Force => proto::push::PushOptions::Force,
|
|
PushOptions::SetUpstream => {
|
|
proto::push::PushOptions::SetUpstream
|
|
}
|
|
}
|
|
as i32),
|
|
})
|
|
.await?;
|
|
|
|
Ok(RemoteCommandOutput {
|
|
stdout: response.stdout,
|
|
stderr: response.stderr,
|
|
})
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn pull(
|
|
&mut self,
|
|
branch: Option<SharedString>,
|
|
remote: SharedString,
|
|
rebase: bool,
|
|
askpass: AskPassDelegate,
|
|
_cx: &mut App,
|
|
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
|
let askpass_delegates = self.askpass_delegates.clone();
|
|
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
|
let id = self.id;
|
|
|
|
let mut status = "git pull".to_string();
|
|
if rebase {
|
|
status.push_str(" --rebase");
|
|
}
|
|
status.push_str(&format!(" {}", remote));
|
|
if let Some(b) = &branch {
|
|
status.push_str(&format!(" {}", b));
|
|
}
|
|
|
|
self.send_job(Some(status.into()), move |git_repo, cx| async move {
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => {
|
|
backend
|
|
.pull(
|
|
branch.as_ref().map(|b| b.to_string()),
|
|
remote.to_string(),
|
|
rebase,
|
|
askpass,
|
|
environment.clone(),
|
|
cx,
|
|
)
|
|
.await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
askpass_delegates.lock().insert(askpass_id, askpass);
|
|
let _defer = util::defer(|| {
|
|
let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
|
|
debug_assert!(askpass_delegate.is_some());
|
|
});
|
|
let response = client
|
|
.request(proto::Pull {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
askpass_id,
|
|
rebase,
|
|
branch_name: branch.as_ref().map(|b| b.to_string()),
|
|
remote_name: remote.to_string(),
|
|
})
|
|
.await?;
|
|
|
|
Ok(RemoteCommandOutput {
|
|
stdout: response.stdout,
|
|
stderr: response.stderr,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn spawn_set_index_text_job(
|
|
&mut self,
|
|
path: RepoPath,
|
|
content: Option<String>,
|
|
hunk_staging_operation_count: Option<usize>,
|
|
cx: &mut Context<Self>,
|
|
) -> oneshot::Receiver<anyhow::Result<()>> {
|
|
let id = self.id;
|
|
let this = cx.weak_entity();
|
|
let git_store = self.git_store.clone();
|
|
let abs_path = self.snapshot.repo_path_to_abs_path(&path);
|
|
self.send_keyed_job(
|
|
Some(GitJobKey::WriteIndex(vec![path.clone()])),
|
|
None,
|
|
move |git_repo, mut cx| async move {
|
|
log::debug!(
|
|
"start updating index text for buffer {}",
|
|
path.as_unix_str()
|
|
);
|
|
|
|
match git_repo {
|
|
RepositoryState::Local(LocalRepositoryState {
|
|
fs,
|
|
backend,
|
|
environment,
|
|
..
|
|
}) => {
|
|
let executable = match fs.metadata(&abs_path).await {
|
|
Ok(Some(meta)) => meta.is_executable,
|
|
Ok(None) => false,
|
|
Err(_err) => false,
|
|
};
|
|
backend
|
|
.set_index_text(path.clone(), content, environment.clone(), executable)
|
|
.await?;
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::SetIndexText {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
path: path.to_proto(),
|
|
text: content,
|
|
})
|
|
.await?;
|
|
}
|
|
}
|
|
log::debug!(
|
|
"finish updating index text for buffer {}",
|
|
path.as_unix_str()
|
|
);
|
|
|
|
if let Some(hunk_staging_operation_count) = hunk_staging_operation_count {
|
|
let project_path = this
|
|
.read_with(&cx, |this, cx| this.repo_path_to_project_path(&path, cx))
|
|
.ok()
|
|
.flatten();
|
|
git_store
|
|
.update(&mut cx, |git_store, cx| {
|
|
let buffer_id = git_store
|
|
.buffer_store
|
|
.read(cx)
|
|
.get_by_path(&project_path?)?
|
|
.read(cx)
|
|
.remote_id();
|
|
let diff_state = git_store.diffs.get(&buffer_id)?;
|
|
diff_state.update(cx, |diff_state, _| {
|
|
diff_state.hunk_staging_operation_count_as_of_write =
|
|
hunk_staging_operation_count;
|
|
});
|
|
Some(())
|
|
})
|
|
.context("Git store dropped")?;
|
|
}
|
|
Ok(())
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn create_remote(
|
|
&mut self,
|
|
remote_name: String,
|
|
remote_url: String,
|
|
) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
self.send_job(
|
|
Some(format!("git remote add {remote_name} {remote_url}").into()),
|
|
move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.create_remote(remote_name, remote_url).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitCreateRemote {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
remote_name,
|
|
remote_url,
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn remove_remote(&mut self, remote_name: String) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
self.send_job(
|
|
Some(format!("git remove remote {remote_name}").into()),
|
|
move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.remove_remote(remote_name).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitRemoveRemote {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
remote_name,
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn get_remotes(
|
|
&mut self,
|
|
branch_name: Option<String>,
|
|
is_push: bool,
|
|
) -> oneshot::Receiver<Result<Vec<Remote>>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
let remote = if let Some(branch_name) = branch_name {
|
|
if is_push {
|
|
backend.get_push_remote(branch_name).await?
|
|
} else {
|
|
backend.get_branch_remote(branch_name).await?
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
match remote {
|
|
Some(remote) => Ok(vec![remote]),
|
|
None => backend.get_all_remotes().await,
|
|
}
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::GetRemotes {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
branch_name,
|
|
is_push,
|
|
})
|
|
.await?;
|
|
|
|
let remotes = response
|
|
.remotes
|
|
.into_iter()
|
|
.map(|remotes| Remote {
|
|
name: remotes.name.into(),
|
|
})
|
|
.collect();
|
|
|
|
Ok(remotes)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn branches(&mut self) -> oneshot::Receiver<Result<Vec<Branch>>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |repo, _| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.branches().await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::GitGetBranches {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
})
|
|
.await?;
|
|
|
|
let branches = response
|
|
.branches
|
|
.into_iter()
|
|
.map(|branch| proto_to_branch(&branch))
|
|
.collect();
|
|
|
|
Ok(branches)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// If this is a linked worktree (*NOT* the main checkout of a repository),
|
|
/// returns the pathed for the linked worktree.
|
|
///
|
|
/// Returns None if this is the main checkout.
|
|
pub fn linked_worktree_path(&self) -> Option<&Arc<Path>> {
|
|
if self.work_directory_abs_path != self.original_repo_abs_path {
|
|
Some(&self.work_directory_abs_path)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn path_for_new_linked_worktree(
|
|
&self,
|
|
branch_name: &str,
|
|
worktree_directory_setting: &str,
|
|
) -> Result<PathBuf> {
|
|
let original_repo = self.original_repo_abs_path.clone();
|
|
let project_name = original_repo
|
|
.file_name()
|
|
.ok_or_else(|| anyhow!("git repo must have a directory name"))?;
|
|
let directory = worktrees_directory_for_repo(&original_repo, worktree_directory_setting)?;
|
|
Ok(directory.join(branch_name).join(project_name))
|
|
}
|
|
|
|
pub fn worktrees(&mut self) -> oneshot::Receiver<Result<Vec<GitWorktree>>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |repo, _| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.worktrees().await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::GitGetWorktrees {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
})
|
|
.await?;
|
|
|
|
let worktrees = response
|
|
.worktrees
|
|
.into_iter()
|
|
.map(|worktree| proto_to_worktree(&worktree))
|
|
.collect();
|
|
|
|
Ok(worktrees)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn create_worktree(
|
|
&mut self,
|
|
branch_name: String,
|
|
path: PathBuf,
|
|
commit: Option<String>,
|
|
) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
self.send_job(
|
|
Some(format!("git worktree add: {}", branch_name).into()),
|
|
move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.create_worktree(branch_name, path, commit).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitCreateWorktree {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
name: branch_name,
|
|
directory: path.to_string_lossy().to_string(),
|
|
commit,
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn remove_worktree(&mut self, path: PathBuf, force: bool) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
self.send_job(
|
|
Some(format!("git worktree remove: {}", path.display()).into()),
|
|
move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.remove_worktree(path, force).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitRemoveWorktree {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
path: path.to_string_lossy().to_string(),
|
|
force,
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn rename_worktree(
|
|
&mut self,
|
|
old_path: PathBuf,
|
|
new_path: PathBuf,
|
|
) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
self.send_job(
|
|
Some(format!("git worktree move: {}", old_path.display()).into()),
|
|
move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.rename_worktree(old_path, new_path).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitRenameWorktree {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
old_path: old_path.to_string_lossy().to_string(),
|
|
new_path: new_path.to_string_lossy().to_string(),
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn default_branch(
|
|
&mut self,
|
|
include_remote_name: bool,
|
|
) -> oneshot::Receiver<Result<Option<SharedString>>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |repo, _| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.default_branch(include_remote_name).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::GetDefaultBranch {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
})
|
|
.await?;
|
|
|
|
anyhow::Ok(response.branch.map(SharedString::from))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn diff_tree(
|
|
&mut self,
|
|
diff_type: DiffTreeType,
|
|
_cx: &App,
|
|
) -> oneshot::Receiver<Result<TreeDiff>> {
|
|
let repository_id = self.snapshot.id;
|
|
self.send_job(None, move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.diff_tree(diff_type).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
|
|
let response = client
|
|
.request(proto::GetTreeDiff {
|
|
project_id: project_id.0,
|
|
repository_id: repository_id.0,
|
|
is_merge: matches!(diff_type, DiffTreeType::MergeBase { .. }),
|
|
base: diff_type.base().to_string(),
|
|
head: diff_type.head().to_string(),
|
|
})
|
|
.await?;
|
|
|
|
let entries = response
|
|
.entries
|
|
.into_iter()
|
|
.filter_map(|entry| {
|
|
let status = match entry.status() {
|
|
proto::tree_diff_status::Status::Added => TreeDiffStatus::Added,
|
|
proto::tree_diff_status::Status::Modified => {
|
|
TreeDiffStatus::Modified {
|
|
old: git::Oid::from_str(
|
|
&entry.oid.context("missing oid").log_err()?,
|
|
)
|
|
.log_err()?,
|
|
}
|
|
}
|
|
proto::tree_diff_status::Status::Deleted => {
|
|
TreeDiffStatus::Deleted {
|
|
old: git::Oid::from_str(
|
|
&entry.oid.context("missing oid").log_err()?,
|
|
)
|
|
.log_err()?,
|
|
}
|
|
}
|
|
};
|
|
Some((
|
|
RepoPath::from_rel_path(
|
|
&RelPath::from_proto(&entry.path).log_err()?,
|
|
),
|
|
status,
|
|
))
|
|
})
|
|
.collect();
|
|
|
|
Ok(TreeDiff { entries })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.diff(diff_type).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let (proto_diff_type, merge_base_ref) = match &diff_type {
|
|
DiffType::HeadToIndex => {
|
|
(proto::git_diff::DiffType::HeadToIndex.into(), None)
|
|
}
|
|
DiffType::HeadToWorktree => {
|
|
(proto::git_diff::DiffType::HeadToWorktree.into(), None)
|
|
}
|
|
DiffType::MergeBase { base_ref } => (
|
|
proto::git_diff::DiffType::MergeBase.into(),
|
|
Some(base_ref.to_string()),
|
|
),
|
|
};
|
|
let response = client
|
|
.request(proto::GitDiff {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
diff_type: proto_diff_type,
|
|
merge_base_ref,
|
|
})
|
|
.await?;
|
|
|
|
Ok(response.diff)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn create_branch(
|
|
&mut self,
|
|
branch_name: String,
|
|
base_branch: Option<String>,
|
|
) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
let status_msg = if let Some(ref base) = base_branch {
|
|
format!("git switch -c {branch_name} {base}").into()
|
|
} else {
|
|
format!("git switch -c {branch_name}").into()
|
|
};
|
|
self.send_job(Some(status_msg), move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.create_branch(branch_name, base_branch).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitCreateBranch {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
branch_name,
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn change_branch(&mut self, branch_name: String) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
self.send_job(
|
|
Some(format!("git switch {branch_name}").into()),
|
|
move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.change_branch(branch_name).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitChangeBranch {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
branch_name,
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn delete_branch(
|
|
&mut self,
|
|
is_remote: bool,
|
|
branch_name: String,
|
|
) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
self.send_job(
|
|
Some(
|
|
format!(
|
|
"git branch {} {}",
|
|
if is_remote { "-dr" } else { "-d" },
|
|
branch_name
|
|
)
|
|
.into(),
|
|
),
|
|
move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(state) => {
|
|
state.backend.delete_branch(is_remote, branch_name).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitDeleteBranch {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
is_remote,
|
|
branch_name,
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn rename_branch(
|
|
&mut self,
|
|
branch: String,
|
|
new_name: String,
|
|
) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
self.send_job(
|
|
Some(format!("git branch -m {branch} {new_name}").into()),
|
|
move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.rename_branch(branch, new_name).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitRenameBranch {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
branch,
|
|
new_name,
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn check_for_pushed_commits(&mut self) -> oneshot::Receiver<Result<Vec<SharedString>>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.check_for_pushed_commit().await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::CheckForPushedCommits {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
})
|
|
.await?;
|
|
|
|
let branches = response.pushed_to.into_iter().map(Into::into).collect();
|
|
|
|
Ok(branches)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn checkpoint(&mut self) -> oneshot::Receiver<Result<GitRepositoryCheckpoint>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.checkpoint().await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::GitCreateCheckpoint {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
})
|
|
.await?;
|
|
|
|
Ok(GitRepositoryCheckpoint {
|
|
commit_sha: Oid::from_bytes(&response.commit_sha)?,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn restore_checkpoint(
|
|
&mut self,
|
|
checkpoint: GitRepositoryCheckpoint,
|
|
) -> oneshot::Receiver<Result<()>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.restore_checkpoint(checkpoint).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
client
|
|
.request(proto::GitRestoreCheckpoint {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
commit_sha: checkpoint.commit_sha.as_bytes().to_vec(),
|
|
})
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub(crate) fn apply_remote_update(
|
|
&mut self,
|
|
update: proto::UpdateRepository,
|
|
cx: &mut Context<Self>,
|
|
) -> Result<()> {
|
|
if let Some(main_path) = &update.original_repo_abs_path {
|
|
self.snapshot.original_repo_abs_path = Path::new(main_path.as_str()).into();
|
|
}
|
|
|
|
let new_branch = update.branch_summary.as_ref().map(proto_to_branch);
|
|
let new_head_commit = update
|
|
.head_commit_details
|
|
.as_ref()
|
|
.map(proto_to_commit_details);
|
|
if self.snapshot.branch != new_branch || self.snapshot.head_commit != new_head_commit {
|
|
cx.emit(RepositoryEvent::BranchChanged)
|
|
}
|
|
self.snapshot.branch = new_branch;
|
|
self.snapshot.head_commit = new_head_commit;
|
|
|
|
// We don't store any merge head state for downstream projects; the upstream
|
|
// will track it and we will just get the updated conflicts
|
|
let new_merge_heads = TreeMap::from_ordered_entries(
|
|
update
|
|
.current_merge_conflicts
|
|
.into_iter()
|
|
.filter_map(|path| Some((RepoPath::from_proto(&path).ok()?, vec![]))),
|
|
);
|
|
let conflicts_changed =
|
|
self.snapshot.merge.merge_heads_by_conflicted_path != new_merge_heads;
|
|
self.snapshot.merge.merge_heads_by_conflicted_path = new_merge_heads;
|
|
self.snapshot.merge.message = update.merge_message.map(SharedString::from);
|
|
let new_stash_entries = GitStash {
|
|
entries: update
|
|
.stash_entries
|
|
.iter()
|
|
.filter_map(|entry| proto_to_stash(entry).ok())
|
|
.collect(),
|
|
};
|
|
if self.snapshot.stash_entries != new_stash_entries {
|
|
cx.emit(RepositoryEvent::StashEntriesChanged)
|
|
}
|
|
self.snapshot.stash_entries = new_stash_entries;
|
|
let new_linked_worktrees: Arc<[GitWorktree]> = update
|
|
.linked_worktrees
|
|
.iter()
|
|
.map(proto_to_worktree)
|
|
.collect();
|
|
if *self.snapshot.linked_worktrees != *new_linked_worktrees {
|
|
cx.emit(RepositoryEvent::GitWorktreeListChanged);
|
|
}
|
|
self.snapshot.linked_worktrees = new_linked_worktrees;
|
|
self.snapshot.remote_upstream_url = update.remote_upstream_url;
|
|
self.snapshot.remote_origin_url = update.remote_origin_url;
|
|
|
|
let edits = update
|
|
.removed_statuses
|
|
.into_iter()
|
|
.filter_map(|path| {
|
|
Some(sum_tree::Edit::Remove(PathKey(
|
|
RelPath::from_proto(&path).log_err()?,
|
|
)))
|
|
})
|
|
.chain(
|
|
update
|
|
.updated_statuses
|
|
.into_iter()
|
|
.filter_map(|updated_status| {
|
|
Some(sum_tree::Edit::Insert(updated_status.try_into().log_err()?))
|
|
}),
|
|
)
|
|
.collect::<Vec<_>>();
|
|
if conflicts_changed || !edits.is_empty() {
|
|
cx.emit(RepositoryEvent::StatusesChanged);
|
|
}
|
|
self.snapshot.statuses_by_path.edit(edits, ());
|
|
|
|
if update.is_last_update {
|
|
self.snapshot.scan_id = update.scan_id;
|
|
}
|
|
self.clear_pending_ops(cx);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn compare_checkpoints(
|
|
&mut self,
|
|
left: GitRepositoryCheckpoint,
|
|
right: GitRepositoryCheckpoint,
|
|
) -> oneshot::Receiver<Result<bool>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.compare_checkpoints(left, right).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::GitCompareCheckpoints {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
left_commit_sha: left.commit_sha.as_bytes().to_vec(),
|
|
right_commit_sha: right.commit_sha.as_bytes().to_vec(),
|
|
})
|
|
.await?;
|
|
Ok(response.equal)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn diff_checkpoints(
|
|
&mut self,
|
|
base_checkpoint: GitRepositoryCheckpoint,
|
|
target_checkpoint: GitRepositoryCheckpoint,
|
|
) -> oneshot::Receiver<Result<String>> {
|
|
let id = self.id;
|
|
self.send_job(None, move |repo, _cx| async move {
|
|
match repo {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend
|
|
.diff_checkpoints(base_checkpoint, target_checkpoint)
|
|
.await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::GitDiffCheckpoints {
|
|
project_id: project_id.0,
|
|
repository_id: id.to_proto(),
|
|
base_commit_sha: base_checkpoint.commit_sha.as_bytes().to_vec(),
|
|
target_commit_sha: target_checkpoint.commit_sha.as_bytes().to_vec(),
|
|
})
|
|
.await?;
|
|
Ok(response.diff)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn clear_pending_ops(&mut self, cx: &mut Context<Self>) {
|
|
let updated = SumTree::from_iter(
|
|
self.pending_ops.iter().filter_map(|ops| {
|
|
let inner_ops: Vec<PendingOp> =
|
|
ops.ops.iter().filter(|op| op.running()).cloned().collect();
|
|
if inner_ops.is_empty() {
|
|
None
|
|
} else {
|
|
Some(PendingOps {
|
|
repo_path: ops.repo_path.clone(),
|
|
ops: inner_ops,
|
|
})
|
|
}
|
|
}),
|
|
(),
|
|
);
|
|
|
|
if updated != self.pending_ops {
|
|
cx.emit(RepositoryEvent::PendingOpsChanged {
|
|
pending_ops: self.pending_ops.clone(),
|
|
})
|
|
}
|
|
|
|
self.pending_ops = updated;
|
|
}
|
|
|
|
fn schedule_scan(
|
|
&mut self,
|
|
updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let this = cx.weak_entity();
|
|
let _ = self.send_keyed_job(
|
|
Some(GitJobKey::ReloadGitState),
|
|
None,
|
|
|state, mut cx| async move {
|
|
log::debug!("run scheduled git status scan");
|
|
|
|
let Some(this) = this.upgrade() else {
|
|
return Ok(());
|
|
};
|
|
let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else {
|
|
bail!("not a local repository")
|
|
};
|
|
let snapshot = compute_snapshot(this.clone(), backend.clone(), &mut cx).await?;
|
|
this.update(&mut cx, |this, cx| {
|
|
this.clear_pending_ops(cx);
|
|
});
|
|
if let Some(updates_tx) = updates_tx {
|
|
updates_tx
|
|
.unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
|
|
.ok();
|
|
}
|
|
Ok(())
|
|
},
|
|
);
|
|
}
|
|
|
|
fn spawn_local_git_worker(
|
|
state: Shared<Task<Result<LocalRepositoryState, String>>>,
|
|
cx: &mut Context<Self>,
|
|
) -> mpsc::UnboundedSender<GitJob> {
|
|
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
|
|
|
|
cx.spawn(async move |_, cx| {
|
|
let state = state.await.map_err(|err| anyhow::anyhow!(err))?;
|
|
if let Some(git_hosting_provider_registry) =
|
|
cx.update(|cx| GitHostingProviderRegistry::try_global(cx))
|
|
{
|
|
git_hosting_providers::register_additional_providers(
|
|
git_hosting_provider_registry,
|
|
state.backend.clone(),
|
|
)
|
|
.await;
|
|
}
|
|
let state = RepositoryState::Local(state);
|
|
let mut jobs = VecDeque::new();
|
|
loop {
|
|
while let Ok(Some(next_job)) = job_rx.try_next() {
|
|
jobs.push_back(next_job);
|
|
}
|
|
|
|
if let Some(job) = jobs.pop_front() {
|
|
if let Some(current_key) = &job.key
|
|
&& jobs
|
|
.iter()
|
|
.any(|other_job| other_job.key.as_ref() == Some(current_key))
|
|
{
|
|
continue;
|
|
}
|
|
(job.job)(state.clone(), cx).await;
|
|
} else if let Some(job) = job_rx.next().await {
|
|
jobs.push_back(job);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
anyhow::Ok(())
|
|
})
|
|
.detach_and_log_err(cx);
|
|
|
|
job_tx
|
|
}
|
|
|
|
fn spawn_remote_git_worker(
|
|
state: RemoteRepositoryState,
|
|
cx: &mut Context<Self>,
|
|
) -> mpsc::UnboundedSender<GitJob> {
|
|
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
|
|
|
|
cx.spawn(async move |_, cx| {
|
|
let state = RepositoryState::Remote(state);
|
|
let mut jobs = VecDeque::new();
|
|
loop {
|
|
while let Ok(Some(next_job)) = job_rx.try_next() {
|
|
jobs.push_back(next_job);
|
|
}
|
|
|
|
if let Some(job) = jobs.pop_front() {
|
|
if let Some(current_key) = &job.key
|
|
&& jobs
|
|
.iter()
|
|
.any(|other_job| other_job.key.as_ref() == Some(current_key))
|
|
{
|
|
continue;
|
|
}
|
|
(job.job)(state.clone(), cx).await;
|
|
} else if let Some(job) = job_rx.next().await {
|
|
jobs.push_back(job);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
anyhow::Ok(())
|
|
})
|
|
.detach_and_log_err(cx);
|
|
|
|
job_tx
|
|
}
|
|
|
|
fn load_staged_text(
|
|
&mut self,
|
|
buffer_id: BufferId,
|
|
repo_path: RepoPath,
|
|
cx: &App,
|
|
) -> Task<Result<Option<String>>> {
|
|
let rx = self.send_job(None, move |state, _| async move {
|
|
match state {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
anyhow::Ok(backend.load_index_text(repo_path).await)
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
let response = client
|
|
.request(proto::OpenUnstagedDiff {
|
|
project_id: project_id.to_proto(),
|
|
buffer_id: buffer_id.to_proto(),
|
|
})
|
|
.await?;
|
|
Ok(response.staged_text)
|
|
}
|
|
}
|
|
});
|
|
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
|
}
|
|
|
|
fn load_committed_text(
|
|
&mut self,
|
|
buffer_id: BufferId,
|
|
repo_path: RepoPath,
|
|
cx: &App,
|
|
) -> Task<Result<DiffBasesChange>> {
|
|
let rx = self.send_job(None, move |state, _| async move {
|
|
match state {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
let committed_text = backend.load_committed_text(repo_path.clone()).await;
|
|
let staged_text = backend.load_index_text(repo_path).await;
|
|
let diff_bases_change = if committed_text == staged_text {
|
|
DiffBasesChange::SetBoth(committed_text)
|
|
} else {
|
|
DiffBasesChange::SetEach {
|
|
index: staged_text,
|
|
head: committed_text,
|
|
}
|
|
};
|
|
anyhow::Ok(diff_bases_change)
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
|
use proto::open_uncommitted_diff_response::Mode;
|
|
|
|
let response = client
|
|
.request(proto::OpenUncommittedDiff {
|
|
project_id: project_id.to_proto(),
|
|
buffer_id: buffer_id.to_proto(),
|
|
})
|
|
.await?;
|
|
let mode = Mode::from_i32(response.mode).context("Invalid mode")?;
|
|
let bases = match mode {
|
|
Mode::IndexMatchesHead => DiffBasesChange::SetBoth(response.committed_text),
|
|
Mode::IndexAndHead => DiffBasesChange::SetEach {
|
|
head: response.committed_text,
|
|
index: response.staged_text,
|
|
},
|
|
};
|
|
Ok(bases)
|
|
}
|
|
}
|
|
});
|
|
|
|
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
|
}
|
|
|
|
fn load_blob_content(&mut self, oid: Oid, cx: &App) -> Task<Result<String>> {
|
|
let repository_id = self.snapshot.id;
|
|
let rx = self.send_job(None, move |state, _| async move {
|
|
match state {
|
|
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
|
backend.load_blob_content(oid).await
|
|
}
|
|
RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
|
|
let response = client
|
|
.request(proto::GetBlobContent {
|
|
project_id: project_id.to_proto(),
|
|
repository_id: repository_id.0,
|
|
oid: oid.to_string(),
|
|
})
|
|
.await?;
|
|
Ok(response.content)
|
|
}
|
|
}
|
|
});
|
|
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
|
}
|
|
|
|
fn paths_changed(
|
|
&mut self,
|
|
paths: Vec<RepoPath>,
|
|
updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if !paths.is_empty() {
|
|
self.paths_needing_status_update.push(paths);
|
|
}
|
|
|
|
let this = cx.weak_entity();
|
|
let _ = self.send_keyed_job(
|
|
Some(GitJobKey::RefreshStatuses),
|
|
None,
|
|
|state, mut cx| async move {
|
|
let (prev_snapshot, changed_paths) = this.update(&mut cx, |this, _| {
|
|
(
|
|
this.snapshot.clone(),
|
|
mem::take(&mut this.paths_needing_status_update),
|
|
)
|
|
})?;
|
|
let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else {
|
|
bail!("not a local repository")
|
|
};
|
|
|
|
if changed_paths.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let has_head = prev_snapshot.head_commit.is_some();
|
|
|
|
let stash_entries = backend.stash_entries().await?;
|
|
let changed_path_statuses = cx
|
|
.background_spawn(async move {
|
|
let mut changed_paths =
|
|
changed_paths.into_iter().flatten().collect::<BTreeSet<_>>();
|
|
let changed_paths_vec = changed_paths.iter().cloned().collect::<Vec<_>>();
|
|
|
|
let status_task = backend.status(&changed_paths_vec);
|
|
let diff_stat_future = if has_head {
|
|
backend.diff_stat(&changed_paths_vec)
|
|
} else {
|
|
future::ready(Ok(status::GitDiffStat {
|
|
entries: Arc::default(),
|
|
}))
|
|
.boxed()
|
|
};
|
|
|
|
let (statuses, diff_stats) =
|
|
futures::future::try_join(status_task, diff_stat_future).await?;
|
|
|
|
let diff_stats: HashMap<RepoPath, DiffStat> =
|
|
HashMap::from_iter(diff_stats.entries.into_iter().cloned());
|
|
|
|
let mut changed_path_statuses = Vec::new();
|
|
let prev_statuses = prev_snapshot.statuses_by_path.clone();
|
|
let mut cursor = prev_statuses.cursor::<PathProgress>(());
|
|
|
|
for (repo_path, status) in &*statuses.entries {
|
|
let current_diff_stat = diff_stats.get(repo_path).copied();
|
|
|
|
changed_paths.remove(repo_path);
|
|
if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left)
|
|
&& cursor.item().is_some_and(|entry| {
|
|
entry.status == *status && entry.diff_stat == current_diff_stat
|
|
})
|
|
{
|
|
continue;
|
|
}
|
|
|
|
changed_path_statuses.push(Edit::Insert(StatusEntry {
|
|
repo_path: repo_path.clone(),
|
|
status: *status,
|
|
diff_stat: current_diff_stat,
|
|
}));
|
|
}
|
|
let mut cursor = prev_statuses.cursor::<PathProgress>(());
|
|
for path in changed_paths.into_iter() {
|
|
if cursor.seek_forward(&PathTarget::Path(&path), Bias::Left) {
|
|
changed_path_statuses
|
|
.push(Edit::Remove(PathKey(path.as_ref().clone())));
|
|
}
|
|
}
|
|
anyhow::Ok(changed_path_statuses)
|
|
})
|
|
.await?;
|
|
|
|
this.update(&mut cx, |this, cx| {
|
|
if this.snapshot.stash_entries != stash_entries {
|
|
cx.emit(RepositoryEvent::StashEntriesChanged);
|
|
this.snapshot.stash_entries = stash_entries;
|
|
}
|
|
|
|
if !changed_path_statuses.is_empty() {
|
|
cx.emit(RepositoryEvent::StatusesChanged);
|
|
this.snapshot
|
|
.statuses_by_path
|
|
.edit(changed_path_statuses, ());
|
|
this.snapshot.scan_id += 1;
|
|
}
|
|
|
|
if let Some(updates_tx) = updates_tx {
|
|
updates_tx
|
|
.unbounded_send(DownstreamUpdate::UpdateRepository(
|
|
this.snapshot.clone(),
|
|
))
|
|
.ok();
|
|
}
|
|
})
|
|
},
|
|
);
|
|
}
|
|
|
|
/// currently running git command and when it started
|
|
pub fn current_job(&self) -> Option<JobInfo> {
|
|
self.active_jobs.values().next().cloned()
|
|
}
|
|
|
|
pub fn barrier(&mut self) -> oneshot::Receiver<()> {
|
|
self.send_job(None, |_, _| async {})
|
|
}
|
|
|
|
fn spawn_job_with_tracking<AsyncFn>(
|
|
&mut self,
|
|
paths: Vec<RepoPath>,
|
|
git_status: pending_op::GitStatus,
|
|
cx: &mut Context<Self>,
|
|
f: AsyncFn,
|
|
) -> Task<Result<()>>
|
|
where
|
|
AsyncFn: AsyncFnOnce(WeakEntity<Repository>, &mut AsyncApp) -> Result<()> + 'static,
|
|
{
|
|
let ids = self.new_pending_ops_for_paths(paths, git_status);
|
|
|
|
cx.spawn(async move |this, cx| {
|
|
let (job_status, result) = match f(this.clone(), cx).await {
|
|
Ok(()) => (pending_op::JobStatus::Finished, Ok(())),
|
|
Err(err) if err.is::<Canceled>() => (pending_op::JobStatus::Skipped, Ok(())),
|
|
Err(err) => (pending_op::JobStatus::Error, Err(err)),
|
|
};
|
|
|
|
this.update(cx, |this, _| {
|
|
let mut edits = Vec::with_capacity(ids.len());
|
|
for (id, entry) in ids {
|
|
if let Some(mut ops) = this
|
|
.pending_ops
|
|
.get(&PathKey(entry.as_ref().clone()), ())
|
|
.cloned()
|
|
{
|
|
if let Some(op) = ops.op_by_id_mut(id) {
|
|
op.job_status = job_status;
|
|
}
|
|
edits.push(sum_tree::Edit::Insert(ops));
|
|
}
|
|
}
|
|
this.pending_ops.edit(edits, ());
|
|
})?;
|
|
|
|
result
|
|
})
|
|
}
|
|
|
|
fn new_pending_ops_for_paths(
|
|
&mut self,
|
|
paths: Vec<RepoPath>,
|
|
git_status: pending_op::GitStatus,
|
|
) -> Vec<(PendingOpId, RepoPath)> {
|
|
let mut edits = Vec::with_capacity(paths.len());
|
|
let mut ids = Vec::with_capacity(paths.len());
|
|
for path in paths {
|
|
let mut ops = self
|
|
.pending_ops
|
|
.get(&PathKey(path.as_ref().clone()), ())
|
|
.cloned()
|
|
.unwrap_or_else(|| PendingOps::new(&path));
|
|
let id = ops.max_id() + 1;
|
|
ops.ops.push(PendingOp {
|
|
id,
|
|
git_status,
|
|
job_status: pending_op::JobStatus::Running,
|
|
});
|
|
edits.push(sum_tree::Edit::Insert(ops));
|
|
ids.push((id, path));
|
|
}
|
|
self.pending_ops.edit(edits, ());
|
|
ids
|
|
}
|
|
pub fn default_remote_url(&self) -> Option<String> {
|
|
self.remote_upstream_url
|
|
.clone()
|
|
.or(self.remote_origin_url.clone())
|
|
}
|
|
}
|
|
|
|
/// If `path` is a git linked worktree checkout, resolves it to the main
|
|
/// repository's working directory path. Returns `None` if `path` is a normal
|
|
/// repository, not a git repo, or if resolution fails.
|
|
///
|
|
/// Resolution works by:
|
|
/// 1. Reading the `.git` file to get the `gitdir:` pointer
|
|
/// 2. Following that to the worktree-specific git directory
|
|
/// 3. Reading the `commondir` file to find the shared `.git` directory
|
|
/// 4. Deriving the main repo's working directory from the common dir
|
|
pub async fn resolve_git_worktree_to_main_repo(fs: &dyn Fs, path: &Path) -> Option<PathBuf> {
|
|
let dot_git = path.join(".git");
|
|
let metadata = fs.metadata(&dot_git).await.ok()??;
|
|
if metadata.is_dir {
|
|
return None; // Normal repo, not a linked worktree
|
|
}
|
|
// It's a .git file — parse the gitdir: pointer
|
|
let content = fs.load(&dot_git).await.ok()?;
|
|
let gitdir_rel = content.strip_prefix("gitdir:")?.trim();
|
|
let gitdir_abs = fs.canonicalize(&path.join(gitdir_rel)).await.ok()?;
|
|
// Read commondir to find the main .git directory
|
|
let commondir_content = fs.load(&gitdir_abs.join("commondir")).await.ok()?;
|
|
let common_dir = fs
|
|
.canonicalize(&gitdir_abs.join(commondir_content.trim()))
|
|
.await
|
|
.ok()?;
|
|
Some(git::repository::original_repo_path_from_common_dir(
|
|
&common_dir,
|
|
))
|
|
}
|
|
|
|
/// Validates that the resolved worktree directory is acceptable:
|
|
/// - The setting must not be an absolute path.
|
|
/// - The resolved path must be either a subdirectory of the working
|
|
/// directory or a subdirectory of its parent (i.e., a sibling).
|
|
///
|
|
/// Returns `Ok(resolved_path)` or an error with a user-facing message.
|
|
pub fn worktrees_directory_for_repo(
|
|
original_repo_abs_path: &Path,
|
|
worktree_directory_setting: &str,
|
|
) -> Result<PathBuf> {
|
|
// Check the original setting before trimming, since a path like "///"
|
|
// is absolute but becomes "" after stripping trailing separators.
|
|
// Also check for leading `/` or `\` explicitly, because on Windows
|
|
// `Path::is_absolute()` requires a drive letter — so `/tmp/worktrees`
|
|
// would slip through even though it's clearly not a relative path.
|
|
if Path::new(worktree_directory_setting).is_absolute()
|
|
|| worktree_directory_setting.starts_with('/')
|
|
|| worktree_directory_setting.starts_with('\\')
|
|
{
|
|
anyhow::bail!(
|
|
"git.worktree_directory must be a relative path, got: {worktree_directory_setting:?}"
|
|
);
|
|
}
|
|
|
|
if worktree_directory_setting.is_empty() {
|
|
anyhow::bail!("git.worktree_directory must not be empty");
|
|
}
|
|
|
|
let trimmed = worktree_directory_setting.trim_end_matches(['/', '\\']);
|
|
if trimmed == ".." {
|
|
anyhow::bail!("git.worktree_directory must not be \"..\" (use \"../some-name\" instead)");
|
|
}
|
|
|
|
let joined = original_repo_abs_path.join(trimmed);
|
|
let resolved = util::normalize_path(&joined);
|
|
let resolved = if resolved.starts_with(original_repo_abs_path) {
|
|
resolved
|
|
} else if let Some(repo_dir_name) = original_repo_abs_path.file_name() {
|
|
resolved.join(repo_dir_name)
|
|
} else {
|
|
resolved
|
|
};
|
|
|
|
let parent = original_repo_abs_path
|
|
.parent()
|
|
.unwrap_or(original_repo_abs_path);
|
|
|
|
if !resolved.starts_with(parent) {
|
|
anyhow::bail!(
|
|
"git.worktree_directory resolved to {resolved:?}, which is outside \
|
|
the project root and its parent directory. It must resolve to a \
|
|
subdirectory of {original_repo_abs_path:?} or a sibling of it."
|
|
);
|
|
}
|
|
|
|
Ok(resolved)
|
|
}
|
|
|
|
/// Returns a short name for a linked worktree suitable for UI display
|
|
///
|
|
/// Uses the main worktree path to come up with a short name that disambiguates
|
|
/// the linked worktree from the main worktree.
|
|
pub fn linked_worktree_short_name(
|
|
main_worktree_path: &Path,
|
|
linked_worktree_path: &Path,
|
|
) -> Option<SharedString> {
|
|
if main_worktree_path == linked_worktree_path {
|
|
return None;
|
|
}
|
|
|
|
let project_name = main_worktree_path.file_name()?.to_str()?;
|
|
let directory_name = linked_worktree_path.file_name()?.to_str()?;
|
|
let name = if directory_name != project_name {
|
|
directory_name.to_string()
|
|
} else {
|
|
linked_worktree_path
|
|
.parent()?
|
|
.file_name()?
|
|
.to_str()?
|
|
.to_string()
|
|
};
|
|
Some(name.into())
|
|
}
|
|
|
|
fn get_permalink_in_rust_registry_src(
|
|
provider_registry: Arc<GitHostingProviderRegistry>,
|
|
path: PathBuf,
|
|
selection: Range<u32>,
|
|
) -> Result<url::Url> {
|
|
#[derive(Deserialize)]
|
|
struct CargoVcsGit {
|
|
sha1: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CargoVcsInfo {
|
|
git: CargoVcsGit,
|
|
path_in_vcs: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CargoPackage {
|
|
repository: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CargoToml {
|
|
package: CargoPackage,
|
|
}
|
|
|
|
let Some((dir, cargo_vcs_info_json)) = path.ancestors().skip(1).find_map(|dir| {
|
|
let json = std::fs::read_to_string(dir.join(".cargo_vcs_info.json")).ok()?;
|
|
Some((dir, json))
|
|
}) else {
|
|
bail!("No .cargo_vcs_info.json found in parent directories")
|
|
};
|
|
let cargo_vcs_info = serde_json::from_str::<CargoVcsInfo>(&cargo_vcs_info_json)?;
|
|
let cargo_toml = std::fs::read_to_string(dir.join("Cargo.toml"))?;
|
|
let manifest = toml::from_str::<CargoToml>(&cargo_toml)?;
|
|
let (provider, remote) = parse_git_remote_url(provider_registry, &manifest.package.repository)
|
|
.context("parsing package.repository field of manifest")?;
|
|
let path = PathBuf::from(cargo_vcs_info.path_in_vcs).join(path.strip_prefix(dir).unwrap());
|
|
let permalink = provider.build_permalink(
|
|
remote,
|
|
BuildPermalinkParams::new(
|
|
&cargo_vcs_info.git.sha1,
|
|
&RepoPath::from_rel_path(
|
|
&RelPath::new(&path, PathStyle::local()).context("invalid path")?,
|
|
),
|
|
Some(selection),
|
|
),
|
|
);
|
|
Ok(permalink)
|
|
}
|
|
|
|
fn serialize_blame_buffer_response(blame: Option<git::blame::Blame>) -> proto::BlameBufferResponse {
|
|
let Some(blame) = blame else {
|
|
return proto::BlameBufferResponse {
|
|
blame_response: None,
|
|
};
|
|
};
|
|
|
|
let entries = blame
|
|
.entries
|
|
.into_iter()
|
|
.map(|entry| proto::BlameEntry {
|
|
sha: entry.sha.as_bytes().into(),
|
|
start_line: entry.range.start,
|
|
end_line: entry.range.end,
|
|
original_line_number: entry.original_line_number,
|
|
author: entry.author,
|
|
author_mail: entry.author_mail,
|
|
author_time: entry.author_time,
|
|
author_tz: entry.author_tz,
|
|
committer: entry.committer_name,
|
|
committer_mail: entry.committer_email,
|
|
committer_time: entry.committer_time,
|
|
committer_tz: entry.committer_tz,
|
|
summary: entry.summary,
|
|
previous: entry.previous,
|
|
filename: entry.filename,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let messages = blame
|
|
.messages
|
|
.into_iter()
|
|
.map(|(oid, message)| proto::CommitMessage {
|
|
oid: oid.as_bytes().into(),
|
|
message,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
proto::BlameBufferResponse {
|
|
blame_response: Some(proto::blame_buffer_response::BlameResponse { entries, messages }),
|
|
}
|
|
}
|
|
|
|
fn deserialize_blame_buffer_response(
|
|
response: proto::BlameBufferResponse,
|
|
) -> Option<git::blame::Blame> {
|
|
let response = response.blame_response?;
|
|
let entries = response
|
|
.entries
|
|
.into_iter()
|
|
.filter_map(|entry| {
|
|
Some(git::blame::BlameEntry {
|
|
sha: git::Oid::from_bytes(&entry.sha).ok()?,
|
|
range: entry.start_line..entry.end_line,
|
|
original_line_number: entry.original_line_number,
|
|
committer_name: entry.committer,
|
|
committer_time: entry.committer_time,
|
|
committer_tz: entry.committer_tz,
|
|
committer_email: entry.committer_mail,
|
|
author: entry.author,
|
|
author_mail: entry.author_mail,
|
|
author_time: entry.author_time,
|
|
author_tz: entry.author_tz,
|
|
summary: entry.summary,
|
|
previous: entry.previous,
|
|
filename: entry.filename,
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let messages = response
|
|
.messages
|
|
.into_iter()
|
|
.filter_map(|message| Some((git::Oid::from_bytes(&message.oid).ok()?, message.message)))
|
|
.collect::<HashMap<_, _>>();
|
|
|
|
Some(Blame { entries, messages })
|
|
}
|
|
|
|
fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {
|
|
proto::Branch {
|
|
is_head: branch.is_head,
|
|
ref_name: branch.ref_name.to_string(),
|
|
unix_timestamp: branch
|
|
.most_recent_commit
|
|
.as_ref()
|
|
.map(|commit| commit.commit_timestamp as u64),
|
|
upstream: branch.upstream.as_ref().map(|upstream| proto::GitUpstream {
|
|
ref_name: upstream.ref_name.to_string(),
|
|
tracking: upstream
|
|
.tracking
|
|
.status()
|
|
.map(|upstream| proto::UpstreamTracking {
|
|
ahead: upstream.ahead as u64,
|
|
behind: upstream.behind as u64,
|
|
}),
|
|
}),
|
|
most_recent_commit: branch
|
|
.most_recent_commit
|
|
.as_ref()
|
|
.map(|commit| proto::CommitSummary {
|
|
sha: commit.sha.to_string(),
|
|
subject: commit.subject.to_string(),
|
|
commit_timestamp: commit.commit_timestamp,
|
|
author_name: commit.author_name.to_string(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn worktree_to_proto(worktree: &git::repository::Worktree) -> proto::Worktree {
|
|
proto::Worktree {
|
|
path: worktree.path.to_string_lossy().to_string(),
|
|
ref_name: worktree
|
|
.ref_name
|
|
.as_ref()
|
|
.map(|s| s.to_string())
|
|
.unwrap_or_default(),
|
|
sha: worktree.sha.to_string(),
|
|
is_main: worktree.is_main,
|
|
}
|
|
}
|
|
|
|
fn proto_to_worktree(proto: &proto::Worktree) -> git::repository::Worktree {
|
|
git::repository::Worktree {
|
|
path: PathBuf::from(proto.path.clone()),
|
|
ref_name: Some(SharedString::from(&proto.ref_name)),
|
|
sha: proto.sha.clone().into(),
|
|
is_main: proto.is_main,
|
|
}
|
|
}
|
|
|
|
fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
|
|
git::repository::Branch {
|
|
is_head: proto.is_head,
|
|
ref_name: proto.ref_name.clone().into(),
|
|
upstream: proto
|
|
.upstream
|
|
.as_ref()
|
|
.map(|upstream| git::repository::Upstream {
|
|
ref_name: upstream.ref_name.to_string().into(),
|
|
tracking: upstream
|
|
.tracking
|
|
.as_ref()
|
|
.map(|tracking| {
|
|
git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus {
|
|
ahead: tracking.ahead as u32,
|
|
behind: tracking.behind as u32,
|
|
})
|
|
})
|
|
.unwrap_or(git::repository::UpstreamTracking::Gone),
|
|
}),
|
|
most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {
|
|
git::repository::CommitSummary {
|
|
sha: commit.sha.to_string().into(),
|
|
subject: commit.subject.to_string().into(),
|
|
commit_timestamp: commit.commit_timestamp,
|
|
author_name: commit.author_name.to_string().into(),
|
|
has_parent: true,
|
|
}
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn commit_details_to_proto(commit: &CommitDetails) -> proto::GitCommitDetails {
|
|
proto::GitCommitDetails {
|
|
sha: commit.sha.to_string(),
|
|
message: commit.message.to_string(),
|
|
commit_timestamp: commit.commit_timestamp,
|
|
author_email: commit.author_email.to_string(),
|
|
author_name: commit.author_name.to_string(),
|
|
}
|
|
}
|
|
|
|
fn proto_to_commit_details(proto: &proto::GitCommitDetails) -> CommitDetails {
|
|
CommitDetails {
|
|
sha: proto.sha.clone().into(),
|
|
message: proto.message.clone().into(),
|
|
commit_timestamp: proto.commit_timestamp,
|
|
author_email: proto.author_email.clone().into(),
|
|
author_name: proto.author_name.clone().into(),
|
|
}
|
|
}
|
|
|
|
/// This snapshot computes the repository state on the foreground thread while
|
|
/// running the git commands on the background thread. We update branch, head,
|
|
/// remotes, and worktrees first so the UI can react sooner, then compute file
|
|
/// state and emit those events immediately after.
|
|
async fn compute_snapshot(
|
|
this: Entity<Repository>,
|
|
backend: Arc<dyn GitRepository>,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<RepositorySnapshot> {
|
|
let (id, work_directory_abs_path, prev_snapshot) = this.update(cx, |this, _| {
|
|
this.paths_needing_status_update.clear();
|
|
(
|
|
this.id,
|
|
this.work_directory_abs_path.clone(),
|
|
this.snapshot.clone(),
|
|
)
|
|
});
|
|
|
|
let head_commit_future = {
|
|
let backend = backend.clone();
|
|
async move {
|
|
Ok(match backend.head_sha().await {
|
|
Some(head_sha) => backend.show(head_sha).await.log_err(),
|
|
None => None,
|
|
})
|
|
}
|
|
};
|
|
let (branches, head_commit, all_worktrees) = cx
|
|
.background_spawn({
|
|
let backend = backend.clone();
|
|
async move {
|
|
futures::future::try_join3(
|
|
backend.branches(),
|
|
head_commit_future,
|
|
backend.worktrees(),
|
|
)
|
|
.await
|
|
}
|
|
})
|
|
.await?;
|
|
let branch = branches.into_iter().find(|branch| branch.is_head);
|
|
|
|
let linked_worktrees: Arc<[GitWorktree]> = all_worktrees
|
|
.into_iter()
|
|
.filter(|wt| wt.path != *work_directory_abs_path)
|
|
.collect();
|
|
|
|
let (remote_origin_url, remote_upstream_url) = cx
|
|
.background_spawn({
|
|
let backend = backend.clone();
|
|
async move {
|
|
Ok::<_, anyhow::Error>(
|
|
futures::future::join(
|
|
backend.remote_url("origin"),
|
|
backend.remote_url("upstream"),
|
|
)
|
|
.await,
|
|
)
|
|
}
|
|
})
|
|
.await?;
|
|
|
|
let snapshot = this.update(cx, |this, cx| {
|
|
let branch_changed =
|
|
branch != this.snapshot.branch || head_commit != this.snapshot.head_commit;
|
|
let worktrees_changed = *linked_worktrees != *this.snapshot.linked_worktrees;
|
|
|
|
this.snapshot = RepositorySnapshot {
|
|
id,
|
|
work_directory_abs_path,
|
|
branch,
|
|
head_commit,
|
|
remote_origin_url,
|
|
remote_upstream_url,
|
|
linked_worktrees,
|
|
scan_id: prev_snapshot.scan_id + 1,
|
|
..prev_snapshot
|
|
};
|
|
|
|
if branch_changed {
|
|
cx.emit(RepositoryEvent::BranchChanged);
|
|
}
|
|
|
|
if worktrees_changed {
|
|
cx.emit(RepositoryEvent::GitWorktreeListChanged);
|
|
}
|
|
|
|
this.snapshot.clone()
|
|
});
|
|
|
|
let (statuses, diff_stats, stash_entries) = cx
|
|
.background_spawn({
|
|
let backend = backend.clone();
|
|
let snapshot = snapshot.clone();
|
|
async move {
|
|
let diff_stat_future: BoxFuture<'_, Result<status::GitDiffStat>> =
|
|
if snapshot.head_commit.is_some() {
|
|
backend.diff_stat(&[])
|
|
} else {
|
|
future::ready(Ok(status::GitDiffStat {
|
|
entries: Arc::default(),
|
|
}))
|
|
.boxed()
|
|
};
|
|
futures::future::try_join3(
|
|
backend.status(&[RepoPath::from_rel_path(
|
|
&RelPath::new(".".as_ref(), PathStyle::local()).unwrap(),
|
|
)]),
|
|
diff_stat_future,
|
|
backend.stash_entries(),
|
|
)
|
|
.await
|
|
}
|
|
})
|
|
.await?;
|
|
|
|
let diff_stat_map: HashMap<&RepoPath, DiffStat> =
|
|
diff_stats.entries.iter().map(|(p, s)| (p, *s)).collect();
|
|
let mut conflicted_paths = Vec::new();
|
|
let statuses_by_path = SumTree::from_iter(
|
|
statuses.entries.iter().map(|(repo_path, status)| {
|
|
if status.is_conflicted() {
|
|
conflicted_paths.push(repo_path.clone());
|
|
}
|
|
StatusEntry {
|
|
repo_path: repo_path.clone(),
|
|
status: *status,
|
|
diff_stat: diff_stat_map.get(repo_path).copied(),
|
|
}
|
|
}),
|
|
(),
|
|
);
|
|
|
|
let merge_details = cx
|
|
.background_spawn({
|
|
let backend = backend.clone();
|
|
let mut merge_details = snapshot.merge.clone();
|
|
async move {
|
|
let conflicts_changed = merge_details.update(&backend, conflicted_paths).await?;
|
|
Ok::<_, anyhow::Error>((merge_details, conflicts_changed))
|
|
}
|
|
})
|
|
.await?;
|
|
let (merge_details, conflicts_changed) = merge_details;
|
|
log::debug!("new merge details: {merge_details:?}");
|
|
|
|
Ok(this.update(cx, |this, cx| {
|
|
if conflicts_changed || statuses_by_path != this.snapshot.statuses_by_path {
|
|
cx.emit(RepositoryEvent::StatusesChanged);
|
|
}
|
|
if stash_entries != this.snapshot.stash_entries {
|
|
cx.emit(RepositoryEvent::StashEntriesChanged);
|
|
}
|
|
|
|
this.snapshot.scan_id += 1;
|
|
this.snapshot.merge = merge_details;
|
|
this.snapshot.statuses_by_path = statuses_by_path;
|
|
this.snapshot.stash_entries = stash_entries;
|
|
|
|
this.snapshot.clone()
|
|
}))
|
|
}
|
|
|
|
fn status_from_proto(
|
|
simple_status: i32,
|
|
status: Option<proto::GitFileStatus>,
|
|
) -> anyhow::Result<FileStatus> {
|
|
use proto::git_file_status::Variant;
|
|
|
|
let Some(variant) = status.and_then(|status| status.variant) else {
|
|
let code = proto::GitStatus::from_i32(simple_status)
|
|
.with_context(|| format!("Invalid git status code: {simple_status}"))?;
|
|
let result = match code {
|
|
proto::GitStatus::Added => TrackedStatus {
|
|
worktree_status: StatusCode::Added,
|
|
index_status: StatusCode::Unmodified,
|
|
}
|
|
.into(),
|
|
proto::GitStatus::Modified => TrackedStatus {
|
|
worktree_status: StatusCode::Modified,
|
|
index_status: StatusCode::Unmodified,
|
|
}
|
|
.into(),
|
|
proto::GitStatus::Conflict => UnmergedStatus {
|
|
first_head: UnmergedStatusCode::Updated,
|
|
second_head: UnmergedStatusCode::Updated,
|
|
}
|
|
.into(),
|
|
proto::GitStatus::Deleted => TrackedStatus {
|
|
worktree_status: StatusCode::Deleted,
|
|
index_status: StatusCode::Unmodified,
|
|
}
|
|
.into(),
|
|
_ => anyhow::bail!("Invalid code for simple status: {simple_status}"),
|
|
};
|
|
return Ok(result);
|
|
};
|
|
|
|
let result = match variant {
|
|
Variant::Untracked(_) => FileStatus::Untracked,
|
|
Variant::Ignored(_) => FileStatus::Ignored,
|
|
Variant::Unmerged(unmerged) => {
|
|
let [first_head, second_head] =
|
|
[unmerged.first_head, unmerged.second_head].map(|head| {
|
|
let code = proto::GitStatus::from_i32(head)
|
|
.with_context(|| format!("Invalid git status code: {head}"))?;
|
|
let result = match code {
|
|
proto::GitStatus::Added => UnmergedStatusCode::Added,
|
|
proto::GitStatus::Updated => UnmergedStatusCode::Updated,
|
|
proto::GitStatus::Deleted => UnmergedStatusCode::Deleted,
|
|
_ => anyhow::bail!("Invalid code for unmerged status: {code:?}"),
|
|
};
|
|
Ok(result)
|
|
});
|
|
let [first_head, second_head] = [first_head?, second_head?];
|
|
UnmergedStatus {
|
|
first_head,
|
|
second_head,
|
|
}
|
|
.into()
|
|
}
|
|
Variant::Tracked(tracked) => {
|
|
let [index_status, worktree_status] = [tracked.index_status, tracked.worktree_status]
|
|
.map(|status| {
|
|
let code = proto::GitStatus::from_i32(status)
|
|
.with_context(|| format!("Invalid git status code: {status}"))?;
|
|
let result = match code {
|
|
proto::GitStatus::Modified => StatusCode::Modified,
|
|
proto::GitStatus::TypeChanged => StatusCode::TypeChanged,
|
|
proto::GitStatus::Added => StatusCode::Added,
|
|
proto::GitStatus::Deleted => StatusCode::Deleted,
|
|
proto::GitStatus::Renamed => StatusCode::Renamed,
|
|
proto::GitStatus::Copied => StatusCode::Copied,
|
|
proto::GitStatus::Unmodified => StatusCode::Unmodified,
|
|
_ => anyhow::bail!("Invalid code for tracked status: {code:?}"),
|
|
};
|
|
Ok(result)
|
|
});
|
|
let [index_status, worktree_status] = [index_status?, worktree_status?];
|
|
TrackedStatus {
|
|
index_status,
|
|
worktree_status,
|
|
}
|
|
.into()
|
|
}
|
|
};
|
|
Ok(result)
|
|
}
|
|
|
|
fn status_to_proto(status: FileStatus) -> proto::GitFileStatus {
|
|
use proto::git_file_status::{Tracked, Unmerged, Variant};
|
|
|
|
let variant = match status {
|
|
FileStatus::Untracked => Variant::Untracked(Default::default()),
|
|
FileStatus::Ignored => Variant::Ignored(Default::default()),
|
|
FileStatus::Unmerged(UnmergedStatus {
|
|
first_head,
|
|
second_head,
|
|
}) => Variant::Unmerged(Unmerged {
|
|
first_head: unmerged_status_to_proto(first_head),
|
|
second_head: unmerged_status_to_proto(second_head),
|
|
}),
|
|
FileStatus::Tracked(TrackedStatus {
|
|
index_status,
|
|
worktree_status,
|
|
}) => Variant::Tracked(Tracked {
|
|
index_status: tracked_status_to_proto(index_status),
|
|
worktree_status: tracked_status_to_proto(worktree_status),
|
|
}),
|
|
};
|
|
proto::GitFileStatus {
|
|
variant: Some(variant),
|
|
}
|
|
}
|
|
|
|
fn unmerged_status_to_proto(code: UnmergedStatusCode) -> i32 {
|
|
match code {
|
|
UnmergedStatusCode::Added => proto::GitStatus::Added as _,
|
|
UnmergedStatusCode::Deleted => proto::GitStatus::Deleted as _,
|
|
UnmergedStatusCode::Updated => proto::GitStatus::Updated as _,
|
|
}
|
|
}
|
|
|
|
fn tracked_status_to_proto(code: StatusCode) -> i32 {
|
|
match code {
|
|
StatusCode::Added => proto::GitStatus::Added as _,
|
|
StatusCode::Deleted => proto::GitStatus::Deleted as _,
|
|
StatusCode::Modified => proto::GitStatus::Modified as _,
|
|
StatusCode::Renamed => proto::GitStatus::Renamed as _,
|
|
StatusCode::TypeChanged => proto::GitStatus::TypeChanged as _,
|
|
StatusCode::Copied => proto::GitStatus::Copied as _,
|
|
StatusCode::Unmodified => proto::GitStatus::Unmodified as _,
|
|
}
|
|
}
|