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:
parent
1c6dd03e50
commit
102e70816c
13 changed files with 1006 additions and 840 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5113,6 +5113,7 @@ dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
|
"futures 0.3.31",
|
||||||
"git",
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
|
@ -5123,6 +5124,7 @@ dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
|
"sum_tree",
|
||||||
"theme",
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
|
|
|
@ -682,6 +682,38 @@
|
||||||
"space": "project_panel::Open"
|
"space": "project_panel::Open"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "GitPanel && !CommitEditor",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"escape": "git_panel::Close"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "GitPanel && ChangesList",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"up": "menu::SelectPrev",
|
||||||
|
"down": "menu::SelectNext",
|
||||||
|
"cmd-up": "menu::SelectFirst",
|
||||||
|
"cmd-down": "menu::SelectLast",
|
||||||
|
"enter": "menu::Confirm",
|
||||||
|
"space": "git::ToggleStaged",
|
||||||
|
"cmd-shift-space": "git::StageAll",
|
||||||
|
"ctrl-shift-space": "git::UnstageAll",
|
||||||
|
"alt-down": "git_panel::FocusEditor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "GitPanel && CommitEditor > Editor",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"alt-up": "git_panel::FocusChanges",
|
||||||
|
"escape": "git_panel::FocusChanges",
|
||||||
|
"cmd-enter": "git::CommitChanges",
|
||||||
|
"cmd-alt-enter": "git::CommitAllChanges"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "CollabPanel && not_editing",
|
"context": "CollabPanel && not_editing",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|
|
@ -1221,7 +1221,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||||
id,
|
id,
|
||||||
guest_project.remote_id(),
|
guest_project.remote_id(),
|
||||||
);
|
);
|
||||||
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
|
assert_eq!(guest_snapshot.repositories().iter().collect::<Vec<_>>(), host_snapshot.repositories().iter().collect::<Vec<_>>(),
|
||||||
"{} has different repositories than the host for worktree {:?} and project {:?}",
|
"{} has different repositories than the host for worktree {:?} and project {:?}",
|
||||||
client.username,
|
client.username,
|
||||||
host_snapshot.abs_path(),
|
host_snapshot.abs_path(),
|
||||||
|
|
|
@ -197,9 +197,10 @@ impl ProjectDiffEditor {
|
||||||
let snapshot = worktree.read(cx).snapshot();
|
let snapshot = worktree.read(cx).snapshot();
|
||||||
let applicable_entries = snapshot
|
let applicable_entries = snapshot
|
||||||
.repositories()
|
.repositories()
|
||||||
|
.iter()
|
||||||
.flat_map(|entry| {
|
.flat_map(|entry| {
|
||||||
entry.status().map(|git_entry| {
|
entry.status().map(|git_entry| {
|
||||||
(git_entry.status, entry.join(git_entry.repo_path))
|
(git_entry.combined_status(), entry.join(git_entry.repo_path))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.filter_map(|(status, path)| {
|
.filter_map(|(status, path)| {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
use crate::status::GitStatusPair;
|
||||||
use crate::GitHostingProviderRegistry;
|
use crate::GitHostingProviderRegistry;
|
||||||
use crate::{blame::Blame, status::GitStatus};
|
use crate::{blame::Blame, status::GitStatus};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use git2::BranchType;
|
use git2::BranchType;
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
|
@ -15,6 +16,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use sum_tree::MapSeekTarget;
|
use sum_tree::MapSeekTarget;
|
||||||
|
use util::command::new_std_command;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash, PartialEq)]
|
#[derive(Clone, Debug, Hash, PartialEq)]
|
||||||
|
@ -51,6 +53,8 @@ pub trait GitRepository: Send + Sync {
|
||||||
|
|
||||||
/// Returns the path to the repository, typically the `.git` folder.
|
/// Returns the path to the repository, typically the `.git` folder.
|
||||||
fn dot_git_dir(&self) -> PathBuf;
|
fn dot_git_dir(&self) -> PathBuf;
|
||||||
|
|
||||||
|
fn update_index(&self, stage: &[RepoPath], unstage: &[RepoPath]) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for dyn GitRepository {
|
impl std::fmt::Debug for dyn GitRepository {
|
||||||
|
@ -152,7 +156,7 @@ impl GitRepository for RealGitRepository {
|
||||||
Ok(_) => Ok(true),
|
Ok(_) => Ok(true),
|
||||||
Err(e) => match e.code() {
|
Err(e) => match e.code() {
|
||||||
git2::ErrorCode::NotFound => Ok(false),
|
git2::ErrorCode::NotFound => Ok(false),
|
||||||
_ => Err(anyhow::anyhow!(e)),
|
_ => Err(anyhow!(e)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,7 +200,7 @@ impl GitRepository for RealGitRepository {
|
||||||
repo.set_head(
|
repo.set_head(
|
||||||
revision
|
revision
|
||||||
.name()
|
.name()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
|
.ok_or_else(|| anyhow!("Branch name could not be retrieved"))?,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -228,6 +232,36 @@ impl GitRepository for RealGitRepository {
|
||||||
self.hosting_provider_registry.clone(),
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -298,18 +332,24 @@ impl GitRepository for FakeGitRepository {
|
||||||
let mut entries = state
|
let mut entries = state
|
||||||
.worktree_statuses
|
.worktree_statuses
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(repo_path, status)| {
|
.filter_map(|(repo_path, status_worktree)| {
|
||||||
if path_prefixes
|
if path_prefixes
|
||||||
.iter()
|
.iter()
|
||||||
.any(|path_prefix| repo_path.0.starts_with(path_prefix))
|
.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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
|
||||||
|
|
||||||
Ok(GitStatus {
|
Ok(GitStatus {
|
||||||
entries: entries.into(),
|
entries: entries.into(),
|
||||||
|
@ -363,6 +403,10 @@ impl GitRepository for FakeGitRepository {
|
||||||
.with_context(|| format!("failed to get blame for {:?}", path))
|
.with_context(|| format!("failed to get blame for {:?}", path))
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_index(&self, _stage: &[RepoPath], _unstage: &[RepoPath]) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
|
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 {
|
pub enum GitFileStatus {
|
||||||
Added,
|
Added,
|
||||||
Modified,
|
Modified,
|
||||||
|
// TODO conflicts should be represented by the GitStatusPair
|
||||||
Conflict,
|
Conflict,
|
||||||
Deleted,
|
Deleted,
|
||||||
Untracked,
|
Untracked,
|
||||||
|
@ -426,6 +471,16 @@ impl GitFileStatus {
|
||||||
_ => None,
|
_ => 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> =
|
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 {
|
impl From<&Path> for RepoPath {
|
||||||
fn from(value: &Path) -> Self {
|
fn from(value: &Path) -> Self {
|
||||||
RepoPath::new(value.into())
|
RepoPath::new(value.into())
|
||||||
|
|
|
@ -2,9 +2,33 @@ use crate::repository::{GitFileStatus, RepoPath};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use std::{path::Path, process::Stdio, sync::Arc};
|
use std::{path::Path, process::Stdio, sync::Arc};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct GitStatusPair {
|
||||||
|
// Not both `None`.
|
||||||
|
pub index_status: Option<GitFileStatus>,
|
||||||
|
pub worktree_status: Option<GitFileStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitStatusPair {
|
||||||
|
pub fn is_staged(&self) -> Option<bool> {
|
||||||
|
match (self.index_status, self.worktree_status) {
|
||||||
|
(Some(_), None) => Some(true),
|
||||||
|
(None, Some(_)) => Some(false),
|
||||||
|
(Some(GitFileStatus::Untracked), Some(GitFileStatus::Untracked)) => Some(false),
|
||||||
|
(Some(_), Some(_)) => None,
|
||||||
|
(None, None) => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO reconsider uses of this
|
||||||
|
pub fn combined(&self) -> GitFileStatus {
|
||||||
|
self.index_status.or(self.worktree_status).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GitStatus {
|
pub struct GitStatus {
|
||||||
pub entries: Arc<[(RepoPath, GitFileStatus)]>,
|
pub entries: Arc<[(RepoPath, GitStatusPair)]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitStatus {
|
impl GitStatus {
|
||||||
|
@ -20,6 +44,7 @@ impl GitStatus {
|
||||||
"status",
|
"status",
|
||||||
"--porcelain=v1",
|
"--porcelain=v1",
|
||||||
"--untracked-files=all",
|
"--untracked-files=all",
|
||||||
|
"--no-renames",
|
||||||
"-z",
|
"-z",
|
||||||
])
|
])
|
||||||
.args(path_prefixes.iter().map(|path_prefix| {
|
.args(path_prefixes.iter().map(|path_prefix| {
|
||||||
|
@ -47,36 +72,32 @@ impl GitStatus {
|
||||||
let mut entries = stdout
|
let mut entries = stdout
|
||||||
.split('\0')
|
.split('\0')
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
if entry.is_char_boundary(3) {
|
let sep = entry.get(2..3)?;
|
||||||
let (status, path) = entry.split_at(3);
|
if sep != " " {
|
||||||
let status = status.trim();
|
return None;
|
||||||
Some((
|
};
|
||||||
RepoPath(Path::new(path).into()),
|
let path = &entry[3..];
|
||||||
match status {
|
let status = entry[0..2].as_bytes();
|
||||||
"A" => GitFileStatus::Added,
|
let index_status = GitFileStatus::from_byte(status[0]);
|
||||||
"M" => GitFileStatus::Modified,
|
let worktree_status = GitFileStatus::from_byte(status[1]);
|
||||||
"D" => GitFileStatus::Deleted,
|
if (index_status, worktree_status) == (None, None) {
|
||||||
"??" => GitFileStatus::Untracked,
|
return None;
|
||||||
_ => return None,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
let path = RepoPath(Path::new(path).into());
|
||||||
|
Some((
|
||||||
|
path,
|
||||||
|
GitStatusPair {
|
||||||
|
index_status,
|
||||||
|
worktree_status,
|
||||||
|
},
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
entries: entries.into(),
|
entries: entries.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, path: &Path) -> Option<GitFileStatus> {
|
|
||||||
self.entries
|
|
||||||
.binary_search_by(|(repo_path, _)| repo_path.0.as_ref().cmp(path))
|
|
||||||
.ok()
|
|
||||||
.map(|index| self.entries[index].1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GitStatus {
|
impl Default for GitStatus {
|
||||||
|
|
|
@ -17,6 +17,7 @@ anyhow.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
@ -27,6 +28,7 @@ serde.workspace = true
|
||||||
serde_derive.workspace = true
|
serde_derive.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
|
sum_tree.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
### General
|
|
||||||
|
|
||||||
- [x] Disable staging and committing actions for read-only projects
|
|
||||||
|
|
||||||
### List
|
|
||||||
|
|
||||||
- [x] Add uniform list
|
|
||||||
- [x] Git status item
|
|
||||||
- [ ] Directory item
|
|
||||||
- [x] Scrollbar
|
|
||||||
- [ ] Add indent size setting
|
|
||||||
- [ ] Add tree settings
|
|
||||||
|
|
||||||
### List Items
|
|
||||||
|
|
||||||
- [x] Checkbox for staging
|
|
||||||
- [x] Git status icon
|
|
||||||
- [ ] Context menu
|
|
||||||
- [ ] Discard Changes
|
|
||||||
- ---
|
|
||||||
- [ ] Ignore
|
|
||||||
- [ ] Ignore directory
|
|
||||||
- ---
|
|
||||||
- [ ] Copy path
|
|
||||||
- [ ] Copy relative path
|
|
||||||
- ---
|
|
||||||
- [ ] Reveal in Finder
|
|
||||||
|
|
||||||
### Commit Editor
|
|
||||||
|
|
||||||
- [ ] Add commit editor
|
|
||||||
- [ ] Add commit message placeholder & add commit message to store
|
|
||||||
- [ ] Add a way to get the current collaborators & automatically add them to the commit message as co-authors
|
|
||||||
- [ ] Add action to clear commit message
|
|
||||||
- [x] Swap commit button between "Commit" and "Commit All" based on modifier key
|
|
||||||
|
|
||||||
### Component Updates
|
|
||||||
|
|
||||||
- [ ] ChangedLineCount (new)
|
|
||||||
- takes `lines_added: usize, lines_removed: usize`, returns a added/removed badge
|
|
||||||
- [x] GitStatusIcon (new)
|
|
||||||
- [ ] Checkbox
|
|
||||||
- update checkbox design
|
|
||||||
- [ ] ScrollIndicator
|
|
||||||
- shows a gradient overlay when more content is available to be scrolled
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,56 +1,282 @@
|
||||||
use ::settings::Settings;
|
use ::settings::Settings;
|
||||||
use git::repository::GitFileStatus;
|
use collections::HashMap;
|
||||||
use gpui::{actions, AppContext, Context, Global, Hsla, Model};
|
use futures::{future::FusedFuture, select, FutureExt};
|
||||||
|
use git::repository::{GitFileStatus, GitRepository, RepoPath};
|
||||||
|
use gpui::{actions, AppContext, Context, Global, Hsla, Model, ModelContext};
|
||||||
|
use project::{Project, WorktreeId};
|
||||||
use settings::GitPanelSettings;
|
use settings::GitPanelSettings;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::{
|
||||||
|
pin::{pin, Pin},
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use sum_tree::SumTree;
|
||||||
use ui::{Color, Icon, IconName, IntoElement, SharedString};
|
use ui::{Color, Icon, IconName, IntoElement, SharedString};
|
||||||
|
use worktree::RepositoryEntry;
|
||||||
|
|
||||||
pub mod git_panel;
|
pub mod git_panel;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
|
||||||
|
const GIT_TASK_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
git_ui,
|
git,
|
||||||
[
|
[
|
||||||
|
StageFile,
|
||||||
|
UnstageFile,
|
||||||
|
ToggleStaged,
|
||||||
|
// Revert actions are currently in the editor crate:
|
||||||
|
// editor::RevertFile,
|
||||||
|
// editor::RevertSelectedHunks
|
||||||
StageAll,
|
StageAll,
|
||||||
UnstageAll,
|
UnstageAll,
|
||||||
RevertAll,
|
RevertAll,
|
||||||
CommitStagedChanges,
|
CommitChanges,
|
||||||
CommitAllChanges,
|
CommitAllChanges,
|
||||||
ClearMessage
|
ClearCommitMessage
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
GitPanelSettings::register(cx);
|
GitPanelSettings::register(cx);
|
||||||
let git_state = cx.new_model(|_cx| GitState::new());
|
let git_state = cx.new_model(GitState::new);
|
||||||
cx.set_global(GlobalGitState(git_state));
|
cx.set_global(GlobalGitState(git_state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum GitViewMode {
|
||||||
|
#[default]
|
||||||
|
List,
|
||||||
|
Tree,
|
||||||
|
}
|
||||||
|
|
||||||
struct GlobalGitState(Model<GitState>);
|
struct GlobalGitState(Model<GitState>);
|
||||||
|
|
||||||
impl Global for GlobalGitState {}
|
impl Global for GlobalGitState {}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum StatusAction {
|
||||||
|
Stage,
|
||||||
|
Unstage,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct GitState {
|
pub struct GitState {
|
||||||
|
/// The current commit message being composed.
|
||||||
commit_message: Option<SharedString>,
|
commit_message: Option<SharedString>,
|
||||||
|
|
||||||
|
/// When a git repository is selected, this is used to track which repository's changes
|
||||||
|
/// are currently being viewed or modified in the UI.
|
||||||
|
active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
|
||||||
|
|
||||||
|
updater_tx: mpsc::Sender<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>,
|
||||||
|
|
||||||
|
all_repositories: HashMap<WorktreeId, SumTree<RepositoryEntry>>,
|
||||||
|
|
||||||
|
list_view_mode: GitViewMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitState {
|
impl GitState {
|
||||||
pub fn new() -> Self {
|
pub fn new(cx: &mut ModelContext<'_, Self>) -> Self {
|
||||||
|
let (updater_tx, updater_rx) = mpsc::channel();
|
||||||
|
cx.spawn(|_, cx| async move {
|
||||||
|
// Long-running task to periodically update git indices based on messages from the panel.
|
||||||
|
|
||||||
|
// We read messages from the channel in batches that refer to the same repository.
|
||||||
|
// When we read a message whose repository is different from the current batch's repository,
|
||||||
|
// the batch is finished, and since we can't un-receive this last message, we save it
|
||||||
|
// to begin the next batch.
|
||||||
|
let mut leftover_message: Option<(
|
||||||
|
Arc<dyn GitRepository>,
|
||||||
|
Vec<RepoPath>,
|
||||||
|
StatusAction,
|
||||||
|
)> = None;
|
||||||
|
let mut git_task = None;
|
||||||
|
loop {
|
||||||
|
let mut timer = cx.background_executor().timer(GIT_TASK_DEBOUNCE).fuse();
|
||||||
|
let _result = {
|
||||||
|
let mut task: Pin<&mut dyn FusedFuture<Output = anyhow::Result<()>>> =
|
||||||
|
match git_task.as_mut() {
|
||||||
|
Some(task) => pin!(task),
|
||||||
|
// If no git task is running, just wait for the timeout.
|
||||||
|
None => pin!(std::future::pending().fuse()),
|
||||||
|
};
|
||||||
|
select! {
|
||||||
|
result = task => {
|
||||||
|
// Task finished.
|
||||||
|
git_task = None;
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
_ = timer => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO handle failure of the git command
|
||||||
|
|
||||||
|
if git_task.is_none() {
|
||||||
|
// No git task running now; let's see if we should launch a new one.
|
||||||
|
let mut to_stage = Vec::new();
|
||||||
|
let mut to_unstage = Vec::new();
|
||||||
|
let mut current_repo = leftover_message.as_ref().map(|msg| msg.0.clone());
|
||||||
|
for (git_repo, paths, action) in leftover_message
|
||||||
|
.take()
|
||||||
|
.into_iter()
|
||||||
|
.chain(updater_rx.try_iter())
|
||||||
|
{
|
||||||
|
if current_repo
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |repo| !Arc::ptr_eq(repo, &git_repo))
|
||||||
|
{
|
||||||
|
// End of a batch, save this for the next one.
|
||||||
|
leftover_message = Some((git_repo.clone(), paths, action));
|
||||||
|
break;
|
||||||
|
} else if current_repo.is_none() {
|
||||||
|
// Start of a batch.
|
||||||
|
current_repo = Some(git_repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == StatusAction::Stage {
|
||||||
|
to_stage.extend(paths);
|
||||||
|
} else {
|
||||||
|
to_unstage.extend(paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO handle the same path being staged and unstaged
|
||||||
|
|
||||||
|
if to_stage.is_empty() && to_unstage.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(git_repo) = current_repo {
|
||||||
|
git_task = Some(
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move { git_repo.update_index(&to_stage, &to_unstage) })
|
||||||
|
.fuse(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
GitState {
|
GitState {
|
||||||
commit_message: None,
|
commit_message: None,
|
||||||
|
active_repository: None,
|
||||||
|
updater_tx,
|
||||||
|
list_view_mode: GitViewMode::default(),
|
||||||
|
all_repositories: HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_message(&mut self, message: Option<SharedString>) {
|
|
||||||
self.commit_message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_message(&mut self) {
|
|
||||||
self.commit_message = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
|
pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
|
||||||
cx.global::<GlobalGitState>().0.clone()
|
cx.global::<GlobalGitState>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
|
||||||
|
self.active_repository.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit_message(&mut self, message: Option<SharedString>) {
|
||||||
|
self.commit_message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_commit_message(&mut self) {
|
||||||
|
self.commit_message = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stage_entry(&mut self, repo_path: RepoPath) {
|
||||||
|
if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
|
||||||
|
let _ = self
|
||||||
|
.updater_tx
|
||||||
|
.send((git_repo.clone(), vec![repo_path], StatusAction::Stage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unstage_entry(&mut self, repo_path: RepoPath) {
|
||||||
|
if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
|
||||||
|
let _ =
|
||||||
|
self.updater_tx
|
||||||
|
.send((git_repo.clone(), vec![repo_path], StatusAction::Unstage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stage_entries(&mut self, entries: Vec<RepoPath>) {
|
||||||
|
if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
|
||||||
|
let _ = self
|
||||||
|
.updater_tx
|
||||||
|
.send((git_repo.clone(), entries, StatusAction::Stage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn act_on_all(&mut self, action: StatusAction) {
|
||||||
|
if let Some((_, active_repository, git_repo)) = self.active_repository.as_ref() {
|
||||||
|
let _ = self.updater_tx.send((
|
||||||
|
git_repo.clone(),
|
||||||
|
active_repository
|
||||||
|
.status()
|
||||||
|
.map(|entry| entry.repo_path)
|
||||||
|
.collect(),
|
||||||
|
action,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stage_all(&mut self) {
|
||||||
|
self.act_on_all(StatusAction::Stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unstage_all(&mut self) {
|
||||||
|
self.act_on_all(StatusAction::Unstage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_worktree_repository(
|
||||||
|
project: &Model<Project>,
|
||||||
|
worktree_id: WorktreeId,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Option<(RepositoryEntry, Arc<dyn GitRepository>)> {
|
||||||
|
project
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(worktree_id, cx)
|
||||||
|
.and_then(|worktree| {
|
||||||
|
let snapshot = worktree.read(cx).snapshot();
|
||||||
|
let repo = snapshot.repositories().iter().next()?.clone();
|
||||||
|
let git_repo = worktree
|
||||||
|
.read(cx)
|
||||||
|
.as_local()?
|
||||||
|
.get_local_repo(&repo)?
|
||||||
|
.repo()
|
||||||
|
.clone();
|
||||||
|
Some((repo, git_repo))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_repository_in_project(
|
||||||
|
project: &Model<Project>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
|
||||||
|
project.read(cx).worktrees(cx).next().and_then(|worktree| {
|
||||||
|
let snapshot = worktree.read(cx).snapshot();
|
||||||
|
let repo = snapshot.repositories().iter().next()?.clone();
|
||||||
|
let git_repo = worktree
|
||||||
|
.read(cx)
|
||||||
|
.as_local()?
|
||||||
|
.get_local_repo(&repo)?
|
||||||
|
.repo()
|
||||||
|
.clone();
|
||||||
|
Some((snapshot.id(), repo, git_repo))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADDED_COLOR: Hsla = Hsla {
|
const ADDED_COLOR: Hsla = Hsla {
|
||||||
|
|
|
@ -487,6 +487,7 @@ impl RenderOnce for ButtonLike {
|
||||||
self.base
|
self.base
|
||||||
.h_flex()
|
.h_flex()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
|
.font_ui(cx)
|
||||||
.group("")
|
.group("")
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.h(self.height.unwrap_or(self.size.rems().into()))
|
.h(self.height.unwrap_or(self.size.rems().into()))
|
||||||
|
|
|
@ -21,6 +21,7 @@ use fuzzy::CharBag;
|
||||||
use git::GitHostingProviderRegistry;
|
use git::GitHostingProviderRegistry;
|
||||||
use git::{
|
use git::{
|
||||||
repository::{GitFileStatus, GitRepository, RepoPath},
|
repository::{GitFileStatus, GitRepository, RepoPath},
|
||||||
|
status::GitStatusPair,
|
||||||
COOKIES, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE,
|
COOKIES, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -193,8 +194,8 @@ pub struct RepositoryEntry {
|
||||||
/// - my_sub_folder_1/project_root/changed_file_1
|
/// - my_sub_folder_1/project_root/changed_file_1
|
||||||
/// - my_sub_folder_2/changed_file_2
|
/// - my_sub_folder_2/changed_file_2
|
||||||
pub(crate) statuses_by_path: SumTree<StatusEntry>,
|
pub(crate) statuses_by_path: SumTree<StatusEntry>,
|
||||||
pub(crate) work_directory_id: ProjectEntryId,
|
pub work_directory_id: ProjectEntryId,
|
||||||
pub(crate) work_directory: WorkDirectory,
|
pub work_directory: WorkDirectory,
|
||||||
pub(crate) branch: Option<Arc<str>>,
|
pub(crate) branch: Option<Arc<str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,6 +226,12 @@ impl RepositoryEntry {
|
||||||
self.statuses_by_path.iter().cloned()
|
self.statuses_by_path.iter().cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
|
||||||
|
self.statuses_by_path
|
||||||
|
.get(&PathKey(path.0.clone()), &())
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn initial_update(&self) -> proto::RepositoryEntry {
|
pub fn initial_update(&self) -> proto::RepositoryEntry {
|
||||||
proto::RepositoryEntry {
|
proto::RepositoryEntry {
|
||||||
work_directory_id: self.work_directory_id.to_proto(),
|
work_directory_id: self.work_directory_id.to_proto(),
|
||||||
|
@ -234,7 +241,7 @@ impl RepositoryEntry {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| proto::StatusEntry {
|
.map(|entry| proto::StatusEntry {
|
||||||
repo_path: entry.repo_path.to_string_lossy().to_string(),
|
repo_path: entry.repo_path.to_string_lossy().to_string(),
|
||||||
status: git_status_to_proto(entry.status),
|
status: status_pair_to_proto(entry.status.clone()),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
removed_statuses: Default::default(),
|
removed_statuses: Default::default(),
|
||||||
|
@ -259,7 +266,7 @@ impl RepositoryEntry {
|
||||||
current_new_entry = new_statuses.next();
|
current_new_entry = new_statuses.next();
|
||||||
}
|
}
|
||||||
Ordering::Equal => {
|
Ordering::Equal => {
|
||||||
if new_entry.status != old_entry.status {
|
if new_entry.combined_status() != old_entry.combined_status() {
|
||||||
updated_statuses.push(new_entry.to_proto());
|
updated_statuses.push(new_entry.to_proto());
|
||||||
}
|
}
|
||||||
current_old_entry = old_statuses.next();
|
current_old_entry = old_statuses.next();
|
||||||
|
@ -2360,7 +2367,7 @@ impl Snapshot {
|
||||||
let repo_path = repo.relativize(path).unwrap();
|
let repo_path = repo.relativize(path).unwrap();
|
||||||
repo.statuses_by_path
|
repo.statuses_by_path
|
||||||
.get(&PathKey(repo_path.0), &())
|
.get(&PathKey(repo_path.0), &())
|
||||||
.map(|entry| entry.status)
|
.map(|entry| entry.combined_status())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2574,8 +2581,8 @@ impl Snapshot {
|
||||||
.map(|repo| repo.status().collect())
|
.map(|repo| repo.status().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repositories(&self) -> impl Iterator<Item = &RepositoryEntry> {
|
pub fn repositories(&self) -> &SumTree<RepositoryEntry> {
|
||||||
self.repositories.iter()
|
&self.repositories
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the repository whose work directory corresponds to the given path.
|
/// Get the repository whose work directory corresponds to the given path.
|
||||||
|
@ -2609,7 +2616,7 @@ impl Snapshot {
|
||||||
entries: impl 'a + Iterator<Item = &'a Entry>,
|
entries: impl 'a + Iterator<Item = &'a Entry>,
|
||||||
) -> impl 'a + Iterator<Item = (&'a Entry, Option<&'a RepositoryEntry>)> {
|
) -> impl 'a + Iterator<Item = (&'a Entry, Option<&'a RepositoryEntry>)> {
|
||||||
let mut containing_repos = Vec::<&RepositoryEntry>::new();
|
let mut containing_repos = Vec::<&RepositoryEntry>::new();
|
||||||
let mut repositories = self.repositories().peekable();
|
let mut repositories = self.repositories().iter().peekable();
|
||||||
entries.map(move |entry| {
|
entries.map(move |entry| {
|
||||||
while let Some(repository) = containing_repos.last() {
|
while let Some(repository) = containing_repos.last() {
|
||||||
if repository.directory_contains(&entry.path) {
|
if repository.directory_contains(&entry.path) {
|
||||||
|
@ -3626,14 +3633,31 @@ pub type UpdatedGitRepositoriesSet = Arc<[(Arc<Path>, GitRepositoryChange)]>;
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct StatusEntry {
|
pub struct StatusEntry {
|
||||||
pub repo_path: RepoPath,
|
pub repo_path: RepoPath,
|
||||||
pub status: GitFileStatus,
|
pub status: GitStatusPair,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusEntry {
|
impl StatusEntry {
|
||||||
|
// TODO revisit uses of this
|
||||||
|
pub fn combined_status(&self) -> GitFileStatus {
|
||||||
|
self.status.combined()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index_status(&self) -> Option<GitFileStatus> {
|
||||||
|
self.status.index_status
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn worktree_status(&self) -> Option<GitFileStatus> {
|
||||||
|
self.status.worktree_status
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_staged(&self) -> Option<bool> {
|
||||||
|
self.status.is_staged()
|
||||||
|
}
|
||||||
|
|
||||||
fn to_proto(&self) -> proto::StatusEntry {
|
fn to_proto(&self) -> proto::StatusEntry {
|
||||||
proto::StatusEntry {
|
proto::StatusEntry {
|
||||||
repo_path: self.repo_path.to_proto(),
|
repo_path: self.repo_path.to_proto(),
|
||||||
status: git_status_to_proto(self.status),
|
status: status_pair_to_proto(self.status.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3641,11 +3665,10 @@ impl StatusEntry {
|
||||||
impl TryFrom<proto::StatusEntry> for StatusEntry {
|
impl TryFrom<proto::StatusEntry> for StatusEntry {
|
||||||
type Error = anyhow::Error;
|
type Error = anyhow::Error;
|
||||||
fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
|
fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
let repo_path = RepoPath(Path::new(&value.repo_path).into());
|
||||||
repo_path: RepoPath(Path::new(&value.repo_path).into()),
|
let status = status_pair_from_proto(value.status)
|
||||||
status: git_status_from_proto(Some(value.status))
|
.ok_or_else(|| anyhow!("Unable to parse status value {}", value.status))?;
|
||||||
.ok_or_else(|| anyhow!("Unable to parse status value {}", value.status))?,
|
Ok(Self { repo_path, status })
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3729,7 +3752,7 @@ impl sum_tree::Item for StatusEntry {
|
||||||
fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
|
fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
|
||||||
PathSummary {
|
PathSummary {
|
||||||
max_path: self.repo_path.0.clone(),
|
max_path: self.repo_path.0.clone(),
|
||||||
item_summary: match self.status {
|
item_summary: match self.combined_status() {
|
||||||
GitFileStatus::Added => GitStatuses {
|
GitFileStatus::Added => GitStatuses {
|
||||||
added: 1,
|
added: 1,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -4820,15 +4843,15 @@ impl BackgroundScanner {
|
||||||
|
|
||||||
for (repo_path, status) in &*status.entries {
|
for (repo_path, status) in &*status.entries {
|
||||||
paths.remove_repo_path(repo_path);
|
paths.remove_repo_path(repo_path);
|
||||||
if cursor.seek_forward(&PathTarget::Path(&repo_path), Bias::Left, &()) {
|
if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left, &()) {
|
||||||
if cursor.item().unwrap().status == *status {
|
if &cursor.item().unwrap().status == status {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changed_path_statuses.push(Edit::Insert(StatusEntry {
|
changed_path_statuses.push(Edit::Insert(StatusEntry {
|
||||||
repo_path: repo_path.clone(),
|
repo_path: repo_path.clone(),
|
||||||
status: *status,
|
status: status.clone(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5257,7 +5280,7 @@ impl BackgroundScanner {
|
||||||
new_entries_by_path.insert_or_replace(
|
new_entries_by_path.insert_or_replace(
|
||||||
StatusEntry {
|
StatusEntry {
|
||||||
repo_path: repo_path.clone(),
|
repo_path: repo_path.clone(),
|
||||||
status: *status,
|
status: status.clone(),
|
||||||
},
|
},
|
||||||
&(),
|
&(),
|
||||||
);
|
);
|
||||||
|
@ -5771,7 +5794,7 @@ impl<'a> GitTraversal<'a> {
|
||||||
} else if entry.is_file() {
|
} else if entry.is_file() {
|
||||||
// For a file entry, park the cursor on the corresponding status
|
// For a file entry, park the cursor on the corresponding status
|
||||||
if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()) {
|
if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()) {
|
||||||
self.current_entry_status = Some(statuses.item().unwrap().status);
|
self.current_entry_status = Some(statuses.item().unwrap().combined_status());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6136,19 +6159,23 @@ impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_status_from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
|
// TODO pass the status pair all the way through
|
||||||
git_status.and_then(|status| {
|
fn status_pair_from_proto(proto: i32) -> Option<GitStatusPair> {
|
||||||
proto::GitStatus::from_i32(status).map(|status| match status {
|
let proto = proto::GitStatus::from_i32(proto)?;
|
||||||
proto::GitStatus::Added => GitFileStatus::Added,
|
let worktree_status = match proto {
|
||||||
proto::GitStatus::Modified => GitFileStatus::Modified,
|
proto::GitStatus::Added => GitFileStatus::Added,
|
||||||
proto::GitStatus::Conflict => GitFileStatus::Conflict,
|
proto::GitStatus::Modified => GitFileStatus::Modified,
|
||||||
proto::GitStatus::Deleted => GitFileStatus::Deleted,
|
proto::GitStatus::Conflict => GitFileStatus::Conflict,
|
||||||
})
|
proto::GitStatus::Deleted => GitFileStatus::Deleted,
|
||||||
|
};
|
||||||
|
Some(GitStatusPair {
|
||||||
|
index_status: None,
|
||||||
|
worktree_status: Some(worktree_status),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_status_to_proto(status: GitFileStatus) -> i32 {
|
fn status_pair_to_proto(status: GitStatusPair) -> i32 {
|
||||||
match status {
|
match status.combined() {
|
||||||
GitFileStatus::Added => proto::GitStatus::Added as i32,
|
GitFileStatus::Added => proto::GitStatus::Added as i32,
|
||||||
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
|
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
|
||||||
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
|
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
|
||||||
|
|
|
@ -2179,7 +2179,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
let tree = tree.read(cx);
|
let tree = tree.read(cx);
|
||||||
let repo = tree.repositories().next().unwrap();
|
let repo = tree.repositories().iter().next().unwrap();
|
||||||
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
|
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project1/a")),
|
tree.status_for_file(Path::new("projects/project1/a")),
|
||||||
|
@ -2200,7 +2200,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
let tree = tree.read(cx);
|
let tree = tree.read(cx);
|
||||||
let repo = tree.repositories().next().unwrap();
|
let repo = tree.repositories().iter().next().unwrap();
|
||||||
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
|
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project2/a")),
|
tree.status_for_file(Path::new("projects/project2/a")),
|
||||||
|
@ -2380,8 +2380,8 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||||
// Check that the right git state is observed on startup
|
// Check that the right git state is observed on startup
|
||||||
tree.read_with(cx, |tree, _cx| {
|
tree.read_with(cx, |tree, _cx| {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
assert_eq!(snapshot.repositories().count(), 1);
|
assert_eq!(snapshot.repositories().iter().count(), 1);
|
||||||
let repo_entry = snapshot.repositories().next().unwrap();
|
let repo_entry = snapshot.repositories().iter().next().unwrap();
|
||||||
assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
|
assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
|
||||||
assert!(repo_entry.location_in_repo.is_none());
|
assert!(repo_entry.location_in_repo.is_none());
|
||||||
|
|
||||||
|
@ -2554,16 +2554,16 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||||
// Check that the right git state is observed on startup
|
// Check that the right git state is observed on startup
|
||||||
tree.read_with(cx, |tree, _cx| {
|
tree.read_with(cx, |tree, _cx| {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
let repo = snapshot.repositories().next().unwrap();
|
let repo = snapshot.repositories().iter().next().unwrap();
|
||||||
let entries = repo.status().collect::<Vec<_>>();
|
let entries = repo.status().collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(entries.len(), 3);
|
assert_eq!(entries.len(), 3);
|
||||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||||
assert_eq!(entries[0].status, GitFileStatus::Modified);
|
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Modified));
|
||||||
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
||||||
assert_eq!(entries[1].status, GitFileStatus::Untracked);
|
assert_eq!(entries[1].worktree_status(), Some(GitFileStatus::Untracked));
|
||||||
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
|
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
|
||||||
assert_eq!(entries[2].status, GitFileStatus::Deleted);
|
assert_eq!(entries[2].worktree_status(), Some(GitFileStatus::Deleted));
|
||||||
});
|
});
|
||||||
|
|
||||||
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
|
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
|
||||||
|
@ -2576,19 +2576,19 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
tree.read_with(cx, |tree, _cx| {
|
tree.read_with(cx, |tree, _cx| {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
let repository = snapshot.repositories().next().unwrap();
|
let repository = snapshot.repositories().iter().next().unwrap();
|
||||||
let entries = repository.status().collect::<Vec<_>>();
|
let entries = repository.status().collect::<Vec<_>>();
|
||||||
|
|
||||||
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
|
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
|
||||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||||
assert_eq!(entries[0].status, GitFileStatus::Modified);
|
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Modified));
|
||||||
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
||||||
assert_eq!(entries[1].status, GitFileStatus::Untracked);
|
assert_eq!(entries[1].worktree_status(), Some(GitFileStatus::Untracked));
|
||||||
// Status updated
|
// Status updated
|
||||||
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
|
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
|
||||||
assert_eq!(entries[2].status, GitFileStatus::Modified);
|
assert_eq!(entries[2].worktree_status(), Some(GitFileStatus::Modified));
|
||||||
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
|
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
|
||||||
assert_eq!(entries[3].status, GitFileStatus::Deleted);
|
assert_eq!(entries[3].worktree_status(), Some(GitFileStatus::Deleted));
|
||||||
});
|
});
|
||||||
|
|
||||||
git_add("a.txt", &repo);
|
git_add("a.txt", &repo);
|
||||||
|
@ -2609,7 +2609,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
tree.read_with(cx, |tree, _cx| {
|
tree.read_with(cx, |tree, _cx| {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
let repo = snapshot.repositories().next().unwrap();
|
let repo = snapshot.repositories().iter().next().unwrap();
|
||||||
let entries = repo.status().collect::<Vec<_>>();
|
let entries = repo.status().collect::<Vec<_>>();
|
||||||
|
|
||||||
// Deleting an untracked entry, b.txt, should leave no status
|
// Deleting an untracked entry, b.txt, should leave no status
|
||||||
|
@ -2621,7 +2621,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||||
&entries
|
&entries
|
||||||
);
|
);
|
||||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||||
assert_eq!(entries[0].status, GitFileStatus::Deleted);
|
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Deleted));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2676,8 +2676,8 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
||||||
// Ensure that the git status is loaded correctly
|
// Ensure that the git status is loaded correctly
|
||||||
tree.read_with(cx, |tree, _cx| {
|
tree.read_with(cx, |tree, _cx| {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
assert_eq!(snapshot.repositories().count(), 1);
|
assert_eq!(snapshot.repositories().iter().count(), 1);
|
||||||
let repo = snapshot.repositories().next().unwrap();
|
let repo = snapshot.repositories().iter().next().unwrap();
|
||||||
// Path is blank because the working directory of
|
// Path is blank because the working directory of
|
||||||
// the git repository is located at the root of the project
|
// the git repository is located at the root of the project
|
||||||
assert_eq!(repo.path.as_ref(), Path::new(""));
|
assert_eq!(repo.path.as_ref(), Path::new(""));
|
||||||
|
@ -2707,7 +2707,7 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
||||||
tree.read_with(cx, |tree, _cx| {
|
tree.read_with(cx, |tree, _cx| {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
|
|
||||||
assert!(snapshot.repositories().next().is_some());
|
assert!(snapshot.repositories().iter().next().is_some());
|
||||||
|
|
||||||
assert_eq!(snapshot.status_for_file("c.txt"), None);
|
assert_eq!(snapshot.status_for_file("c.txt"), None);
|
||||||
assert_eq!(snapshot.status_for_file("d/e.txt"), None);
|
assert_eq!(snapshot.status_for_file("d/e.txt"), None);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue