Changed file picker (#5645)
Co-authored-by: WJH <hou32hou@gmail.com> Co-authored-by: Michael Davis <mcarsondavis@gmail.com> Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
This commit is contained in:
parent
1abb64e48d
commit
a224ee5079
9 changed files with 380 additions and 24 deletions
67
Cargo.lock
generated
67
Cargo.lock
generated
|
@ -338,6 +338,19 @@ dependencies = [
|
|||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.12.3",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.4"
|
||||
|
@ -536,6 +549,7 @@ dependencies = [
|
|||
"gix-config",
|
||||
"gix-date",
|
||||
"gix-diff",
|
||||
"gix-dir",
|
||||
"gix-discover",
|
||||
"gix-features",
|
||||
"gix-filter",
|
||||
|
@ -557,6 +571,7 @@ dependencies = [
|
|||
"gix-revision",
|
||||
"gix-revwalk",
|
||||
"gix-sec",
|
||||
"gix-status",
|
||||
"gix-submodule",
|
||||
"gix-tempfile",
|
||||
"gix-trace",
|
||||
|
@ -699,8 +714,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "78e605593c2ef74980a534ade0909c7dc57cca72baa30cbb67d2dda621f99ac4"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-command",
|
||||
"gix-filter",
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-object",
|
||||
"gix-path",
|
||||
"gix-tempfile",
|
||||
"gix-trace",
|
||||
"gix-worktree",
|
||||
"imara-diff",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-dir"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3413ccd29130900c17574678aee640e4847909acae9febf6424dc77b782c6d32"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-discover",
|
||||
"gix-fs",
|
||||
"gix-ignore",
|
||||
"gix-index",
|
||||
"gix-object",
|
||||
"gix-path",
|
||||
"gix-pathspec",
|
||||
"gix-trace",
|
||||
"gix-utils",
|
||||
"gix-worktree",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
@ -1054,6 +1097,28 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-status"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca216db89947eca709f69ec5851aa76f9628e7c7aab7aa5a927d0c619d046bf2"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"filetime",
|
||||
"gix-diff",
|
||||
"gix-dir",
|
||||
"gix-features",
|
||||
"gix-filter",
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-index",
|
||||
"gix-object",
|
||||
"gix-path",
|
||||
"gix-pathspec",
|
||||
"gix-worktree",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-submodule"
|
||||
version = "0.10.0"
|
||||
|
@ -1075,6 +1140,7 @@ version = "13.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d337955b7af00fb87120d053d87cdfb422a80b9ff7a3aa4057a99c79422dc30"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"gix-fs",
|
||||
"libc",
|
||||
"once_cell",
|
||||
|
@ -1124,6 +1190,7 @@ version = "0.1.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0066432d4c277f9877f091279a597ea5331f68ca410efc874f0bdfb1cd348f92"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"fastrand",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
|
|
@ -251,6 +251,7 @@ We use a similar set of scopes as
|
|||
- `gutter` - gutter indicator
|
||||
- `delta` - modifications
|
||||
- `moved` - renamed or moved files/changes
|
||||
- `conflict` - merge conflicts
|
||||
- `gutter` - gutter indicator
|
||||
|
||||
#### Interface
|
||||
|
|
|
@ -3,10 +3,14 @@ pub(crate) mod lsp;
|
|||
pub(crate) mod typed;
|
||||
|
||||
pub use dap::*;
|
||||
use helix_event::status;
|
||||
use helix_stdx::rope::{self, RopeSliceExt};
|
||||
use helix_vcs::Hunk;
|
||||
use helix_vcs::{FileChange, Hunk};
|
||||
pub use lsp::*;
|
||||
use tui::widgets::Row;
|
||||
use tui::{
|
||||
text::Span,
|
||||
widgets::{Cell, Row},
|
||||
};
|
||||
pub use typed::*;
|
||||
|
||||
use helix_core::{
|
||||
|
@ -39,6 +43,7 @@ use helix_view::{
|
|||
info::Info,
|
||||
input::KeyEvent,
|
||||
keyboard::KeyCode,
|
||||
theme::Style,
|
||||
tree,
|
||||
view::View,
|
||||
Document, DocumentId, Editor, ViewId,
|
||||
|
@ -54,7 +59,7 @@ use crate::{
|
|||
filter_picker_entry,
|
||||
job::Callback,
|
||||
keymap::ReverseKeymap,
|
||||
ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent},
|
||||
ui::{self, menu::Item, overlay::overlaid, Picker, Popup, Prompt, PromptEvent},
|
||||
};
|
||||
|
||||
use crate::job::{self, Jobs};
|
||||
|
@ -324,6 +329,7 @@ impl MappableCommand {
|
|||
buffer_picker, "Open buffer picker",
|
||||
jumplist_picker, "Open jumplist picker",
|
||||
symbol_picker, "Open symbol picker",
|
||||
changed_file_picker, "Open changed file picker",
|
||||
select_references_to_symbol_under_cursor, "Select symbol references",
|
||||
workspace_symbol_picker, "Open workspace symbol picker",
|
||||
diagnostics_picker, "Open diagnostic picker",
|
||||
|
@ -2996,6 +3002,94 @@ fn jumplist_picker(cx: &mut Context) {
|
|||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
fn changed_file_picker(cx: &mut Context) {
|
||||
pub struct FileChangeData {
|
||||
cwd: PathBuf,
|
||||
style_untracked: Style,
|
||||
style_modified: Style,
|
||||
style_conflict: Style,
|
||||
style_deleted: Style,
|
||||
style_renamed: Style,
|
||||
}
|
||||
|
||||
impl Item for FileChange {
|
||||
type Data = FileChangeData;
|
||||
|
||||
fn format(&self, data: &Self::Data) -> Row {
|
||||
let process_path = |path: &PathBuf| {
|
||||
path.strip_prefix(&data.cwd)
|
||||
.unwrap_or(path)
|
||||
.display()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let (sign, style, content) = match self {
|
||||
Self::Untracked { path } => ("[+]", data.style_untracked, process_path(path)),
|
||||
Self::Modified { path } => ("[~]", data.style_modified, process_path(path)),
|
||||
Self::Conflict { path } => ("[x]", data.style_conflict, process_path(path)),
|
||||
Self::Deleted { path } => ("[-]", data.style_deleted, process_path(path)),
|
||||
Self::Renamed { from_path, to_path } => (
|
||||
"[>]",
|
||||
data.style_renamed,
|
||||
format!("{} -> {}", process_path(from_path), process_path(to_path)),
|
||||
),
|
||||
};
|
||||
|
||||
Row::new([Cell::from(Span::styled(sign, style)), Cell::from(content)])
|
||||
}
|
||||
}
|
||||
|
||||
let cwd = helix_stdx::env::current_working_dir();
|
||||
if !cwd.exists() {
|
||||
cx.editor
|
||||
.set_error("Current working directory does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
let added = cx.editor.theme.get("diff.plus");
|
||||
let modified = cx.editor.theme.get("diff.delta");
|
||||
let conflict = cx.editor.theme.get("diff.delta.conflict");
|
||||
let deleted = cx.editor.theme.get("diff.minus");
|
||||
let renamed = cx.editor.theme.get("diff.delta.moved");
|
||||
|
||||
let picker = Picker::new(
|
||||
Vec::new(),
|
||||
FileChangeData {
|
||||
cwd: cwd.clone(),
|
||||
style_untracked: added,
|
||||
style_modified: modified,
|
||||
style_conflict: conflict,
|
||||
style_deleted: deleted,
|
||||
style_renamed: renamed,
|
||||
},
|
||||
|cx, meta: &FileChange, action| {
|
||||
let path_to_open = meta.path();
|
||||
if let Err(e) = cx.editor.open(path_to_open, action) {
|
||||
let err = if let Some(err) = e.source() {
|
||||
format!("{}", err)
|
||||
} else {
|
||||
format!("unable to open \"{}\"", path_to_open.display())
|
||||
};
|
||||
cx.editor.set_error(err);
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_preview(|_editor, meta| Some((meta.path().to_path_buf().into(), None)));
|
||||
let injector = picker.injector();
|
||||
|
||||
cx.editor
|
||||
.diff_providers
|
||||
.clone()
|
||||
.for_each_changed_file(cwd, move |change| match change {
|
||||
Ok(change) => injector.push(change).is_ok(),
|
||||
Err(err) => {
|
||||
status::report_blocking(err);
|
||||
true
|
||||
}
|
||||
});
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
impl ui::menu::Item for MappableCommand {
|
||||
type Data = ReverseKeymap;
|
||||
|
||||
|
|
|
@ -225,9 +225,10 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
|||
"S" => workspace_symbol_picker,
|
||||
"d" => diagnostics_picker,
|
||||
"D" => workspace_diagnostics_picker,
|
||||
"g" => changed_file_picker,
|
||||
"a" => code_action,
|
||||
"'" => last_picker,
|
||||
"g" => { "Debug (experimental)" sticky=true
|
||||
"G" => { "Debug (experimental)" sticky=true
|
||||
"l" => dap_launch,
|
||||
"r" => dap_restart,
|
||||
"b" => dap_toggle_breakpoint,
|
||||
|
|
|
@ -19,7 +19,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
|
|||
parking_lot = "0.12"
|
||||
arc-swap = { version = "1.7.1" }
|
||||
|
||||
gix = { version = "0.61.0", features = ["attributes"], default-features = false, optional = true }
|
||||
gix = { version = "0.61.0", features = ["attributes", "status"], default-features = false, optional = true }
|
||||
imara-diff = "0.1.5"
|
||||
anyhow = "1"
|
||||
|
||||
|
|
|
@ -5,15 +5,24 @@ use std::io::Read;
|
|||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gix::bstr::ByteSlice;
|
||||
use gix::diff::Rewrites;
|
||||
use gix::dir::entry::Status;
|
||||
use gix::objs::tree::EntryKind;
|
||||
use gix::sec::trust::DefaultForLevel;
|
||||
use gix::status::{
|
||||
index_worktree::iter::Item,
|
||||
plumbing::index_as_worktree::{Change, EntryStatus},
|
||||
UntrackedFiles,
|
||||
};
|
||||
use gix::{Commit, ObjectId, Repository, ThreadSafeRepository};
|
||||
|
||||
use crate::DiffProvider;
|
||||
use crate::{DiffProvider, FileChange};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Git;
|
||||
|
||||
impl Git {
|
||||
|
@ -61,10 +70,77 @@ impl Git {
|
|||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Emulates the result of running `git status` from the command line.
|
||||
fn status(repo: &Repository, f: impl Fn(Result<FileChange>) -> bool) -> Result<()> {
|
||||
let work_dir = repo
|
||||
.work_dir()
|
||||
.ok_or_else(|| anyhow::anyhow!("working tree not found"))?
|
||||
.to_path_buf();
|
||||
|
||||
let status_platform = repo
|
||||
.status(gix::progress::Discard)?
|
||||
// Here we discard the `status.showUntrackedFiles` config, as it makes little sense in
|
||||
// our case to not list new (untracked) files. We could have respected this config
|
||||
// if the default value weren't `Collapsed` though, as this default value would render
|
||||
// the feature unusable to many.
|
||||
.untracked_files(UntrackedFiles::Files)
|
||||
// Turn on file rename detection, which is off by default.
|
||||
.index_worktree_rewrites(Some(Rewrites {
|
||||
copies: None,
|
||||
percentage: Some(0.5),
|
||||
limit: 1000,
|
||||
}));
|
||||
|
||||
// No filtering based on path
|
||||
let empty_patterns = vec![];
|
||||
|
||||
let status_iter = status_platform.into_index_worktree_iter(empty_patterns)?;
|
||||
|
||||
for item in status_iter {
|
||||
let Ok(item) = item.map_err(|err| f(Err(err.into()))) else {
|
||||
continue;
|
||||
};
|
||||
let change = match item {
|
||||
Item::Modification {
|
||||
rela_path, status, ..
|
||||
} => {
|
||||
let path = work_dir.join(rela_path.to_path()?);
|
||||
match status {
|
||||
EntryStatus::Conflict(_) => FileChange::Conflict { path },
|
||||
EntryStatus::Change(Change::Removed) => FileChange::Deleted { path },
|
||||
EntryStatus::Change(Change::Modification { .. }) => {
|
||||
FileChange::Modified { path }
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
Item::DirectoryContents { entry, .. } if entry.status == Status::Untracked => {
|
||||
FileChange::Untracked {
|
||||
path: work_dir.join(entry.rela_path.to_path()?),
|
||||
}
|
||||
}
|
||||
Item::Rewrite {
|
||||
source,
|
||||
dirwalk_entry,
|
||||
..
|
||||
} => FileChange::Renamed {
|
||||
from_path: work_dir.join(source.rela_path().to_path()?),
|
||||
to_path: work_dir.join(dirwalk_entry.rela_path.to_path()?),
|
||||
},
|
||||
_ => continue,
|
||||
};
|
||||
if !f(Ok(change)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DiffProvider for Git {
|
||||
fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
|
||||
impl Git {
|
||||
pub fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
|
||||
debug_assert!(!file.exists() || file.is_file());
|
||||
debug_assert!(file.is_absolute());
|
||||
|
||||
|
@ -95,7 +171,7 @@ impl DiffProvider for Git {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
|
||||
pub fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
|
||||
debug_assert!(!file.exists() || file.is_file());
|
||||
debug_assert!(file.is_absolute());
|
||||
let repo_dir = file.parent().context("file has no parent directory")?;
|
||||
|
@ -112,6 +188,20 @@ impl DiffProvider for Git {
|
|||
|
||||
Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str())))
|
||||
}
|
||||
|
||||
pub fn for_each_changed_file(
|
||||
&self,
|
||||
cwd: &Path,
|
||||
f: impl Fn(Result<FileChange>) -> bool,
|
||||
) -> Result<()> {
|
||||
Self::status(&Self::open_repo(cwd, None)?.to_thread_local(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Git> for DiffProvider {
|
||||
fn from(value: Git) -> Self {
|
||||
DiffProvider::Git(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the object that contains the contents of a file at a specific commit.
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::{fs::File, io::Write, path::Path, process::Command};
|
|||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::{DiffProvider, Git};
|
||||
use crate::Git;
|
||||
|
||||
fn exec_git_cmd(args: &str, git_dir: &Path) {
|
||||
let res = Command::new("git")
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use anyhow::{bail, Result};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use arc_swap::ArcSwap;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(feature = "git")]
|
||||
pub use git::Git;
|
||||
|
@ -14,18 +17,14 @@ mod diff;
|
|||
|
||||
pub use diff::{DiffHandle, Hunk};
|
||||
|
||||
pub trait DiffProvider {
|
||||
/// Returns the data that a diff should be computed against
|
||||
/// if this provider is used.
|
||||
/// The data is returned as raw byte without any decoding or encoding performed
|
||||
/// to ensure all file encodings are handled correctly.
|
||||
fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>>;
|
||||
fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>>;
|
||||
}
|
||||
mod status;
|
||||
|
||||
pub use status::FileChange;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Dummy;
|
||||
impl DiffProvider for Dummy {
|
||||
impl Dummy {
|
||||
fn get_diff_base(&self, _file: &Path) -> Result<Vec<u8>> {
|
||||
bail!("helix was compiled without git support")
|
||||
}
|
||||
|
@ -33,10 +32,25 @@ impl DiffProvider for Dummy {
|
|||
fn get_current_head_name(&self, _file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
|
||||
bail!("helix was compiled without git support")
|
||||
}
|
||||
|
||||
fn for_each_changed_file(
|
||||
&self,
|
||||
_cwd: &Path,
|
||||
_f: impl Fn(Result<FileChange>) -> bool,
|
||||
) -> Result<()> {
|
||||
bail!("helix was compiled without git support")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Dummy> for DiffProvider {
|
||||
fn from(value: Dummy) -> Self {
|
||||
DiffProvider::Dummy(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DiffProviderRegistry {
|
||||
providers: Vec<Box<dyn DiffProvider>>,
|
||||
providers: Vec<DiffProvider>,
|
||||
}
|
||||
|
||||
impl DiffProviderRegistry {
|
||||
|
@ -65,14 +79,71 @@ impl DiffProviderRegistry {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Fire-and-forget changed file iteration. Runs everything in a background task. Keeps
|
||||
/// iteration until `on_change` returns `false`.
|
||||
pub fn for_each_changed_file(
|
||||
self,
|
||||
cwd: PathBuf,
|
||||
f: impl Fn(Result<FileChange>) -> bool + Send + 'static,
|
||||
) {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if self
|
||||
.providers
|
||||
.iter()
|
||||
.find_map(|provider| provider.for_each_changed_file(&cwd, &f).ok())
|
||||
.is_none()
|
||||
{
|
||||
f(Err(anyhow!("no diff provider returns success")));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DiffProviderRegistry {
|
||||
fn default() -> Self {
|
||||
// currently only git is supported
|
||||
// TODO make this configurable when more providers are added
|
||||
let git: Box<dyn DiffProvider> = Box::new(Git);
|
||||
let providers = vec![git];
|
||||
let providers = vec![Git.into()];
|
||||
DiffProviderRegistry { providers }
|
||||
}
|
||||
}
|
||||
|
||||
/// A union type that includes all types that implement [DiffProvider]. We need this type to allow
|
||||
/// cloning [DiffProviderRegistry] as `Clone` cannot be used in trait objects.
|
||||
#[derive(Clone)]
|
||||
pub enum DiffProvider {
|
||||
Dummy(Dummy),
|
||||
#[cfg(feature = "git")]
|
||||
Git(Git),
|
||||
}
|
||||
|
||||
impl DiffProvider {
|
||||
fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
|
||||
match self {
|
||||
Self::Dummy(inner) => inner.get_diff_base(file),
|
||||
#[cfg(feature = "git")]
|
||||
Self::Git(inner) => inner.get_diff_base(file),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
|
||||
match self {
|
||||
Self::Dummy(inner) => inner.get_current_head_name(file),
|
||||
#[cfg(feature = "git")]
|
||||
Self::Git(inner) => inner.get_current_head_name(file),
|
||||
}
|
||||
}
|
||||
|
||||
fn for_each_changed_file(
|
||||
&self,
|
||||
cwd: &Path,
|
||||
f: impl Fn(Result<FileChange>) -> bool,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
Self::Dummy(inner) => inner.for_each_changed_file(cwd, f),
|
||||
#[cfg(feature = "git")]
|
||||
Self::Git(inner) => inner.for_each_changed_file(cwd, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
32
helix-vcs/src/status.rs
Normal file
32
helix-vcs/src/status.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub enum FileChange {
|
||||
Untracked {
|
||||
path: PathBuf,
|
||||
},
|
||||
Modified {
|
||||
path: PathBuf,
|
||||
},
|
||||
Conflict {
|
||||
path: PathBuf,
|
||||
},
|
||||
Deleted {
|
||||
path: PathBuf,
|
||||
},
|
||||
Renamed {
|
||||
from_path: PathBuf,
|
||||
to_path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
impl FileChange {
|
||||
pub fn path(&self) -> &Path {
|
||||
match self {
|
||||
Self::Untracked { path } => path,
|
||||
Self::Modified { path } => path,
|
||||
Self::Conflict { path } => path,
|
||||
Self::Deleted { path } => path,
|
||||
Self::Renamed { to_path, .. } => to_path,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue