git: Implement a basic repository selector (#23419)
This PR adds a rough-and-ready picker for selecting which of the project's repositories the git panel should display. Release Notes: - N/A --------- Co-authored-by: Nate Butler <iamnbutler@gmail.com> Co-authored-by: Nate <nate@zed.dev>
This commit is contained in:
parent
417760ade7
commit
31909bf334
13 changed files with 661 additions and 356 deletions
|
@ -1,25 +1,55 @@
|
|||
use anyhow::{anyhow, Context as _};
|
||||
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
|
||||
use crate::{Project, ProjectPath};
|
||||
use anyhow::anyhow;
|
||||
use futures::channel::mpsc;
|
||||
use futures::{SinkExt as _, StreamExt as _};
|
||||
use git::{
|
||||
repository::{GitRepository, RepoPath},
|
||||
status::{GitSummary, TrackedSummary},
|
||||
};
|
||||
use gpui::{AppContext, Context as _, Model};
|
||||
use gpui::{
|
||||
AppContext, Context as _, EventEmitter, Model, ModelContext, SharedString, Subscription,
|
||||
WeakModel,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use settings::WorktreeId;
|
||||
use std::sync::Arc;
|
||||
use text::Rope;
|
||||
use worktree::RepositoryEntry;
|
||||
use util::maybe;
|
||||
use worktree::{RepositoryEntry, StatusEntry};
|
||||
|
||||
pub struct GitState {
|
||||
pub commit_message: Model<Buffer>,
|
||||
|
||||
/// When a git repository is selected, this is used to track which repository's changes
|
||||
/// are currently being viewed or modified in the UI.
|
||||
pub active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
|
||||
|
||||
repositories: Vec<RepositoryHandle>,
|
||||
active_index: Option<usize>,
|
||||
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RepositoryHandle {
|
||||
git_state: WeakModel<GitState>,
|
||||
worktree_id: WorktreeId,
|
||||
repository_entry: RepositoryEntry,
|
||||
git_repo: Arc<dyn GitRepository>,
|
||||
commit_message: Model<Buffer>,
|
||||
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for RepositoryHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.worktree_id == other.worktree_id
|
||||
&& self.repository_entry.work_directory_id()
|
||||
== other.repository_entry.work_directory_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RepositoryHandle {}
|
||||
|
||||
impl PartialEq<RepositoryEntry> for RepositoryHandle {
|
||||
fn eq(&self, other: &RepositoryEntry) -> bool {
|
||||
self.repository_entry.work_directory_id() == other.work_directory_id()
|
||||
}
|
||||
}
|
||||
|
||||
enum Message {
|
||||
|
@ -29,11 +59,21 @@ enum Message {
|
|||
Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
RepositoriesUpdated,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for GitState {}
|
||||
|
||||
impl GitState {
|
||||
pub fn new(languages: Arc<LanguageRegistry>, cx: &mut AppContext) -> Self {
|
||||
pub fn new(
|
||||
worktree_store: &Model<WorktreeStore>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut ModelContext<'_, Self>,
|
||||
) -> Self {
|
||||
let (update_sender, mut update_receiver) =
|
||||
mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
|
||||
cx.spawn(|cx| async move {
|
||||
cx.spawn(|_, cx| async move {
|
||||
while let Some((msg, mut err_sender)) = update_receiver.next().await {
|
||||
let result = cx
|
||||
.background_executor()
|
||||
|
@ -57,39 +97,147 @@ impl GitState {
|
|||
})
|
||||
.detach();
|
||||
|
||||
let commit_message = cx.new_model(|cx| Buffer::local("", cx));
|
||||
let markdown = languages.language_for_name("Markdown");
|
||||
cx.spawn({
|
||||
let commit_message = commit_message.clone();
|
||||
|mut cx| async move {
|
||||
let markdown = markdown.await.context("failed to load Markdown language")?;
|
||||
commit_message.update(&mut cx, |commit_message, cx| {
|
||||
commit_message.set_language(Some(markdown), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
|
||||
|
||||
GitState {
|
||||
commit_message,
|
||||
active_repository: None,
|
||||
languages,
|
||||
repositories: vec![],
|
||||
active_index: None,
|
||||
update_sender,
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate_repository(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
active_repository: RepositoryEntry,
|
||||
git_repo: Arc<dyn GitRepository>,
|
||||
) {
|
||||
self.active_repository = Some((worktree_id, active_repository, git_repo));
|
||||
pub fn active_repository(&self) -> Option<RepositoryHandle> {
|
||||
self.active_index
|
||||
.map(|index| self.repositories[index].clone())
|
||||
}
|
||||
|
||||
pub fn active_repository(
|
||||
&self,
|
||||
) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
|
||||
self.active_repository.as_ref()
|
||||
fn on_worktree_store_event(
|
||||
&mut self,
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
_event: &WorktreeStoreEvent,
|
||||
cx: &mut ModelContext<'_, Self>,
|
||||
) {
|
||||
// TODO inspect the event
|
||||
|
||||
let mut new_repositories = Vec::new();
|
||||
let mut new_active_index = None;
|
||||
let this = cx.weak_model();
|
||||
|
||||
worktree_store.update(cx, |worktree_store, cx| {
|
||||
for worktree in worktree_store.worktrees() {
|
||||
worktree.update(cx, |worktree, cx| {
|
||||
let snapshot = worktree.snapshot();
|
||||
let Some(local) = worktree.as_local() else {
|
||||
return;
|
||||
};
|
||||
for repo in snapshot.repositories().iter() {
|
||||
let Some(local_repo) = local.get_local_repo(repo) else {
|
||||
continue;
|
||||
};
|
||||
let existing = self
|
||||
.repositories
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, existing_handle)| existing_handle == &repo);
|
||||
let handle = if let Some((index, handle)) = existing {
|
||||
if self.active_index == Some(index) {
|
||||
new_active_index = Some(new_repositories.len());
|
||||
}
|
||||
// Update the statuses but keep everything else.
|
||||
let mut existing_handle = handle.clone();
|
||||
existing_handle.repository_entry = repo.clone();
|
||||
existing_handle
|
||||
} else {
|
||||
let commit_message = cx.new_model(|cx| Buffer::local("", cx));
|
||||
cx.spawn({
|
||||
let commit_message = commit_message.downgrade();
|
||||
let languages = self.languages.clone();
|
||||
|_, mut cx| async move {
|
||||
let markdown = languages.language_for_name("Markdown").await?;
|
||||
commit_message.update(&mut cx, |commit_message, cx| {
|
||||
commit_message.set_language(Some(markdown), cx);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
RepositoryHandle {
|
||||
git_state: this.clone(),
|
||||
worktree_id: worktree.id(),
|
||||
repository_entry: repo.clone(),
|
||||
git_repo: local_repo.repo().clone(),
|
||||
commit_message,
|
||||
update_sender: self.update_sender.clone(),
|
||||
}
|
||||
};
|
||||
new_repositories.push(handle);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if new_active_index == None && new_repositories.len() > 0 {
|
||||
new_active_index = Some(0);
|
||||
}
|
||||
|
||||
self.repositories = new_repositories;
|
||||
self.active_index = new_active_index;
|
||||
|
||||
cx.emit(Event::RepositoriesUpdated);
|
||||
}
|
||||
|
||||
pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
|
||||
self.repositories.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl RepositoryHandle {
|
||||
pub fn display_name(&self, project: &Project, cx: &AppContext) -> SharedString {
|
||||
maybe!({
|
||||
let path = self.unrelativize(&"".into())?;
|
||||
Some(
|
||||
project
|
||||
.absolute_path(&path, cx)?
|
||||
.file_name()?
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
.unwrap_or("".into())
|
||||
}
|
||||
|
||||
pub fn activate(&self, cx: &mut AppContext) {
|
||||
let Some(git_state) = self.git_state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
git_state.update(cx, |git_state, cx| {
|
||||
let Some((index, _)) = git_state
|
||||
.repositories
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, handle)| handle == &self)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
git_state.active_index = Some(index);
|
||||
cx.emit(Event::RepositoriesUpdated);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
|
||||
self.repository_entry.status()
|
||||
}
|
||||
|
||||
pub fn unrelativize(&self, path: &RepoPath) -> Option<ProjectPath> {
|
||||
let path = self.repository_entry.unrelativize(path)?;
|
||||
Some((self.worktree_id, path).into())
|
||||
}
|
||||
|
||||
pub fn commit_message(&self) -> Model<Buffer> {
|
||||
self.commit_message.clone()
|
||||
}
|
||||
|
||||
pub fn stage_entries(
|
||||
|
@ -100,11 +248,8 @@ impl GitState {
|
|||
if entries.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
|
||||
return Err(anyhow!("No active repository"));
|
||||
};
|
||||
self.update_sender
|
||||
.unbounded_send((Message::Stage(git_repo.clone(), entries), err_sender))
|
||||
.unbounded_send((Message::Stage(self.git_repo.clone(), entries), err_sender))
|
||||
.map_err(|_| anyhow!("Failed to submit stage operation"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -117,20 +262,15 @@ impl GitState {
|
|||
if entries.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
|
||||
return Err(anyhow!("No active repository"));
|
||||
};
|
||||
self.update_sender
|
||||
.unbounded_send((Message::Unstage(git_repo.clone(), entries), err_sender))
|
||||
.unbounded_send((Message::Unstage(self.git_repo.clone(), entries), err_sender))
|
||||
.map_err(|_| anyhow!("Failed to submit unstage operation"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
|
||||
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
||||
return Err(anyhow!("No active repository"));
|
||||
};
|
||||
let to_stage = entry
|
||||
let to_stage = self
|
||||
.repository_entry
|
||||
.status()
|
||||
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
|
||||
.map(|entry| entry.repo_path.clone())
|
||||
|
@ -140,10 +280,8 @@ impl GitState {
|
|||
}
|
||||
|
||||
pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
|
||||
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
||||
return Err(anyhow!("No active repository"));
|
||||
};
|
||||
let to_unstage = entry
|
||||
let to_unstage = self
|
||||
.repository_entry
|
||||
.status()
|
||||
.filter(|entry| entry.status.is_staged().unwrap_or(true))
|
||||
.map(|entry| entry.repo_path.clone())
|
||||
|
@ -155,23 +293,15 @@ impl GitState {
|
|||
/// Get a count of all entries in the active repository, including
|
||||
/// untracked files.
|
||||
pub fn entry_count(&self) -> usize {
|
||||
self.active_repository
|
||||
.as_ref()
|
||||
.map_or(0, |(_, entry, _)| entry.status_len())
|
||||
self.repository_entry.status_len()
|
||||
}
|
||||
|
||||
fn have_changes(&self) -> bool {
|
||||
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
entry.status_summary() != GitSummary::UNCHANGED
|
||||
self.repository_entry.status_summary() != GitSummary::UNCHANGED
|
||||
}
|
||||
|
||||
fn have_staged_changes(&self) -> bool {
|
||||
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
entry.status_summary().index != TrackedSummary::UNCHANGED
|
||||
self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
|
||||
}
|
||||
|
||||
pub fn can_commit(&self, commit_all: bool, cx: &AppContext) -> bool {
|
||||
|
@ -185,36 +315,33 @@ impl GitState {
|
|||
}
|
||||
|
||||
pub fn commit(
|
||||
&mut self,
|
||||
&self,
|
||||
err_sender: mpsc::Sender<anyhow::Error>,
|
||||
cx: &AppContext,
|
||||
cx: &mut AppContext,
|
||||
) -> anyhow::Result<()> {
|
||||
if !self.can_commit(false, cx) {
|
||||
return Err(anyhow!("Unable to commit"));
|
||||
}
|
||||
let Some((_, _, git_repo)) = self.active_repository() else {
|
||||
return Err(anyhow!("No active repository"));
|
||||
};
|
||||
let git_repo = git_repo.clone();
|
||||
let message = self.commit_message.read(cx).as_rope().clone();
|
||||
self.update_sender
|
||||
.unbounded_send((Message::Commit(git_repo, message), err_sender))
|
||||
.unbounded_send((Message::Commit(self.git_repo.clone(), message), err_sender))
|
||||
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
|
||||
self.commit_message.update(cx, |commit_message, cx| {
|
||||
commit_message.set_text("", cx);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn commit_all(
|
||||
&mut self,
|
||||
&self,
|
||||
err_sender: mpsc::Sender<anyhow::Error>,
|
||||
cx: &AppContext,
|
||||
cx: &mut AppContext,
|
||||
) -> anyhow::Result<()> {
|
||||
if !self.can_commit(true, cx) {
|
||||
return Err(anyhow!("Unable to commit"));
|
||||
}
|
||||
let Some((_, entry, git_repo)) = self.active_repository.as_ref() else {
|
||||
return Err(anyhow!("No active repository"));
|
||||
};
|
||||
let to_stage = entry
|
||||
let to_stage = self
|
||||
.repository_entry
|
||||
.status()
|
||||
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
|
||||
.map(|entry| entry.repo_path.clone())
|
||||
|
@ -222,10 +349,13 @@ impl GitState {
|
|||
let message = self.commit_message.read(cx).as_rope().clone();
|
||||
self.update_sender
|
||||
.unbounded_send((
|
||||
Message::StageAndCommit(git_repo.clone(), message, to_stage),
|
||||
Message::StageAndCommit(self.git_repo.clone(), message, to_stage),
|
||||
err_sender,
|
||||
))
|
||||
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
|
||||
self.commit_message.update(cx, |commit_message, cx| {
|
||||
commit_message.set_text("", cx);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue