git: Git Panel UI, continued (#22960)

TODO:

- [ ] Investigate incorrect hit target for `stage all` button
- [ ] Add top level context menu
- [ ] Add entry context menus
- [x] Show paths in list view
- [ ] For now, `enter` can just open the file
- [ ] 🐞: Hover deadzone in list caused by scrollbar
- [x] 🐞: Incorrect status/nothing shown when multiple worktrees are
added

---

This PR continues work on the feature flagged git panel.

Changes:
- Defines and wires up git panel actions & keybindings
- Re-scopes some actions from `git_ui` -> `git`.
- General git actions (StageAll, CommitChanges, ...) are scoped to
`git`.
- Git panel specific actions (Close, FocusCommitEditor, ...) are scoped
to `git_panel.
- Staging actions & UI are now connected to git!
- Unify more reusable git status into the GitState global over being
tied to the panel directly.
- Uses the new git status codepaths instead of filtering all workspace
entries

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <53574922+cole-miller@users.noreply.github.com>
Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Nate Butler 2025-01-13 11:47:09 -05:00 committed by GitHub
parent 1c6dd03e50
commit 102e70816c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1006 additions and 840 deletions

View file

@ -1,6 +1,7 @@
use crate::status::GitStatusPair;
use crate::GitHostingProviderRegistry;
use crate::{blame::Blame, status::GitStatus};
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use collections::{HashMap, HashSet};
use git2::BranchType;
use gpui::SharedString;
@ -15,6 +16,7 @@ use std::{
sync::Arc,
};
use sum_tree::MapSeekTarget;
use util::command::new_std_command;
use util::ResultExt;
#[derive(Clone, Debug, Hash, PartialEq)]
@ -51,6 +53,8 @@ pub trait GitRepository: Send + Sync {
/// Returns the path to the repository, typically the `.git` folder.
fn dot_git_dir(&self) -> PathBuf;
fn update_index(&self, stage: &[RepoPath], unstage: &[RepoPath]) -> Result<()>;
}
impl std::fmt::Debug for dyn GitRepository {
@ -152,7 +156,7 @@ impl GitRepository for RealGitRepository {
Ok(_) => Ok(true),
Err(e) => match e.code() {
git2::ErrorCode::NotFound => Ok(false),
_ => Err(anyhow::anyhow!(e)),
_ => Err(anyhow!(e)),
},
}
}
@ -196,7 +200,7 @@ impl GitRepository for RealGitRepository {
repo.set_head(
revision
.name()
.ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
.ok_or_else(|| anyhow!("Branch name could not be retrieved"))?,
)?;
Ok(())
}
@ -228,6 +232,36 @@ impl GitRepository for RealGitRepository {
self.hosting_provider_registry.clone(),
)
}
fn update_index(&self, stage: &[RepoPath], unstage: &[RepoPath]) -> Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
if !stage.is_empty() {
let add = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["add", "--"])
.args(stage.iter().map(|p| p.as_ref()))
.status()?;
if !add.success() {
return Err(anyhow!("Failed to stage files: {add}"));
}
}
if !unstage.is_empty() {
let rm = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["restore", "--staged", "--"])
.args(unstage.iter().map(|p| p.as_ref()))
.status()?;
if !rm.success() {
return Err(anyhow!("Failed to unstage files: {rm}"));
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
@ -298,18 +332,24 @@ impl GitRepository for FakeGitRepository {
let mut entries = state
.worktree_statuses
.iter()
.filter_map(|(repo_path, status)| {
.filter_map(|(repo_path, status_worktree)| {
if path_prefixes
.iter()
.any(|path_prefix| repo_path.0.starts_with(path_prefix))
{
Some((repo_path.to_owned(), *status))
Some((
repo_path.to_owned(),
GitStatusPair {
index_status: None,
worktree_status: Some(*status_worktree),
},
))
} else {
None
}
})
.collect::<Vec<_>>();
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
Ok(GitStatus {
entries: entries.into(),
@ -363,6 +403,10 @@ impl GitRepository for FakeGitRepository {
.with_context(|| format!("failed to get blame for {:?}", path))
.cloned()
}
fn update_index(&self, _stage: &[RepoPath], _unstage: &[RepoPath]) -> Result<()> {
unimplemented!()
}
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
@ -398,6 +442,7 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
pub enum GitFileStatus {
Added,
Modified,
// TODO conflicts should be represented by the GitStatusPair
Conflict,
Deleted,
Untracked,
@ -426,6 +471,16 @@ impl GitFileStatus {
_ => None,
}
}
pub fn from_byte(byte: u8) -> Option<Self> {
match byte {
b'M' => Some(GitFileStatus::Modified),
b'A' => Some(GitFileStatus::Added),
b'D' => Some(GitFileStatus::Deleted),
b'?' => Some(GitFileStatus::Untracked),
_ => None,
}
}
}
pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
@ -453,6 +508,12 @@ impl RepoPath {
}
}
impl std::fmt::Display for RepoPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.to_string_lossy().fmt(f)
}
}
impl From<&Path> for RepoPath {
fn from(value: &Path) -> Self {
RepoPath::new(value.into())