diff --git a/.github/config-schema.json b/.github/config-schema.json index ac945d236..5dc577147 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -682,6 +682,14 @@ "staged": "+", "untracked": "?", "typechanged": "", + "worktree_added": "", + "worktree_deleted": "", + "worktree_modified": "", + "worktree_typechanged": "", + "index_added": "", + "index_deleted": "", + "index_modified": "", + "index_typechanged": "", "ignore_submodules": false, "disabled": false, "use_git_executable": false @@ -3575,6 +3583,38 @@ "type": "string", "default": "" }, + "worktree_added": { + "type": "string", + "default": "" + }, + "worktree_deleted": { + "type": "string", + "default": "" + }, + "worktree_modified": { + "type": "string", + "default": "" + }, + "worktree_typechanged": { + "type": "string", + "default": "" + }, + "index_added": { + "type": "string", + "default": "" + }, + "index_deleted": { + "type": "string", + "default": "" + }, + "index_modified": { + "type": "string", + "default": "" + }, + "index_typechanged": { + "type": "string", + "default": "" + }, "ignore_submodules": { "type": "boolean", "default": false diff --git a/docs/config/README.md b/docs/config/README.md index 8b7cf822e..375977d56 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -2044,44 +2044,60 @@ current directory. ### Options -| Option | Default | Description | -| -------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| `format` | `'([\[$all_status$ahead_behind\]]($style) )'` | The default format for `git_status` | -| `conflicted` | `'='` | This branch has merge conflicts. | -| `ahead` | `'⇡'` | The format of `ahead` | -| `behind` | `'⇣'` | The format of `behind` | -| `diverged` | `'⇕'` | The format of `diverged` | -| `up_to_date` | `''` | The format of `up_to_date` | -| `untracked` | `'?'` | The format of `untracked` | -| `stashed` | `'\$'` | The format of `stashed` | -| `modified` | `'!'` | The format of `modified` | -| `staged` | `'+'` | The format of `staged` | -| `renamed` | `'»'` | The format of `renamed` | -| `deleted` | `'✘'` | The format of `deleted` | -| `typechanged` | `""` | The format of `typechanged` | -| `style` | `'bold red'` | The style for the module. | -| `ignore_submodules` | `false` | Ignore changes to submodules. | -| `disabled` | `false` | Disables the `git_status` module. | -| `windows_starship` | | Use this (Linux) path to a Windows Starship executable to render `git_status` when on Windows paths in WSL. | -| `use_git_executable` | `false` | Do not use `gitoxide` for computing the status, but use the `git` executable instead. | +| Option | Default | Description | +| ---------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `format` | `'([\[$all_status$ahead_behind\]]($style) )'` | The default format for `git_status` | +| `conflicted` | `'='` | This branch has merge conflicts. | +| `ahead` | `'⇡'` | The format of `ahead` | +| `behind` | `'⇣'` | The format of `behind` | +| `diverged` | `'⇕'` | The format of `diverged` | +| `up_to_date` | `''` | The format of `up_to_date` | +| `untracked` | `'?'` | The format of `untracked` | +| `stashed` | `'\$'` | The format of `stashed` | +| `modified` | `'!'` | The format of `modified` | +| `staged` | `'+'` | The format of `staged` | +| `renamed` | `'»'` | The format of `renamed` | +| `deleted` | `'✘'` | The format of `deleted` | +| `typechanged` | `""` | The format of `typechanged` | +| `style` | `'bold red'` | The style for the module. | +| `ignore_submodules` | `false` | Ignore changes to submodules. | +| `worktree_added` | `""` | The format of `worktree_added` | +| `worktree_deleted` | `""` | The format of `worktree_deleted` | +| `worktree_modified` | `""` | The format of `worktree_modified` | +| `worktree_typechanged` | `""` | The format of `worktree_typechanged` | +| `index_added` | `""` | The format of `index_added` | +| `index_deleted` | `""` | The format of `index_deleted` | +| `index_modified` | `""` | The format of `index_modified` | +| `index_typechanged` | `""` | The format of `index_typechanged` | +| `disabled` | `false` | Disables the `git_status` module. | +| `windows_starship` | | Use this (Linux) path to a Windows Starship executable to render `git_status` when on Windows paths in WSL. | +| `use_git_executable` | `false` | Do not use `gitoxide` for computing the status, but use the `git` executable instead. | ### Variables The following variables can be used in `format`: -| Variable | Description | -| -------------- | ------------------------------------------------------------------------------------------------------------- | -| `all_status` | Shortcut for`$conflicted$stashed$deleted$renamed$modified$typechanged$staged$untracked` | -| `ahead_behind` | Displays `diverged`, `ahead`, `behind` or `up_to_date` format string based on the current status of the repo. | -| `conflicted` | Displays `conflicted` when this branch has merge conflicts. | -| `untracked` | Displays `untracked` when there are untracked files in the working directory. | -| `stashed` | Displays `stashed` when a stash exists for the local repository. | -| `modified` | Displays `modified` when there are file modifications in the working directory. | -| `staged` | Displays `staged` when a new file has been added to the staging area. | -| `renamed` | Displays `renamed` when a renamed file has been added to the staging area. | -| `deleted` | Displays `deleted` when a file's deletion has been added to the staging area. | -| `typechanged` | Displays `typechanged` when a file's type has been changed in the staging area. | -| style\* | Mirrors the value of option `style` | +| Variable | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------- | +| `all_status` | Shortcut for`$conflicted$stashed$deleted$renamed$modified$typechanged$staged$untracked` | +| `ahead_behind` | Displays `diverged`, `ahead`, `behind` or `up_to_date` format string based on the current status of the repo. | +| `conflicted` | Displays `conflicted` when this branch has merge conflicts. | +| `untracked` | Displays `untracked` when there are untracked files in the working directory. | +| `stashed` | Displays `stashed` when a stash exists for the local repository. | +| `modified` | Displays `modified` when there are file modifications in the working directory. | +| `staged` | Displays `staged` when a new file has been added to the staging area. | +| `renamed` | Displays `renamed` when a renamed file has been added to the staging area. | +| `deleted` | Displays `deleted` when a file's deletion has been added to the staging area. | +| `typechanged` | Displays `typechanged` when a file's type has been changed in the staging area. | +| `worktree_added` | Displays `worktree_added` when a new file has been added in the working directory. | +| `worktree_deleted` | Displays `worktree_deleted` when a file's been deleted in the working directory. | +| `worktree_modified` | Displays `worktree_modified` when a file's been modified in the working directory. | +| `worktree_typechanged` | Displays `worktree_typechanged` when a file's type has been changed in the working directory. | +| `index_added` | Displays `index_added` when a new file has been added to the staging area. | +| `index_deleted` | Displays `index_deleted` when a file's been deleted to the staging area. | +| `index_modified` | Displays `index_modified` when a file's been modified to the staging area. | +| `index_typechanged` | Displays `index_typechanged` when a file's type has been changed to the staging area. | +| style\* | Mirrors the value of option `style` | *: This variable can only be used as a part of a style string @@ -2092,7 +2108,7 @@ The following variables can be used in `diverged`: | `ahead_count` | Number of commits ahead of the tracking branch | | `behind_count` | Number of commits behind the tracking branch | -The following variables can be used in `conflicted`, `ahead`, `behind`, `untracked`, `stashed`, `modified`, `staged`, `renamed` and `deleted`: +The following variables can be used in `conflicted`, `ahead`, `behind`, `untracked`, `stashed`, `modified`, `staged`, `renamed`, `deleted`, `typechanged`, `worktree_added`, `worktree_deleted`, `worktree_modified`, `worktree_typechanged`, `index_added`, `index_deleted`, `index_modified`, and `index_typechanged`: | Variable | Description | | -------- | ------------------------ | diff --git a/src/configs/git_status.rs b/src/configs/git_status.rs index 56460288c..2371339ed 100644 --- a/src/configs/git_status.rs +++ b/src/configs/git_status.rs @@ -22,6 +22,14 @@ pub struct GitStatusConfig<'a> { pub staged: &'a str, pub untracked: &'a str, pub typechanged: &'a str, + pub worktree_added: &'a str, + pub worktree_deleted: &'a str, + pub worktree_modified: &'a str, + pub worktree_typechanged: &'a str, + pub index_added: &'a str, + pub index_deleted: &'a str, + pub index_modified: &'a str, + pub index_typechanged: &'a str, pub ignore_submodules: bool, pub disabled: bool, pub use_git_executable: bool, @@ -46,6 +54,14 @@ impl Default for GitStatusConfig<'_> { staged: "+", untracked: "?", typechanged: "", + worktree_added: "", + worktree_deleted: "", + worktree_modified: "", + worktree_typechanged: "", + index_added: "", + index_deleted: "", + index_modified: "", + index_typechanged: "", ignore_submodules: false, disabled: false, use_git_executable: false, diff --git a/src/modules/git_status.rs b/src/modules/git_status.rs index dbcef5780..dee36c0b7 100644 --- a/src/modules/git_status.rs +++ b/src/modules/git_status.rs @@ -107,6 +107,65 @@ pub fn module<'a>(context: &'a Context) -> Option> { "typechanged" => info.get_typechanged().and_then(|count| { format_count(config.typechanged, "git_status.typechanged", context, count) }), + "worktree_added" => info.get_worktree_added().and_then(|count| { + format_count( + config.worktree_added, + "git_status.worktree_added", + context, + count, + ) + }), + "worktree_deleted" => info.get_worktree_deleted().and_then(|count| { + format_count( + config.worktree_deleted, + "git_status.worktree_deleted", + context, + count, + ) + }), + "worktree_modified" => info.get_worktree_modified().and_then(|count| { + format_count( + config.worktree_modified, + "git_status.worktree_modified", + context, + count, + ) + }), + "worktree_typechanged" => info.get_worktree_typechanged().and_then(|count| { + format_count( + config.worktree_typechanged, + "git_status.worktree_typechanged", + context, + count, + ) + }), + "index_added" => info.get_index_added().and_then(|count| { + format_count(config.index_added, "git_status.index_added", context, count) + }), + "index_deleted" => info.get_index_deleted().and_then(|count| { + format_count( + config.index_deleted, + "git_status.index_deleted", + context, + count, + ) + }), + "index_modified" => info.get_index_modified().and_then(|count| { + format_count( + config.index_modified, + "git_status.index_modified", + context, + count, + ) + }), + "index_typechanged" => info.get_index_typechanged().and_then(|count| { + format_count( + config.index_typechanged, + "git_status.index_typechanged", + context, + count, + ) + }), _ => None, }; segments.map(Ok) @@ -204,6 +263,38 @@ impl<'a> GitStatusInfo<'a> { pub fn get_typechanged(&self) -> Option { self.get_repo_status().map(|data| data.typechanged) } + + pub fn get_worktree_added(&self) -> Option { + self.get_repo_status().map(|data| data.worktree_added) + } + + pub fn get_worktree_deleted(&self) -> Option { + self.get_repo_status().map(|data| data.worktree_deleted) + } + + pub fn get_worktree_modified(&self) -> Option { + self.get_repo_status().map(|data| data.worktree_modified) + } + + pub fn get_worktree_typechanged(&self) -> Option { + self.get_repo_status().map(|data| data.worktree_typechanged) + } + + pub fn get_index_added(&self) -> Option { + self.get_repo_status().map(|data| data.index_added) + } + + pub fn get_index_deleted(&self) -> Option { + self.get_repo_status().map(|data| data.index_deleted) + } + + pub fn get_index_modified(&self) -> Option { + self.get_repo_status().map(|data| data.index_modified) + } + + pub fn get_index_typechanged(&self) -> Option { + self.get_repo_status().map(|data| data.index_typechanged) + } } /// Return a globally shared version the repository status so it can be reused. @@ -380,11 +471,25 @@ fn get_repo_status( status::Item::TreeIndex(change) => { use gix::diff::index::Change; match change { - Change::Addition { .. } | Change::Modification { .. } => { + Change::Addition { .. } => { repo_status.staged += 1; + repo_status.index_added += 1; } Change::Deletion { .. } => { repo_status.deleted += 1; + repo_status.index_deleted += 1; + } + Change::Modification { + previous_entry_mode, + entry_mode, + .. + } => { + repo_status.staged += 1; + if previous_entry_mode == entry_mode { + repo_status.index_modified += 1; + } else { + repo_status.index_typechanged += 1; + } } Change::Rewrite { .. } => { repo_status.renamed += 1; @@ -406,22 +511,31 @@ fn get_repo_status( .. } => { repo_status.deleted += 1; + repo_status.worktree_deleted += 1 + } + Item::Modification { + status: EntryStatus::IntentToAdd, + .. + } => { + repo_status.modified += 1; + repo_status.worktree_added += 1 } Item::Modification { status: - EntryStatus::IntentToAdd - | EntryStatus::Change( + EntryStatus::Change( Change::Modification { .. } | Change::SubmoduleModification(_), ), .. } => { repo_status.modified += 1; + repo_status.worktree_modified += 1 } Item::Modification { status: EntryStatus::Change(Change::Type { .. }), .. } => { repo_status.typechanged += 1; + repo_status.worktree_typechanged += 1 } Item::DirectoryContents { entry: @@ -500,46 +614,86 @@ pub(crate) struct RepoStatus { staged: usize, typechanged: usize, untracked: usize, + worktree_added: usize, + worktree_deleted: usize, + worktree_modified: usize, + worktree_typechanged: usize, + index_added: usize, + index_deleted: usize, + index_modified: usize, + index_typechanged: usize, } impl RepoStatus { - fn is_deleted(short_status: &str) -> bool { - // is_wt_deleted || is_index_deleted - short_status.contains('D') + fn is_index_typechanged(short_status: &str) -> bool { + short_status.starts_with('T') } - fn is_modified(short_status: &str) -> bool { - // is_wt_modified || is_wt_added - short_status.ends_with('M') || short_status.ends_with('A') - } - - fn is_staged(short_status: &str) -> bool { - // is_index_modified || is_index_added || is_index_typechanged - short_status.starts_with('M') - || short_status.starts_with('A') - || short_status.starts_with('T') - } - - fn is_typechanged(short_status: &str) -> bool { + fn is_worktree_typechanged(short_status: &str) -> bool { short_status.ends_with('T') } + fn is_index_deleted(short_status: &str) -> bool { + short_status.starts_with('D') + } + + fn is_worktree_deleted(short_status: &str) -> bool { + short_status.ends_with('D') + } + + fn is_index_added(short_status: &str) -> bool { + short_status.starts_with('A') + } + + fn is_worktree_added(short_status: &str) -> bool { + short_status.ends_with('A') + } + + fn is_index_modified(short_status: &str) -> bool { + short_status.starts_with('M') + } + + fn is_worktree_modified(short_status: &str) -> bool { + short_status.ends_with('M') + } + fn parse_normal_status(&mut self, short_status: &str) { - if Self::is_deleted(short_status) { - self.deleted += 1; + if Self::is_worktree_added(short_status) { + self.worktree_added += 1; } - if Self::is_modified(short_status) { - self.modified += 1; + if Self::is_worktree_deleted(short_status) { + self.worktree_deleted += 1; } - if Self::is_staged(short_status) { - self.staged += 1; + if Self::is_worktree_modified(short_status) { + self.worktree_modified += 1; } - if Self::is_typechanged(short_status) { - self.typechanged += 1; + if Self::is_worktree_typechanged(short_status) { + self.worktree_typechanged += 1; } + + if Self::is_index_added(short_status) { + self.index_added += 1; + } + + if Self::is_index_deleted(short_status) { + self.index_deleted += 1; + } + + if Self::is_index_modified(short_status) { + self.index_modified += 1; + } + + if Self::is_index_typechanged(short_status) { + self.index_typechanged += 1; + } + + self.deleted = self.worktree_deleted + self.index_deleted; + self.modified = self.worktree_modified + self.worktree_added; + self.staged = self.index_modified + self.index_added + self.index_typechanged; + self.typechanged = self.worktree_typechanged; } fn add(&mut self, s: &str) { @@ -1248,6 +1402,26 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn shows_worktree_typechanged_with_count() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + create_typechanged(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .config(toml::toml! { + [git_status] + format = "$worktree_typechanged" + worktree_typechanged = "$count" + }) + .path(repo_dir.path()) + .collect(); + let expected = Some(String::from("1")); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_modified() -> io::Result<()> { for mode in NORMAL_AND_REFTABLES { @@ -1312,6 +1486,46 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn shows_worktree_modified_with_count() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + create_modified(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .config(toml::toml! { + [git_status] + format = "$worktree_modified" + worktree_modified = "$count" + }) + .path(repo_dir.path()) + .collect(); + let expected = Some(String::from("1")); + + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn shows_index_modified_with_count() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + create_indexed_modified(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .config(toml::toml! { + [git_status] + format = "$index_modified" + index_modified = "$count" + }) + .path(repo_dir.path()) + .collect(); + let expected = Some(String::from("1")); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_added() -> io::Result<()> { for mode in NORMAL_AND_REFTABLES { @@ -1330,6 +1544,26 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn shows_worktree_added_with_count() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + create_added(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .config(toml::toml! { + [git_status] + format = "$worktree_added" + worktree_added = "$count" + }) + .path(repo_dir.path()) + .collect(); + let expected = Some(String::from("1")); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_staged_file() -> io::Result<()> { for mode in NORMAL_AND_REFTABLES { @@ -1377,6 +1611,26 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn shows_index_added_with_count() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + create_staged(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .config(toml::toml! { + [git_status] + format = "$index_added" + index_added = "$count" + }) + .path(repo_dir.path()) + .collect(); + let expected = Some(String::from("1")); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_staged_typechange_with_count() -> io::Result<()> { for mode in NORMAL_AND_REFTABLES { @@ -1406,6 +1660,26 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn shows_index_typechanged_with_count() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + create_staged_typechange(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .config(toml::toml! { + [git_status] + format = "$index_typechanged" + index_typechanged = "$count" + }) + .path(repo_dir.path()) + .collect(); + let expected = Some(String::from("1")); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_staged_and_modified_file() -> io::Result<()> { for mode in NORMAL_AND_REFTABLES { @@ -1540,6 +1814,46 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn shows_worktree_deleted_file_with_count() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + create_deleted(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .config(toml::toml! { + [git_status] + format = "$worktree_deleted" + worktree_deleted = "$count" + }) + .path(repo_dir.path()) + .collect(); + let expected = Some(String::from("1")); + + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn shows_index_deleted_file_with_count() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + create_indexed_deleted(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .config(toml::toml! { + [git_status] + format = "$index_deleted" + index_deleted = "$count" + }) + .path(repo_dir.path()) + .collect(); + let expected = Some(String::from("1")); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn doesnt_show_ignored_file() -> io::Result<()> { for mode in NORMAL_AND_REFTABLES { @@ -1814,6 +2128,17 @@ pub(crate) mod tests { Ok(()) } + fn create_indexed_modified(repo_dir: &Path) -> io::Result<()> { + create_modified(repo_dir)?; + + create_command("git")? + .args(["add", "."]) + .current_dir(repo_dir) + .output()?; + + Ok(()) + } + fn create_staged(repo_dir: &Path) -> io::Result<()> { File::create(repo_dir.join("license"))?.sync_all()?; @@ -1887,6 +2212,17 @@ pub(crate) mod tests { Ok(()) } + fn create_indexed_deleted(repo_dir: &Path) -> io::Result<()> { + create_deleted(repo_dir)?; + + create_command("git")? + .args(["add", "."]) + .current_dir(repo_dir) + .output()?; + + Ok(()) + } + fn create_staged_and_ignored(repo_dir: &Path) -> io::Result<()> { let mut file = File::create(repo_dir.join(".gitignore"))?; writeln!(&mut file, "ignored.txt")?;