Unified · Split
| @@ -1,5 +1,5 @@ | |||
| 1 | **/generated-archives/*.tar.xz filter=lfs diff=lfs merge=lfs -text | 1 | **/generated-archives/*.tar.xz filter=lfs diff=lfs merge=lfs -text |
| 2 | 2 | ||
| 3 | # assure line feeds don't interfere with our working copy hash | 3 | # assure line feeds don't interfere with our working copy hash |
| 4 | **/tests/fixtures/*.sh text crlf=input eol=lf | 4 | **/tests/fixtures/**/*.sh text crlf=input eol=lf |
| 5 | /justfile text crlf=input eol=lf | 5 | /justfile text crlf=input eol=lf |
| @@ -1186,6 +1186,7 @@ dependencies = [ · 1185 unmodified lines | |||
| 1186 | "gix-odb", | 1186 | "gix-odb", |
| 1187 | "gix-pack", | 1187 | "gix-pack", |
| 1188 | "gix-path 0.8.4", | 1188 | "gix-path 0.8.4", |
| 1189 | "gix-pathspec", | ||
| 1189 | "gix-prompt", | 1190 | "gix-prompt", |
| 1190 | "gix-protocol", | 1191 | "gix-protocol", |
| 1191 | "gix-ref 0.34.0", | 1192 | "gix-ref 0.34.0", |
| @@ -1969,6 +1970,7 @@ dependencies = [ · 777 unmodified lines | |||
| 1969 | "bstr", | 1970 | "bstr", |
| 1970 | "gix-attributes 0.16.0", | 1971 | "gix-attributes 0.16.0", |
| 1971 | "gix-glob 0.10.2", | 1972 | "gix-glob 0.10.2", |
| 1973 | "gix-path 0.8.4", | ||
| 1972 | "gix-testtools", | 1974 | "gix-testtools", |
| 1973 | "once_cell", | 1975 | "once_cell", |
| 1974 | "thiserror", | 1976 | "thiserror", |
| @@ -361,8 +361,9 @@ Check out the [performance discussion][gix-traverse-performance] as well. · 360 unmodified lines | |||
| 361 | * [ ] check for match | 361 | * [ ] check for match |
| 362 | 362 | ||
| 363 | ### gix-pathspec | 363 | ### gix-pathspec |
| 364 | * [x] parse | 364 | * [x] parse single |
| 365 | * [ ] matching of paths | 365 | * [ ] parse file line by line (with or without quoting, NUL and LF/CRLF line separation) (see `--pathspec-from-file` and `--pathspec-file-nul`) |
| 366 | * [ ] matching of paths with git-attributes support | ||
| 366 | 367 | ||
| 367 | ### gix-refspec | 368 | ### gix-refspec |
| 368 | * [x] parse | 369 | * [x] parse |
| @@ -1,6 +1,6 @@ | |||
| 1 | use std::collections::HashMap; | 1 | use std::collections::HashMap; |
| 2 | 2 | ||
| 3 | use anyhow::Context; | 3 | use anyhow::{bail, Context}; |
| 4 | use gix::{bstr::ByteSlice, prelude::ObjectIdExt, Progress}; | 4 | use gix::{bstr::ByteSlice, prelude::ObjectIdExt, Progress}; |
| 5 | use rusqlite::{params, OptionalExtension}; | 5 | use rusqlite::{params, OptionalExtension}; |
| 6 | 6 | ||
| @@ -18,10 +18,16 @@ impl query::Engine { · 11 unmodified lines | |||
| 18 | ) -> anyhow::Result<()> { | 18 | ) -> anyhow::Result<()> { |
| 19 | match cmd { | 19 | match cmd { |
| 20 | Command::TracePath { mut spec } => { | 20 | Command::TracePath { mut spec } => { |
| 21 | if let Some(prefix) = self.repo.prefix() { | 21 | let is_excluded = spec.is_excluded(); |
| 22 | spec.apply_prefix(&prefix?); | 22 | let relpath = spec |
| 23 | }; | 23 | .normalize( |
| 24 | let relpath = spec.items().next().expect("spec has at least one item"); | 24 | self.repo.prefix()?.unwrap_or_default().as_ref(), |
| 25 | self.repo.work_dir().unwrap_or_else(|| self.repo.git_dir()), | ||
| 26 | )? | ||
| 27 | .path(); | ||
| 28 | if relpath.is_empty() || is_excluded { | ||
| 29 | bail!("Invalid pathspec {spec} - path must not be empty, not be excluded, and wildcards are taken literally") | ||
| 30 | } | ||
| 25 | let file_id: usize = self | 31 | let file_id: usize = self |
| 26 | .con | 32 | .con |
| 27 | .query_row( | 33 | .query_row( |
| @@ -1,7 +1,7 @@ | |||
| 1 | pub enum Command { | 1 | pub enum Command { |
| 2 | TracePath { | 2 | TracePath { |
| 3 | /// The repo-relative path to the file to trace | 3 | /// The repo-relative path to the file to trace |
| 4 | spec: gix::path::Spec, | 4 | spec: gix::pathspec::Pattern, |
| 5 | }, | 5 | }, |
| 6 | } | 6 | } |
| 7 | 7 | ||
| @@ -18,7 +18,7 @@ pub(crate) mod function { · 17 unmodified lines | |||
| 18 | 18 | ||
| 19 | pub fn query( | 19 | pub fn query( |
| 20 | repo: gix::Repository, | 20 | repo: gix::Repository, |
| 21 | pathspecs: impl Iterator<Item = gix::path::Spec>, | 21 | pathspecs: impl Iterator<Item = gix::pathspec::Pattern>, |
| 22 | mut out: impl io::Write, | 22 | mut out: impl io::Write, |
| 23 | mut err: impl io::Write, | 23 | mut err: impl io::Write, |
| 24 | Options { format, statistics }: Options, | 24 | Options { format, statistics }: Options, |
| @@ -28,28 +28,31 @@ pub(crate) mod function { · 3 unmodified lines | |||
| 28 | } | 28 | } |
| 29 | 29 | ||
| 30 | let mut cache = attributes_cache(&repo)?; | 30 | let mut cache = attributes_cache(&repo)?; |
| 31 | let prefix = repo.prefix().expect("worktree - we have an index by now")?; | ||
| 32 | let mut matches = cache.attribute_matches(); | 31 | let mut matches = cache.attribute_matches(); |
| 32 | // TODO(pathspec): The search is just used as a shortcut to normalization, but one day should be used for an actual search. | ||
| 33 | let search = gix::pathspec::Search::from_specs( | ||
| 34 | pathspecs, | ||
| 35 | repo.prefix()?.as_deref(), | ||
| 36 | repo.work_dir().unwrap_or_else(|| repo.git_dir()), | ||
| 37 | )?; | ||
| 33 | 38 | ||
| 34 | for mut spec in pathspecs { | 39 | for spec in search.into_patterns() { |
| 35 | for path in spec.apply_prefix(&prefix).items() { | 40 | let is_dir = gix::path::from_bstr(spec.path()).metadata().ok().map(|m| m.is_dir()); |
| 36 | let is_dir = gix::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); | 41 | let entry = cache.at_entry(spec.path(), is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; |
| 37 | let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; | ||
| 38 | 42 | ||
| 39 | if !entry.matching_attributes(&mut matches) { | 43 | if !entry.matching_attributes(&mut matches) { |
| 40 | continue; | 44 | continue; |
| 41 | } | 45 | } |
| 42 | for m in matches.iter() { | 46 | for m in matches.iter() { |
| 43 | writeln!( | 47 | writeln!( |
| 44 | out, | 48 | out, |
| 45 | "{}:{}:{}\t{}\t{}", | 49 | "{}:{}:{}\t{}\t{}", |
| 46 | m.location.source.map(Path::to_string_lossy).unwrap_or_default(), | 50 | m.location.source.map(Path::to_string_lossy).unwrap_or_default(), |
| 47 | m.location.sequence_number, | 51 | m.location.sequence_number, |
| 48 | m.pattern, | 52 | m.pattern, |
| 49 | path, | 53 | spec.path(), |
| 50 | m.assignment | 54 | m.assignment |
| 51 | )?; | 55 | )?; |
| 52 | } | ||
| 53 | } | 56 | } |
| 54 | } | 57 | } |
| 55 | 58 | ||
| @@ -18,6 +18,7 @@ pub(crate) mod function { · 17 unmodified lines | |||
| 18 | }; | 18 | }; |
| 19 | 19 | ||
| 20 | use anyhow::{anyhow, bail}; | 20 | use anyhow::{anyhow, bail}; |
| 21 | use gix::bstr::BString; | ||
| 21 | use gix::{attrs::Assignment, odb::FindExt, Progress}; | 22 | use gix::{attrs::Assignment, odb::FindExt, Progress}; |
| 22 | 23 | ||
| 23 | use crate::{ | 24 | use crate::{ |
| @@ -27,7 +28,7 @@ pub(crate) mod function { · 3 unmodified lines | |||
| 27 | 28 | ||
| 28 | pub fn validate_baseline( | 29 | pub fn validate_baseline( |
| 29 | repo: gix::Repository, | 30 | repo: gix::Repository, |
| 30 | pathspecs: Option<impl Iterator<Item = gix::path::Spec> + Send + 'static>, | 31 | paths: Option<impl Iterator<Item = BString> + Send + 'static>, |
| 31 | mut progress: impl Progress + 'static, | 32 | mut progress: impl Progress + 'static, |
| 32 | mut out: impl io::Write, | 33 | mut out: impl io::Write, |
| 33 | mut err: impl io::Write, | 34 | mut err: impl io::Write, |
| @@ -51,7 +52,7 @@ pub(crate) mod function { · 17 unmodified lines | |||
| 51 | ignore = false; | 52 | ignore = false; |
| 52 | } | 53 | } |
| 53 | let mut num_entries = None; | 54 | let mut num_entries = None; |
| 54 | let pathspecs = pathspecs.map_or_else( | 55 | let paths = paths.map_or_else( |
| 55 | { | 56 | { |
| 56 | let repo = repo.clone(); | 57 | let repo = repo.clone(); |
| 57 | let num_entries = &mut num_entries; | 58 | let num_entries = &mut num_entries; |
| @@ -59,18 +60,16 @@ pub(crate) mod function { · 1 unmodified line | |||
| 59 | let index = repo.index_or_load_from_head()?.into_owned(); | 60 | let index = repo.index_or_load_from_head()?.into_owned(); |
| 60 | let (entries, path_backing) = index.into_parts().0.into_entries(); | 61 | let (entries, path_backing) = index.into_parts().0.into_entries(); |
| 61 | *num_entries = Some(entries.len()); | 62 | *num_entries = Some(entries.len()); |
| 62 | let iter = Box::new(entries.into_iter().map(move |e| { | 63 | let iter = Box::new(entries.into_iter().map(move |e| e.path_in(&path_backing).to_owned())); |
| 63 | gix::path::Spec::from_bytes(e.path_in(&path_backing)).expect("each entry path is a valid spec") | 64 | Ok(iter as Box<dyn Iterator<Item = BString> + Send + 'static>) |
| 64 | })); | ||
| 65 | Ok(iter as Box<dyn Iterator<Item = gix::path::Spec> + Send + 'static>) | ||
| 66 | } | 65 | } |
| 67 | }, | 66 | }, |
| 68 | |i| anyhow::Result::Ok(Box::new(i)), | 67 | |paths| anyhow::Result::Ok(Box::new(paths)), |
| 69 | )?; | 68 | )?; |
| 70 | 69 | ||
| 71 | let (tx_base, rx_base) = std::sync::mpsc::channel::<(String, Baseline)>(); | 70 | let (tx_base, rx_base) = std::sync::mpsc::channel::<(String, Baseline)>(); |
| 72 | let feed_attrs = { | 71 | let feed_attrs = { |
| 73 | let (tx, rx) = std::sync::mpsc::sync_channel::<gix::path::Spec>(1); | 72 | let (tx, rx) = std::sync::mpsc::sync_channel::<BString>(100); |
| 74 | std::thread::spawn({ | 73 | std::thread::spawn({ |
| 75 | let path = repo.path().to_owned(); | 74 | let path = repo.path().to_owned(); |
| 76 | let tx_base = tx_base.clone(); | 75 | let tx_base = tx_base.clone(); |
| @@ -89,12 +88,10 @@ pub(crate) mod function { · 12 unmodified lines | |||
| 89 | move || -> anyhow::Result<()> { | 88 | move || -> anyhow::Result<()> { |
| 90 | progress.init(num_entries, gix::progress::count("paths")); | 89 | progress.init(num_entries, gix::progress::count("paths")); |
| 91 | let start = std::time::Instant::now(); | 90 | let start = std::time::Instant::now(); |
| 92 | for spec in rx { | 91 | for path in rx { |
| 93 | progress.inc(); | 92 | progress.inc(); |
| 94 | for path in spec.items() { | 93 | stdin.write_all(&path)?; |
| 95 | stdin.write_all(path.as_ref())?; | 94 | stdin.write_all(b"\n")?; |
| 96 | stdin.write_all(b"\n")?; | ||
| 97 | } | ||
| 98 | } | 95 | } |
| 99 | progress.show_throughput(start); | 96 | progress.show_throughput(start); |
| 100 | Ok(()) | 97 | Ok(()) |
| @@ -123,7 +120,7 @@ pub(crate) mod function { · 22 unmodified lines | |||
| 123 | }) | 120 | }) |
| 124 | .transpose()?; | 121 | .transpose()?; |
| 125 | let feed_excludes = ignore.then(|| { | 122 | let feed_excludes = ignore.then(|| { |
| 126 | let (tx, rx) = std::sync::mpsc::sync_channel::<gix::path::Spec>(1); | 123 | let (tx, rx) = std::sync::mpsc::sync_channel::<BString>(100); |
| 127 | std::thread::spawn({ | 124 | std::thread::spawn({ |
| 128 | let path = work_dir.expect("present if we are here"); | 125 | let path = work_dir.expect("present if we are here"); |
| 129 | let tx_base = tx_base.clone(); | 126 | let tx_base = tx_base.clone(); |
| @@ -142,12 +139,10 @@ pub(crate) mod function { · 12 unmodified lines | |||
| 142 | move || -> anyhow::Result<()> { | 139 | move || -> anyhow::Result<()> { |
| 143 | progress.init(num_entries, gix::progress::count("paths")); | 140 | progress.init(num_entries, gix::progress::count("paths")); |
| 144 | let start = std::time::Instant::now(); | 141 | let start = std::time::Instant::now(); |
| 145 | for spec in rx { | 142 | for path in rx { |
| 146 | progress.inc(); | 143 | progress.inc(); |
| 147 | for path in spec.items() { | 144 | stdin.write_all(path.as_ref())?; |
| 148 | stdin.write_all(path.as_ref())?; | 145 | stdin.write_all(b"\n")?; |
| 149 | stdin.write_all(b"\n")?; | ||
| 150 | } | ||
| 151 | } | 146 | } |
| 152 | progress.show_throughput(start); | 147 | progress.show_throughput(start); |
| 153 | Ok(()) | 148 | Ok(()) |
| @@ -175,12 +170,12 @@ pub(crate) mod function { · 21 unmodified lines | |||
| 175 | drop(tx_base); | 170 | drop(tx_base); |
| 176 | 171 | ||
| 177 | std::thread::spawn(move || { | 172 | std::thread::spawn(move || { |
| 178 | for spec in pathspecs { | 173 | for path in paths { |
| 179 | if feed_attrs.send(spec.clone()).is_err() { | 174 | if feed_attrs.send(path.clone()).is_err() { |
| 180 | break; | 175 | break; |
| 181 | } | 176 | } |
| 182 | if let Some(ch) = feed_excludes.as_ref() { | 177 | if let Some(ch) = feed_excludes.as_ref() { |
| 183 | if ch.send(spec).is_err() { | 178 | if ch.send(path).is_err() { |
| 184 | break; | 179 | break; |
| 185 | } | 180 | } |
| 186 | } | 181 | } |
| @@ -20,7 +20,7 @@ pub mod query { · 19 unmodified lines | |||
| 20 | 20 | ||
| 21 | pub fn query( | 21 | pub fn query( |
| 22 | repo: gix::Repository, | 22 | repo: gix::Repository, |
| 23 | pathspecs: impl Iterator<Item = gix::path::Spec>, | 23 | pathspecs: impl Iterator<Item = gix::pathspec::Pattern>, |
| 24 | mut out: impl io::Write, | 24 | mut out: impl io::Write, |
| 25 | mut err: impl io::Write, | 25 | mut err: impl io::Write, |
| 26 | query::Options { | 26 | query::Options { |
| @@ -41,28 +41,30 @@ pub fn query( · 14 unmodified lines | |||
| 41 | Default::default(), | 41 | Default::default(), |
| 42 | )?; | 42 | )?; |
| 43 | 43 | ||
| 44 | let prefix = repo.prefix().expect("worktree - we have an index by now")?; | 44 | // TODO(pathspec): actually use the search to find items. This looks like `gix` capabilities to put it all together. |
| 45 | let search = gix::pathspec::Search::from_specs( | ||
| 46 | pathspecs, | ||
| 47 | repo.prefix()?.as_deref(), | ||
| 48 | repo.work_dir().unwrap_or_else(|| repo.git_dir()), | ||
| 49 | )?; | ||
| 45 | 50 | ||
| 46 | for mut spec in pathspecs { | 51 | for spec in search.into_patterns() { |
| 47 | for path in spec.apply_prefix(&prefix).items() { | 52 | let path = spec.path(); |
| 48 | // TODO: what about paths that end in /? Pathspec might handle it, it's definitely something git considers | 53 | let is_dir = gix::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); |
| 49 | // even if the directory doesn't exist. Seems to work as long as these are kept in the spec. | 54 | let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; |
| 50 | let is_dir = gix::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); | 55 | let match_ = entry |
| 51 | let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; | 56 | .matching_exclude_pattern() |
| 52 | let match_ = entry | 57 | .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m)); |
| 53 | .matching_exclude_pattern() | 58 | match match_ { |
| 54 | .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m)); | 59 | Some(m) => writeln!( |
| 55 | match match_ { | 60 | out, |
| 56 | Some(m) => writeln!( | 61 | "{}:{}:{}\t{}", |
| 57 | out, | 62 | m.source.map(std::path::Path::to_string_lossy).unwrap_or_default(), |
| 58 | "{}:{}:{}\t{}", | 63 | m.sequence_number, |
| 59 | m.source.map(std::path::Path::to_string_lossy).unwrap_or_default(), | 64 | m.pattern, |
| 60 | m.sequence_number, | 65 | path |
| 61 | m.pattern, | 66 | )?, |
| 62 | path | 67 | None => writeln!(out, "::\t{path}")?, |
| 63 | )?, | ||
| 64 | None => writeln!(out, "::\t{path}")?, | ||
| 65 | } | ||
| 66 | } | 68 | } |
| 67 | } | 69 | } |
| 68 | 70 | ||
| @@ -4,9 +4,10 @@ pub struct Options { · 3 unmodified lines | |||
| 4 | /// If true, also show attributes | 4 | /// If true, also show attributes |
| 5 | pub attributes: Option<Attributes>, | 5 | pub attributes: Option<Attributes>, |
| 6 | pub statistics: bool, | 6 | pub statistics: bool, |
| 7 | pub simple: bool, | ||
| 7 | } | 8 | } |
| 8 | 9 | ||
| 9 | #[derive(Debug)] | 10 | #[derive(Debug, Copy, Clone)] |
| 10 | pub enum Attributes { | 11 | pub enum Attributes { |
| 11 | /// Look at worktree attributes and index as fallback. | 12 | /// Look at worktree attributes and index as fallback. |
| 12 | WorktreeAndIndex, | 13 | WorktreeAndIndex, |
| @@ -15,6 +16,7 @@ pub enum Attributes { · 2 unmodified lines | |||
| 15 | } | 16 | } |
| 16 | 17 | ||
| 17 | pub(crate) mod function { | 18 | pub(crate) mod function { |
| 19 | use std::collections::BTreeSet; | ||
| 18 | use std::{ | 20 | use std::{ |
| 19 | borrow::Cow, | 21 | borrow::Cow, |
| 20 | io::{BufWriter, Write}, | 22 | io::{BufWriter, Write}, |
| @@ -26,9 +28,11 @@ pub(crate) mod function { · 5 unmodified lines | |||
| 26 | 28 | ||
| 27 | pub fn entries( | 29 | pub fn entries( |
| 28 | repo: gix::Repository, | 30 | repo: gix::Repository, |
| 31 | pathspecs: Vec<gix::pathspec::Pattern>, | ||
| 29 | out: impl std::io::Write, | 32 | out: impl std::io::Write, |
| 30 | mut err: impl std::io::Write, | 33 | mut err: impl std::io::Write, |
| 31 | Options { | 34 | Options { |
| 35 | simple, | ||
| 32 | format, | 36 | format, |
| 33 | attributes, | 37 | attributes, |
| 34 | statistics, | 38 | statistics, |
| @@ -37,6 +41,12 @@ pub(crate) mod function { · 2 unmodified lines | |||
| 37 | use crate::OutputFormat::*; | 41 | use crate::OutputFormat::*; |
| 38 | let index = repo.index_or_load_from_head()?; | 42 | let index = repo.index_or_load_from_head()?; |
| 39 | let mut cache = attributes | 43 | let mut cache = attributes |
| 44 | .or_else(|| { | ||
| 45 | pathspecs | ||
| 46 | .iter() | ||
| 47 | .any(|spec| !spec.attributes.is_empty()) | ||
| 48 | .then_some(Attributes::Index) | ||
| 49 | }) | ||
| 40 | .map(|attrs| { | 50 | .map(|attrs| { |
| 41 | repo.attributes( | 51 | repo.attributes( |
| 42 | &index, | 52 | &index, |
| @@ -70,52 +80,111 @@ pub(crate) mod function { · 27 unmodified lines | |||
| 70 | ..Default::default() | 80 | ..Default::default() |
| 71 | }; | 81 | }; |
| 72 | 82 | ||
| 73 | let mut out = BufWriter::new(out); | 83 | let mut out = BufWriter::with_capacity(64 * 1024, out); |
| 74 | #[cfg(feature = "serde")] | 84 | #[cfg(feature = "serde")] |
| 75 | if let Json = format { | 85 | if let Json = format { |
| 76 | out.write_all(b"[\n")?; | 86 | out.write_all(b"[\n")?; |
| 77 | } | 87 | } |
| 78 | let mut entries = index.entries().iter().peekable(); | 88 | let mut search = gix::pathspec::Search::from_specs( |
| 79 | while let Some(entry) = entries.next() { | 89 | pathspecs, |
| 80 | let attrs = cache | 90 | repo.prefix()?.as_deref(), |
| 81 | .as_mut() | 91 | gix::path::realpath(repo.work_dir().unwrap_or_else(|| repo.git_dir()))?.as_ref(), // TODO(pathspec): this setup needs `gix`. |
| 82 | .map(|(attrs, cache)| { | 92 | )?; |
| 83 | cache | 93 | let mut all_attrs = statistics.then(BTreeSet::new); |
| 84 | .at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf)) | 94 | if let Some(entries) = index.prefixed_entries(search.common_prefix()) { |
| 85 | .map(|entry| { | 95 | stats.entries_after_prune = entries.len(); |
| 86 | let is_excluded = entry.is_excluded(); | 96 | let mut entries = entries.iter().peekable(); |
| 87 | stats.excluded += usize::from(is_excluded); | 97 | while let Some(entry) = entries.next() { |
| 88 | let attributes: Vec<_> = { | 98 | let mut last_match = None; |
| 89 | entry.matching_attributes(attrs); | 99 | let attrs = cache |
| 90 | attrs.iter().map(|m| m.assignment.to_owned()).collect() | 100 | .as_mut() |
| 91 | }; | 101 | .and_then(|(attrs, cache)| { |
| 92 | stats.with_attributes += usize::from(!attributes.is_empty()); | 102 | // If the user wants to see assigned attributes, we always have to match. |
| 93 | Attrs { | 103 | attributes.is_some().then(|| { |
| 94 | is_excluded, | 104 | cache |
| 95 | attributes, | 105 | .at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf)) |
| 96 | } | 106 | .map(|entry| { |
| 107 | let is_excluded = entry.is_excluded(); | ||
| 108 | stats.excluded += usize::from(is_excluded); | ||
| 109 | let attributes: Vec<_> = { | ||
| 110 | last_match = Some(entry.matching_attributes(attrs)); | ||
| 111 | attrs.iter().map(|m| m.assignment.to_owned()).collect() | ||
| 112 | }; | ||
| 113 | stats.with_attributes += usize::from(!attributes.is_empty()); | ||
| 114 | stats.max_attributes_per_path = stats.max_attributes_per_path.max(attributes.len()); | ||
| 115 | if let Some(attrs) = all_attrs.as_mut() { | ||
| 116 | attributes.iter().for_each(|attr| { | ||
| 117 | attrs.insert(attr.clone()); | ||
| 118 | }); | ||
| 119 | } | ||
| 120 | Attrs { | ||
| 121 | is_excluded, | ||
| 122 | attributes, | ||
| 123 | } | ||
| 124 | }) | ||
| 97 | }) | 125 | }) |
| 98 | }) | 126 | }) |
| 99 | .transpose()?; | 127 | .transpose()?; |
| 100 | match format { | 128 | |
| 101 | Human => to_human(&mut out, &index, entry, attrs)?, | 129 | // Note that we intentionally ignore `_case` so that we act like git does, attribute matching case is determined |
| 102 | #[cfg(feature = "serde")] | 130 | // by the repository, not the pathspec. |
| 103 | Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?, | 131 | if search |
| 132 | .pattern_matching_relative_path(entry.path(&index), Some(false), |rela_path, _case, is_dir, out| { | ||
| 133 | cache | ||
| 134 | .as_mut() | ||
| 135 | .map(|(attrs, cache)| { | ||
| 136 | match last_match { | ||
| 137 | // The user wants the attributes for display, so the match happened already. | ||
| 138 | Some(matched) => { | ||
| 139 | attrs.copy_into(cache.attributes_collection(), out); | ||
| 140 | matched | ||
| 141 | } | ||
| 142 | // The user doesn't want attributes, so we set the cache position on demand only | ||
| 143 | None => cache | ||
| 144 | .at_entry(rela_path, Some(is_dir), |id, buf| repo.objects.find_blob(id, buf)) | ||
| 145 | .ok() | ||
| 146 | .map(|platform| platform.matching_attributes(out)) | ||
| 147 | .unwrap_or_default(), | ||
| 148 | } | ||
| 149 | }) | ||
| 150 | .unwrap_or_default() | ||
| 151 | }) | ||
| 152 | .map_or(true, |m| m.is_excluded()) | ||
| 153 | { | ||
| 154 | continue; | ||
| 155 | } | ||
| 156 | match format { | ||
| 157 | Human => { | ||
| 158 | if simple { | ||
| 159 | to_human_simple(&mut out, &index, entry, attrs) | ||
| 160 | } else { | ||
| 161 | to_human(&mut out, &index, entry, attrs) | ||
| 162 | }? | ||
| 163 | } | ||
| 164 | #[cfg(feature = "serde")] | ||
| 165 | Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?, | ||
| 166 | } | ||
| 104 | } | 167 | } |
| 105 | } | ||
| 106 | 168 | ||
| 107 | #[cfg(feature = "serde")] | 169 | #[cfg(feature = "serde")] |
| 108 | if format == Json { | 170 | if format == Json { |
| 109 | out.write_all(b"]\n")?; | 171 | out.write_all(b"]\n")?; |
| 110 | out.flush()?; | 172 | out.flush()?; |
| 111 | if statistics { | 173 | if statistics { |
| 112 | serde_json::to_writer_pretty(&mut err, &stats)?; | 174 | serde_json::to_writer_pretty(&mut err, &stats)?; |
| 175 | } | ||
| 176 | } | ||
| 177 | if format == Human && statistics { | ||
| 178 | out.flush()?; | ||
| 179 | stats.cache = cache.map(|c| *c.1.statistics()); | ||
| 180 | writeln!(err, "{stats:#?}")?; | ||
| 181 | if let Some(attrs) = all_attrs.filter(|a| !a.is_empty()) { | ||
| 182 | writeln!(err, "All encountered attributes:")?; | ||
| 183 | for attr in attrs { | ||
| 184 | writeln!(err, "\t{attr}", attr = attr.as_ref())?; | ||
| 185 | } | ||
| 186 | } | ||
| 113 | } | 187 | } |
| 114 | } | ||
| 115 | if format == Human && statistics { | ||
| 116 | out.flush()?; | ||
| 117 | stats.cache = cache.map(|c| *c.1.statistics()); | ||
| 118 | writeln!(err, "{stats:#?}")?; | ||
| 119 | } | 188 | } |
| 120 | Ok(()) | 189 | Ok(()) |
| 121 | } | 190 | } |
| @@ -131,8 +200,10 @@ pub(crate) mod function { · 9 unmodified lines | |||
| 131 | struct Statistics { | 200 | struct Statistics { |
| 132 | #[allow(dead_code)] // Not really dead, but Debug doesn't count for it even though it's crucial. | 201 | #[allow(dead_code)] // Not really dead, but Debug doesn't count for it even though it's crucial. |
| 133 | pub entries: usize, | 202 | pub entries: usize, |
| 203 | pub entries_after_prune: usize, | ||
| 134 | pub excluded: usize, | 204 | pub excluded: usize, |
| 135 | pub with_attributes: usize, | 205 | pub with_attributes: usize, |
| 206 | pub max_attributes_per_path: usize, | ||
| 136 | pub cache: Option<gix::worktree::cache::Statistics>, | 207 | pub cache: Option<gix::worktree::cache::Statistics>, |
| 137 | } | 208 | } |
| 138 | 209 | ||
| @@ -175,6 +246,22 @@ pub(crate) mod function { · 36 unmodified lines | |||
| 175 | Ok(()) | 246 | Ok(()) |
| 176 | } | 247 | } |
| 177 | 248 | ||
| 249 | fn to_human_simple( | ||
| 250 | out: &mut impl std::io::Write, | ||
| 251 | file: &gix::index::File, | ||
| 252 | entry: &gix::index::Entry, | ||
| 253 | attrs: Option<Attrs>, | ||
| 254 | ) -> std::io::Result<()> { | ||
| 255 | match attrs { | ||
| 256 | Some(attrs) => { | ||
| 257 | out.write_all(entry.path(file))?; | ||
| 258 | out.write_all(print_attrs(Some(attrs)).as_bytes()) | ||
| 259 | } | ||
| 260 | None => out.write_all(entry.path(file)), | ||
| 261 | }?; | ||
| 262 | out.write_all(b"\n") | ||
| 263 | } | ||
| 264 | |||
| 178 | fn to_human( | 265 | fn to_human( |
| 179 | out: &mut impl std::io::Write, | 266 | out: &mut impl std::io::Write, |
| 180 | file: &gix::index::File, | 267 | file: &gix::index::File, |
| @@ -198,24 +285,28 @@ pub(crate) mod function { · 17 unmodified lines | |||
| 198 | entry.mode, | 285 | entry.mode, |
| 199 | entry.id, | 286 | entry.id, |
| 200 | entry.path(file), | 287 | entry.path(file), |
| 201 | attrs.map_or(Cow::Borrowed(""), |a| { | 288 | print_attrs(attrs) |
| 202 | let mut buf = String::new(); | ||
| 203 | if a.is_excluded { | ||
| 204 | buf.push_str(" ❌"); | ||
| 205 | } | ||
| 206 | if !a.attributes.is_empty() { | ||
| 207 | buf.push_str(" ("); | ||
| 208 | for assignment in a.attributes { | ||
| 209 | use std::fmt::Write; | ||
| 210 | write!(&mut buf, "{}", assignment.as_ref()).ok(); | ||
| 211 | buf.push_str(", "); | ||
| 212 | } | ||
| 213 | buf.pop(); | ||
| 214 | buf.pop(); | ||
| 215 | buf.push(')'); | ||
| 216 | } | ||
| 217 | buf.into() | ||
| 218 | }) | ||
| 219 | ) | 289 | ) |
| 220 | } | 290 | } |
| 291 | |||
| 292 | fn print_attrs(attrs: Option<Attrs>) -> Cow<'static, str> { | ||
| 293 | attrs.map_or(Cow::Borrowed(""), |a| { | ||
| 294 | let mut buf = String::new(); | ||
| 295 | if a.is_excluded { | ||
| 296 | buf.push_str(" ❌"); | ||
| 297 | } | ||
| 298 | if !a.attributes.is_empty() { | ||
| 299 | buf.push_str(" ("); | ||
| 300 | for assignment in a.attributes { | ||
| 301 | use std::fmt::Write; | ||
| 302 | write!(&mut buf, "{}", assignment.as_ref()).ok(); | ||
| 303 | buf.push_str(", "); | ||
| 304 | } | ||
| 305 | buf.pop(); | ||
| 306 | buf.pop(); | ||
| 307 | buf.push(')'); | ||
| 308 | } | ||
| 309 | buf.into() | ||
| 310 | }) | ||
| 311 | } | ||
| 221 | } | 312 | } |
| @@ -181,7 +181,9 @@ impl Pattern for Attributes { · 180 unmodified lines | |||
| 181 | }) | 181 | }) |
| 182 | .collect() | 182 | .collect() |
| 183 | } | 183 | } |
| 184 | } | ||
| 184 | 185 | ||
| 186 | impl Attributes { | ||
| 185 | fn may_use_glob_pattern(pattern: &gix_glob::Pattern) -> bool { | 187 | fn may_use_glob_pattern(pattern: &gix_glob::Pattern) -> bool { |
| 186 | pattern.mode != macro_mode() | 188 | pattern.mode != macro_mode() |
| 187 | } | 189 | } |
| @@ -229,7 +231,13 @@ fn pattern_matching_relative_path( · 41 unmodified lines | |||
| 229 | Value::Assignments(attrs) => attrs, | 231 | Value::Assignments(attrs) => attrs, |
| 230 | }; | 232 | }; |
| 231 | if out.has_unspecified_attributes(attrs.iter().map(|attr| attr.id)) | 233 | if out.has_unspecified_attributes(attrs.iter().map(|attr| attr.id)) |
| 232 | && pattern.matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) | 234 | && pattern.matches_repo_relative_path( |
| 235 | relative_path, | ||
| 236 | basename_start_pos, | ||
| 237 | is_dir, | ||
| 238 | case, | ||
| 239 | gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, | ||
| 240 | ) | ||
| 233 | { | 241 | { |
| 234 | let all_filled = out.fill_attributes(attrs.iter(), pattern, list.source.as_ref(), *sequence_number); | 242 | let all_filled = out.fill_attributes(attrs.iter(), pattern, list.source.as_ref(), *sequence_number); |
| 235 | if all_filled { | 243 | if all_filled { |
| @@ -73,6 +73,26 @@ impl Outcome { · 72 unmodified lines | |||
| 73 | self.selected.iter().filter(|(_name, id)| id.is_some()).count() | 73 | self.selected.iter().filter(|(_name, id)| id.is_some()).count() |
| 74 | }); | 74 | }); |
| 75 | } | 75 | } |
| 76 | |||
| 77 | /// A performance optimization which allows results from this instance to be efficiently copied over to `dest`. | ||
| 78 | /// For this to work, `collection` must be the one used to initialize our state, and `dest` should not have been initialized | ||
| 79 | /// with any meaningful collection initially, i.e. be empty the first time this method is called. | ||
| 80 | /// | ||
| 81 | /// Note that it's safe to call it multiple times, so that it can be called after this instance was used to store a search result. | ||
| 82 | pub fn copy_into(&self, collection: &MetadataCollection, dest: &mut Self) { | ||
| 83 | dest.initialize(collection); | ||
| 84 | dest.matches_by_id = self.matches_by_id.clone(); | ||
| 85 | if dest.patterns.len() != self.patterns.len() { | ||
| 86 | dest.patterns = self.patterns.clone(); | ||
| 87 | } | ||
| 88 | if dest.assignments.len() != self.assignments.len() { | ||
| 89 | dest.assignments = self.assignments.clone(); | ||
| 90 | } | ||
| 91 | if dest.source_paths.len() != self.source_paths.len() { | ||
| 92 | dest.source_paths = self.source_paths.clone(); | ||
| 93 | } | ||
| 94 | dest.remaining = self.remaining; | ||
| 95 | } | ||
| 76 | } | 96 | } |
| 77 | 97 | ||
| 78 | /// Access | 98 | /// Access |
| @@ -223,6 +243,29 @@ impl Outcome { · 144 unmodified lines | |||
| 223 | } | 243 | } |
| 224 | } | 244 | } |
| 225 | 245 | ||
| 246 | impl std::fmt::Debug for Outcome { | ||
| 247 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 248 | struct AsDisplay<'a>(&'a dyn std::fmt::Display); | ||
| 249 | impl std::fmt::Debug for AsDisplay<'_> { | ||
| 250 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 251 | self.0.fmt(f) | ||
| 252 | } | ||
| 253 | } | ||
| 254 | |||
| 255 | let mut dbg = f.debug_tuple("Outcome"); | ||
| 256 | if self.selected.is_empty() { | ||
| 257 | for match_ in self.iter() { | ||
| 258 | dbg.field(&AsDisplay(&match_.assignment)); | ||
| 259 | } | ||
| 260 | } else { | ||
| 261 | for match_ in self.iter_selected() { | ||
| 262 | dbg.field(&AsDisplay(&match_.assignment)); | ||
| 263 | } | ||
| 264 | } | ||
| 265 | dbg.finish() | ||
| 266 | } | ||
| 267 | } | ||
| 268 | |||
| 226 | /// Mutation | 269 | /// Mutation |
| 227 | impl MetadataCollection { | 270 | impl MetadataCollection { |
| 228 | /// Assign order ids to each attribute either in macros (along with macros themselves) or attributes of patterns, and store | 271 | /// Assign order ids to each attribute either in macros (along with macros themselves) or attributes of patterns, and store |
| @@ -21,6 +21,9 @@ impl<T> RefMap<T> · 20 unmodified lines | |||
| 21 | where | 21 | where |
| 22 | T: Hash + Clone, | 22 | T: Hash + Clone, |
| 23 | { | 23 | { |
| 24 | pub(crate) fn len(&self) -> usize { | ||
| 25 | self.0.len() | ||
| 26 | } | ||
| 24 | pub(crate) fn insert(&mut self, value: &T) -> RefMapKey { | 27 | pub(crate) fn insert(&mut self, value: &T) -> RefMapKey { |
| 25 | let mut s = DefaultHasher::new(); | 28 | let mut s = DefaultHasher::new(); |
| 26 | value.hash(&mut s); | 29 | value.hash(&mut s); |
| @@ -141,8 +141,10 @@ fn all_attributes_are_listed_in_declaration_order() -> crate::Result { · 140 unmodified lines | |||
| 141 | true, /* use macros */ | 141 | true, /* use macros */ |
| 142 | )?; | 142 | )?; |
| 143 | 143 | ||
| 144 | let mut out = gix_attributes::search::Outcome::default(); | 144 | let mut out = Outcome::default(); |
| 145 | out.initialize(&collection); | 145 | out.initialize(&collection); |
| 146 | let mut alt = Outcome::default(); | ||
| 147 | alt.initialize(&collection); | ||
| 146 | 148 | ||
| 147 | let mut orders = collection | 149 | let mut orders = collection |
| 148 | .iter() | 150 | .iter() |
| @@ -206,10 +208,13 @@ fn all_attributes_are_listed_in_declaration_order() -> crate::Result { · 57 unmodified lines | |||
| 206 | assert_references(&out); | 208 | assert_references(&out); |
| 207 | let actual: Vec<_> = out.iter().map(|m| m.assignment).collect(); | 209 | let actual: Vec<_> = out.iter().map(|m| m.assignment).collect(); |
| 208 | assert_eq!( | 210 | assert_eq!( |
| 209 | by_name(actual), | 211 | by_name(actual.clone()), |
| 210 | by_name(expected), | 212 | by_name(expected), |
| 211 | "{rela_path}: the order of everything matches perfectly" | 213 | "{rela_path}: the order of everything matches perfectly" |
| 212 | ); | 214 | ); |
| 215 | out.copy_into(&collection, &mut alt); | ||
| 216 | let alt_actual: Vec<_> = alt.iter().map(|m| m.assignment).collect(); | ||
| 217 | assert_eq!(alt_actual, actual); | ||
| 213 | } | 218 | } |
| 214 | assert_eq!( | 219 | assert_eq!( |
| 215 | out.iter().count(), | 220 | out.iter().count(), |
| @@ -234,8 +239,10 @@ fn given_attributes_are_made_available_in_given_order() -> crate::Result { · 18 unmodified lines | |||
| 234 | true, /* use macros */ | 239 | true, /* use macros */ |
| 235 | )?; | 240 | )?; |
| 236 | 241 | ||
| 237 | let mut out = gix_attributes::search::Outcome::default(); | 242 | let mut out = Outcome::default(); |
| 238 | out.initialize_with_selection(&collection, ["my-binary", "recursive", "unspecified"]); | 243 | out.initialize_with_selection(&collection, ["my-binary", "recursive", "unspecified"]); |
| 244 | let mut alt = Outcome::default(); | ||
| 245 | alt.initialize_with_selection(&collection, ["my-binary", "unspecified"]); | ||
| 239 | 246 | ||
| 240 | for (rela_path, expected) in (baseline::Expectations { lines: input.lines() }) { | 247 | for (rela_path, expected) in (baseline::Expectations { lines: input.lines() }) { |
| 241 | out.reset(); | 248 | out.reset(); |
| @@ -246,6 +253,10 @@ fn given_attributes_are_made_available_in_given_order() -> crate::Result { · 4 unmodified lines | |||
| 246 | actual, expected, | 253 | actual, expected, |
| 247 | "{rela_path}: the order of everything matches perfectly" | 254 | "{rela_path}: the order of everything matches perfectly" |
| 248 | ); | 255 | ); |
| 256 | out.copy_into(&collection, &mut alt); | ||
| 257 | let alt_actual: Vec<_> = alt.iter_selected().map(|m| m.assignment).collect(); | ||
| 258 | assert_eq!(alt_actual[0], actual[0]); | ||
| 259 | assert_eq!(alt_actual[1], actual[2]); | ||
| 249 | } | 260 | } |
| 250 | assert_eq!( | 261 | assert_eq!( |
| 251 | out.iter().count(), | 262 | out.iter().count(), |
| @@ -255,6 +266,15 @@ fn given_attributes_are_made_available_in_given_order() -> crate::Result { · 3 unmodified lines | |||
| 255 | Ok(()) | 266 | Ok(()) |
| 256 | } | 267 | } |
| 257 | 268 | ||
| 269 | #[test] | ||
| 270 | fn size_of_outcome() { | ||
| 271 | assert_eq!( | ||
| 272 | std::mem::size_of::<Outcome>(), | ||
| 273 | 904, | ||
| 274 | "it's quite big, shouldn't change without us noticing" | ||
| 275 | ) | ||
| 276 | } | ||
| 277 | |||
| 258 | fn by_name(assignments: Vec<AssignmentRef>) -> BTreeMap<NameRef, StateRef> { | 278 | fn by_name(assignments: Vec<AssignmentRef>) -> BTreeMap<NameRef, StateRef> { |
| 259 | assignments.into_iter().map(|a| (a.name, a.state)).collect() | 279 | assignments.into_iter().map(|a| (a.name, a.state)).collect() |
| 260 | } | 280 | } |
| @@ -6,19 +6,23 @@ use crate::{pattern, pattern::Mode}; · 5 unmodified lines | |||
| 6 | /// A sloppy parser that performs only the most basic checks, providing additional information | 6 | /// A sloppy parser that performs only the most basic checks, providing additional information |
| 7 | /// using `pattern::Mode` flags. | 7 | /// using `pattern::Mode` flags. |
| 8 | /// | 8 | /// |
| 9 | /// If `may_alter` is `false`, we won't parse leading `!` or its escaped form. | ||
| 10 | /// | ||
| 9 | /// Returns `(pattern, mode, no_wildcard_len)` | 11 | /// Returns `(pattern, mode, no_wildcard_len)` |
| 10 | pub fn pattern(mut pat: &[u8]) -> Option<(&[u8], pattern::Mode, Option<usize>)> { | 12 | pub fn pattern(mut pat: &[u8], may_alter: bool) -> Option<(&[u8], pattern::Mode, Option<usize>)> { |
| 11 | let mut mode = Mode::empty(); | 13 | let mut mode = Mode::empty(); |
| 12 | if pat.is_empty() { | 14 | if pat.is_empty() { |
| 13 | return None; | 15 | return None; |
| 14 | }; | 16 | }; |
| 15 | if pat.first() == Some(&b'!') { | 17 | if may_alter { |
| 16 | mode |= Mode::NEGATIVE; | 18 | if pat.first() == Some(&b'!') { |
| 17 | pat = &pat[1..]; | 19 | mode |= Mode::NEGATIVE; |
| 18 | } else if pat.first() == Some(&b'\\') { | ||
| 19 | let second = pat.get(1); | ||
| 20 | if second == Some(&b'!') || second == Some(&b'#') { | ||
| 21 | pat = &pat[1..]; | 20 | pat = &pat[1..]; |
| 21 | } else if pat.first() == Some(&b'\\') { | ||
| 22 | let second = pat.get(1); | ||
| 23 | if second == Some(&b'!') || second == Some(&b'#') { | ||
| 24 | pat = &pat[1..]; | ||
| 25 | } | ||
| 22 | } | 26 | } |
| 23 | } | 27 | } |
| 24 | if pat.iter().all(u8::is_ascii_whitespace) { | 28 | if pat.iter().all(u8::is_ascii_whitespace) { |
| @@ -44,7 +44,18 @@ pub enum Case { · 43 unmodified lines | |||
| 44 | impl Pattern { | 44 | impl Pattern { |
| 45 | /// Parse the given `text` as pattern, or return `None` if `text` was empty. | 45 | /// Parse the given `text` as pattern, or return `None` if `text` was empty. |
| 46 | pub fn from_bytes(text: &[u8]) -> Option<Self> { | 46 | pub fn from_bytes(text: &[u8]) -> Option<Self> { |
| 47 | crate::parse::pattern(text).map(|(text, mode, first_wildcard_pos)| Pattern { | 47 | crate::parse::pattern(text, true).map(|(text, mode, first_wildcard_pos)| Pattern { |
| 48 | text: text.into(), | ||
| 49 | mode, | ||
| 50 | first_wildcard_pos, | ||
| 51 | }) | ||
| 52 | } | ||
| 53 | |||
| 54 | /// Parse the given `text` as pattern without supporting leading `!` or `\\!` , or return `None` if `text` was empty. | ||
| 55 | /// | ||
| 56 | /// This assures that `text` remains entirely unaltered, but removes built-in support for negation as well. | ||
| 57 | pub fn from_bytes_without_negation(text: &[u8]) -> Option<Self> { | ||
| 58 | crate::parse::pattern(text, false).map(|(text, mode, first_wildcard_pos)| Pattern { | ||
| 48 | text: text.into(), | 59 | text: text.into(), |
| 49 | mode, | 60 | mode, |
| 50 | first_wildcard_pos, | 61 | first_wildcard_pos, |
| @@ -65,30 +76,37 @@ impl Pattern { · 14 unmodified lines | |||
| 65 | /// We may take various shortcuts which is when `basename_start_pos` and `is_dir` come into play. | 76 | /// We may take various shortcuts which is when `basename_start_pos` and `is_dir` come into play. |
| 66 | /// `basename_start_pos` is the index at which the `path`'s basename starts. | 77 | /// `basename_start_pos` is the index at which the `path`'s basename starts. |
| 67 | /// | 78 | /// |
| 68 | /// Lastly, `case` folding can be configured as well. | 79 | /// `case` folding can be configured as well. |
| 80 | /// `mode` is used to control how [`crate::wildmatch()`] should operate. | ||
| 69 | pub fn matches_repo_relative_path<'a>( | 81 | pub fn matches_repo_relative_path<'a>( |
| 70 | &self, | 82 | &self, |
| 71 | path: impl Into<&'a BStr>, | 83 | path: impl Into<&'a BStr>, |
| 72 | basename_start_pos: Option<usize>, | 84 | basename_start_pos: Option<usize>, |
| 73 | is_dir: Option<bool>, | 85 | is_dir: Option<bool>, |
| 74 | case: Case, | 86 | case: Case, |
| 87 | mode: wildmatch::Mode, | ||
| 75 | ) -> bool { | 88 | ) -> bool { |
| 76 | let is_dir = is_dir.unwrap_or(false); | 89 | let is_dir = is_dir.unwrap_or(false); |
| 77 | if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) { | 90 | if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) { |
| 78 | return false; | 91 | return false; |
| 79 | } | 92 | } |
| 80 | 93 | ||
| 81 | let flags = wildmatch::Mode::NO_MATCH_SLASH_LITERAL | 94 | let flags = mode |
| 82 | | match case { | 95 | | match case { |
| 83 | Case::Fold => wildmatch::Mode::IGNORE_CASE, | 96 | Case::Fold => wildmatch::Mode::IGNORE_CASE, |
| 84 | Case::Sensitive => wildmatch::Mode::empty(), | 97 | Case::Sensitive => wildmatch::Mode::empty(), |
| 85 | }; | 98 | }; |
| 86 | let path = path.into(); | 99 | let path = path.into(); |
| 87 | debug_assert_eq!( | 100 | #[cfg(debug_assertions)] |
| 88 | basename_start_pos, | 101 | { |
| 89 | path.rfind_byte(b'/').map(|p| p + 1), | 102 | if basename_start_pos.is_some() { |
| 90 | "BUG: invalid cached basename_start_pos provided" | 103 | debug_assert_eq!( |
| 91 | ); | 104 | basename_start_pos, |
| 105 | path.rfind_byte(b'/').map(|p| p + 1), | ||
| 106 | "BUG: invalid cached basename_start_pos provided" | ||
| 107 | ); | ||
| 108 | } | ||
| 109 | } | ||
| 92 | debug_assert!(!path.starts_with(b"/"), "input path must be relative"); | 110 | debug_assert!(!path.starts_with(b"/"), "input path must be relative"); |
| 93 | 111 | ||
| 94 | if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) { | 112 | if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) { |
| @@ -16,9 +16,6 @@ pub trait Pattern: Clone + PartialEq + Eq + std::fmt::Debug + std::hash::Hash + · 15 unmodified lines | |||
| 16 | 16 | ||
| 17 | /// Parse all patterns in `bytes` line by line, ignoring lines with errors, and collect them. | 17 | /// Parse all patterns in `bytes` line by line, ignoring lines with errors, and collect them. |
| 18 | fn bytes_to_patterns(bytes: &[u8], source: &Path) -> Vec<pattern::Mapping<Self::Value>>; | 18 | fn bytes_to_patterns(bytes: &[u8], source: &Path) -> Vec<pattern::Mapping<Self::Value>>; |
| 19 | |||
| 20 | /// Returns true if the given pattern may be used for matching. | ||
| 21 | fn may_use_glob_pattern(pattern: &crate::Pattern) -> bool; | ||
| 22 | } | 19 | } |
| 23 | 20 | ||
| 24 | /// Add the given file at `source` if it exists, otherwise do nothing. | 21 | /// Add the given file at `source` if it exists, otherwise do nothing. |
| @@ -66,11 +66,21 @@ fn leading_exclamation_mark_negates_pattern() { · 65 unmodified lines | |||
| 66 | gix_glob::parse(b"!hello"), | 66 | gix_glob::parse(b"!hello"), |
| 67 | pat("hello", Mode::NEGATIVE | Mode::NO_SUB_DIR, None) | 67 | pat("hello", Mode::NEGATIVE | Mode::NO_SUB_DIR, None) |
| 68 | ); | 68 | ); |
| 69 | assert_eq!( | ||
| 70 | gix_glob::Pattern::from_bytes_without_negation(b"!hello"), | ||
| 71 | pat("!hello", Mode::NO_SUB_DIR, None), | ||
| 72 | "negation can be disabled entirely" | ||
| 73 | ); | ||
| 69 | } | 74 | } |
| 70 | 75 | ||
| 71 | #[test] | 76 | #[test] |
| 72 | fn leading_exclamation_marks_can_be_escaped_with_backslash() { | 77 | fn leading_exclamation_marks_can_be_escaped_with_backslash() { |
| 73 | assert_eq!(gix_glob::parse(br"\!hello"), pat("!hello", Mode::NO_SUB_DIR, None)); | 78 | assert_eq!(gix_glob::parse(br"\!hello"), pat("!hello", Mode::NO_SUB_DIR, None)); |
| 79 | assert_eq!( | ||
| 80 | gix_glob::Pattern::from_bytes_without_negation(br"\!hello"), | ||
| 81 | pat("\\!hello", Mode::NO_SUB_DIR, Some(0)), | ||
| 82 | "negation can be disabled entirely, leaving escapes in place" | ||
| 83 | ); | ||
| 74 | } | 84 | } |
| 75 | 85 | ||
| 76 | #[test] | 86 | #[test] |
| @@ -68,7 +68,13 @@ fn compare_baseline_with_ours() { · 67 unmodified lines | |||
| 68 | ); | 68 | ); |
| 69 | match std::panic::catch_unwind(|| { | 69 | match std::panic::catch_unwind(|| { |
| 70 | let pattern = pat(pattern); | 70 | let pattern = pat(pattern); |
| 71 | pattern.matches_repo_relative_path(value, basename_start_pos(value), None, *case) | 71 | pattern.matches_repo_relative_path( |
| 72 | value, | ||
| 73 | basename_start_pos(value), | ||
| 74 | None, | ||
| 75 | *case, | ||
| 76 | gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, | ||
| 77 | ) | ||
| 72 | }) { | 78 | }) { |
| 73 | Ok(actual_match) => { | 79 | Ok(actual_match) => { |
| 74 | if actual_match == is_match { | 80 | if actual_match == is_match { |
| @@ -105,11 +111,23 @@ fn non_dirs_for_must_be_dir_patterns_are_ignored() { · 30 unmodified lines | |||
| 105 | ); | 111 | ); |
| 106 | let path = "hello"; | 112 | let path = "hello"; |
| 107 | assert!( | 113 | assert!( |
| 108 | !pattern.matches_repo_relative_path(path, None, false.into() /* is-dir */, Case::Sensitive), | 114 | !pattern.matches_repo_relative_path( |
| 115 | path, | ||
| 116 | None, | ||
| 117 | false.into(), /* is-dir */ | ||
| 118 | Case::Sensitive, | ||
| 119 | gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL | ||
| 120 | ), | ||
| 109 | "non-dirs never match a dir pattern" | 121 | "non-dirs never match a dir pattern" |
| 110 | ); | 122 | ); |
| 111 | assert!( | 123 | assert!( |
| 112 | pattern.matches_repo_relative_path(path, None, true.into() /* is-dir */, Case::Sensitive), | 124 | pattern.matches_repo_relative_path( |
| 125 | path, | ||
| 126 | None, | ||
| 127 | true.into(), /* is-dir */ | ||
| 128 | Case::Sensitive, | ||
| 129 | gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL | ||
| 130 | ), | ||
| 113 | "dirs can match a dir pattern with the normal rules" | 131 | "dirs can match a dir pattern with the normal rules" |
| 114 | ); | 132 | ); |
| 115 | } | 133 | } |
| @@ -317,7 +335,13 @@ fn match_file<'a>(pattern: &gix_glob::Pattern, path: impl Into<&'a BStr>, case: · 201 unmodified lines | |||
| 317 | 335 | ||
| 318 | fn match_path<'a>(pattern: &gix_glob::Pattern, path: impl Into<&'a BStr>, is_dir: Option<bool>, case: Case) -> bool { | 336 | fn match_path<'a>(pattern: &gix_glob::Pattern, path: impl Into<&'a BStr>, is_dir: Option<bool>, case: Case) -> bool { |
| 319 | let path = path.into(); | 337 | let path = path.into(); |
| 320 | pattern.matches_repo_relative_path(path, basename_start_pos(path), is_dir, case) | 338 | pattern.matches_repo_relative_path( |
| 339 | path, | ||
| 340 | basename_start_pos(path), | ||
| 341 | is_dir, | ||
| 342 | case, | ||
| 343 | gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, | ||
| 344 | ) | ||
| 321 | } | 345 | } |
| 322 | 346 | ||
| 323 | fn basename_start_pos(value: &BStr) -> Option<usize> { | 347 | fn basename_start_pos(value: &BStr) -> Option<usize> { |
| @@ -18,10 +18,6 @@ mod list { · 17 unmodified lines | |||
| 18 | fn bytes_to_patterns(_bytes: &[u8], _source: &Path) -> Vec<Mapping<Self::Value>> { | 18 | fn bytes_to_patterns(_bytes: &[u8], _source: &Path) -> Vec<Mapping<Self::Value>> { |
| 19 | vec![] | 19 | vec![] |
| 20 | } | 20 | } |
| 21 | |||
| 22 | fn may_use_glob_pattern(_pattern: &gix_glob::Pattern) -> bool { | ||
| 23 | unreachable!("won't be called") | ||
| 24 | } | ||
| 25 | } | 21 | } |
| 26 | 22 | ||
| 27 | #[test] | 23 | #[test] |
| @@ -367,7 +367,13 @@ impl Display for MatchResult { · 366 unmodified lines | |||
| 367 | } | 367 | } |
| 368 | 368 | ||
| 369 | fn match_file_path(pattern: &gix_glob::Pattern, path: &str, case: Case) -> bool { | 369 | fn match_file_path(pattern: &gix_glob::Pattern, path: &str, case: Case) -> bool { |
| 370 | pattern.matches_repo_relative_path(path, basename_of(path), false.into() /* is_dir */, case) | 370 | pattern.matches_repo_relative_path( |
| 371 | path, | ||
| 372 | basename_of(path), | ||
| 373 | false.into(), /* is_dir */ | ||
| 374 | case, | ||
| 375 | gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, | ||
| 376 | ) | ||
| 371 | } | 377 | } |
| 372 | fn basename_of(path: &str) -> Option<usize> { | 378 | fn basename_of(path: &str) -> Option<usize> { |
| 373 | path.rfind('/').map(|pos| pos + 1) | 379 | path.rfind('/').map(|pos| pos + 1) |
| @@ -35,10 +35,6 @@ impl Pattern for Ignore { · 34 unmodified lines | |||
| 35 | }) | 35 | }) |
| 36 | .collect() | 36 | .collect() |
| 37 | } | 37 | } |
| 38 | |||
| 39 | fn may_use_glob_pattern(_pattern: &gix_glob::Pattern) -> bool { | ||
| 40 | true | ||
| 41 | } | ||
| 42 | } | 38 | } |
| 43 | 39 | ||
| 44 | /// Instantiation of a search for ignore patterns. | 40 | /// Instantiation of a search for ignore patterns. |
| @@ -115,25 +111,27 @@ pub fn pattern_matching_relative_path<'a>( · 70 unmodified lines | |||
| 115 | ) -> Option<Match<'a>> { | 111 | ) -> Option<Match<'a>> { |
| 116 | let (relative_path, basename_start_pos) = | 112 | let (relative_path, basename_start_pos) = |
| 117 | list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case)?; | 113 | list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case)?; |
| 118 | list.patterns | 114 | list.patterns.iter().rev().find_map( |
| 119 | .iter() | 115 | |pattern::Mapping { |
| 120 | .rev() | 116 | pattern, |
| 121 | .filter(|pm| Ignore::may_use_glob_pattern(&pm.pattern)) | 117 | value: (), |
| 122 | .find_map( | 118 | sequence_number, |
| 123 | |pattern::Mapping { | 119 | }| { |
| 124 | pattern, | 120 | pattern |
| 125 | value: (), | 121 | .matches_repo_relative_path( |
| 126 | sequence_number, | 122 | relative_path, |
| 127 | }| { | 123 | basename_start_pos, |
| 128 | pattern | 124 | is_dir, |
| 129 | .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) | 125 | case, |
| 130 | .then_some(Match { | 126 | gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, |
| 131 | pattern, | 127 | ) |
| 132 | source: list.source.as_deref(), | 128 | .then_some(Match { |
| 133 | sequence_number: *sequence_number, | 129 | pattern, |
| 134 | }) | 130 | source: list.source.as_deref(), |
| 135 | }, | 131 | sequence_number: *sequence_number, |
| 136 | ) | 132 | }) |
| 133 | }, | ||
| 134 | ) | ||
| 137 | } | 135 | } |
| 138 | 136 | ||
| 139 | /// Like [`pattern_matching_relative_path()`], but returns an index to the pattern | 137 | /// Like [`pattern_matching_relative_path()`], but returns an index to the pattern |
| @@ -147,16 +145,17 @@ pub fn pattern_idx_matching_relative_path( · 7 unmodified lines | |||
| 147 | ) -> Option<usize> { | 145 | ) -> Option<usize> { |
| 148 | let (relative_path, basename_start_pos) = | 146 | let (relative_path, basename_start_pos) = |
| 149 | list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case)?; | 147 | list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case)?; |
| 150 | list.patterns | 148 | list.patterns.iter().enumerate().rev().find_map(|(idx, pm)| { |
| 151 | .iter() | 149 | pm.pattern |
| 152 | .enumerate() | 150 | .matches_repo_relative_path( |
| 153 | .rev() | 151 | relative_path, |
| 154 | .filter(|(_, pm)| Ignore::may_use_glob_pattern(&pm.pattern)) | 152 | basename_start_pos, |
| 155 | .find_map(|(idx, pm)| { | 153 | is_dir, |
| 156 | pm.pattern | 154 | case, |
| 157 | .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) | 155 | gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, |
| 158 | .then_some(idx) | 156 | ) |
| 159 | }) | 157 | .then_some(idx) |
| 158 | }) | ||
| 160 | } | 159 | } |
| 161 | 160 | ||
| 162 | /// Matching of ignore patterns. | 161 | /// Matching of ignore patterns. |
| @@ -158,6 +158,34 @@ impl State { · 157 unmodified lines | |||
| 158 | Some(&self.entries[idx]) | 158 | Some(&self.entries[idx]) |
| 159 | } | 159 | } |
| 160 | 160 | ||
| 161 | /// Return the slice of entries which all share the same `prefix`, or `None` if there isn't a single such entry. | ||
| 162 | pub fn prefixed_entries(&self, prefix: &BStr) -> Option<&[Entry]> { | ||
| 163 | if prefix.is_empty() { | ||
| 164 | return Some(self.entries()); | ||
| 165 | } | ||
| 166 | let prefix_len = prefix.len(); | ||
| 167 | let mut low = self | ||
| 168 | .entries | ||
| 169 | .partition_point(|e| e.path(self).get(..prefix_len).map_or(true, |p| p < prefix)); | ||
| 170 | let mut high = low | ||
| 171 | + self.entries[low..].partition_point(|e| e.path(self).get(..prefix_len).map_or(false, |p| p <= prefix)); | ||
| 172 | |||
| 173 | let low_entry = &self.entries[low]; | ||
| 174 | if low_entry.stage() != 0 { | ||
| 175 | low = self | ||
| 176 | .entry_index_by_idx_and_stage(low_entry.path(self), low, 0, low_entry.stage().cmp(&0)) | ||
| 177 | .unwrap_or(low); | ||
| 178 | } | ||
| 179 | if let Some(high_entry) = self.entries.get(high) { | ||
| 180 | if low_entry.stage() != 2 { | ||
| 181 | high = self | ||
| 182 | .entry_index_by_idx_and_stage(high_entry.path(self), high, 2, high_entry.stage().cmp(&2)) | ||
| 183 | .unwrap_or(high); | ||
| 184 | } | ||
| 185 | } | ||
| 186 | (low != high).then_some(low..high).map(|range| &self.entries[range]) | ||
| 187 | } | ||
| 188 | |||
| 161 | /// Return the entry at `idx` or _panic_ if the index is out of bounds. | 189 | /// Return the entry at `idx` or _panic_ if the index is out of bounds. |
| 162 | /// | 190 | /// |
| 163 | /// The `idx` is typically returned by [`entry_by_path_and_stage()`][State::entry_by_path_and_stage()]. | 191 | /// The `idx` is typically returned by [`entry_by_path_and_stage()`][State::entry_by_path_and_stage()]. |
| @@ -28,7 +28,17 @@ fn entry_by_path_with_conflicting_file() { · 27 unmodified lines | |||
| 28 | file.entry_by_path("file".into()).expect("found").stage(), | 28 | file.entry_by_path("file".into()).expect("found").stage(), |
| 29 | 2, | 29 | 2, |
| 30 | "we always find our stage while in a merge" | 30 | "we always find our stage while in a merge" |
| 31 | ) | 31 | ); |
| 32 | assert_eq!( | ||
| 33 | file.prefixed_entries("fil".into()).expect("present"), | ||
| 34 | file.entries(), | ||
| 35 | "it's possible to get the entire range" | ||
| 36 | ); | ||
| 37 | assert_eq!( | ||
| 38 | file.prefixed_entries("".into()).expect("present"), | ||
| 39 | file.entries(), | ||
| 40 | "empty prefix matches all" | ||
| 41 | ); | ||
| 32 | } | 42 | } |
| 33 | 43 | ||
| 34 | #[test] | 44 | #[test] |
| @@ -65,4 +75,31 @@ fn sort_entries() { · 30 unmodified lines | |||
| 65 | new_entry_path, | 75 | new_entry_path, |
| 66 | "we can find the correct entry now" | 76 | "we can find the correct entry now" |
| 67 | ); | 77 | ); |
| 78 | |||
| 79 | check_prefix(&file, "a", &["a", "an initially incorrectly ordered entry"]); | ||
| 80 | check_prefix( | ||
| 81 | &file, | ||
| 82 | "d", | ||
| 83 | &["d/a", "d/b", "d/c", "d/last/123", "d/last/34", "d/last/6"], | ||
| 84 | ); | ||
| 85 | check_prefix( | ||
| 86 | &file, | ||
| 87 | "d/", | ||
| 88 | &["d/a", "d/b", "d/c", "d/last/123", "d/last/34", "d/last/6"], | ||
| 89 | ); | ||
| 90 | check_prefix(&file, "d/last", &["d/last/123", "d/last/34", "d/last/6"]); | ||
| 91 | check_prefix(&file, "d/las", &["d/last/123", "d/last/34", "d/last/6"]); | ||
| 92 | check_prefix(&file, "x", &["x"]); | ||
| 93 | } | ||
| 94 | |||
| 95 | fn check_prefix(index: &gix_index::State, prefix: &str, expected: &[&str]) { | ||
| 96 | assert_eq!( | ||
| 97 | index | ||
| 98 | .prefixed_entries(prefix.into()) | ||
| 99 | .expect("present") | ||
| 100 | .iter() | ||
| 101 | .map(|e| e.path(index)) | ||
| 102 | .collect::<Vec<_>>(), | ||
| 103 | expected | ||
| 104 | ); | ||
| 68 | } | 105 | } |
| @@ -239,6 +239,7 @@ pub fn to_windows_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr · 238 unmodified lines | |||
| 239 | /// Resolve relative components virtually without accessing the file system, e.g. turn `a/./b/c/.././..` into `a`, | 239 | /// Resolve relative components virtually without accessing the file system, e.g. turn `a/./b/c/.././..` into `a`, |
| 240 | /// without keeping intermediate `..` and `/a/../b/..` becomes `/`. | 240 | /// without keeping intermediate `..` and `/a/../b/..` becomes `/`. |
| 241 | /// If the input path was relative and ends up being the `current_dir`, `.` is returned instead of the full path to `current_dir`. | 241 | /// If the input path was relative and ends up being the `current_dir`, `.` is returned instead of the full path to `current_dir`. |
| 242 | /// Note that single `.` components as well as duplicate separators are left untouched. | ||
| 242 | /// | 243 | /// |
| 243 | /// This is particularly useful when manipulating paths that are based on user input, and not resolving intermediate | 244 | /// This is particularly useful when manipulating paths that are based on user input, and not resolving intermediate |
| 244 | /// symlinks keeps the path similar to what the user provided. If that's not desirable, use `[realpath()][crate::realpath()` | 245 | /// symlinks keeps the path similar to what the user provided. If that's not desirable, use `[realpath()][crate::realpath()` |
| @@ -50,19 +50,12 @@ · 49 unmodified lines | |||
| 50 | #![forbid(unsafe_code)] | 50 | #![forbid(unsafe_code)] |
| 51 | 51 | ||
| 52 | /// A dummy type to represent path specs and help finding all spots that take path specs once it is implemented. | 52 | /// A dummy type to represent path specs and help finding all spots that take path specs once it is implemented. |
| 53 | |||
| 54 | /// A preliminary version of a path-spec based on glances of the code. | ||
| 55 | #[derive(Clone, Debug)] | ||
| 56 | pub struct Spec(bstr::BString); | ||
| 57 | |||
| 58 | mod convert; | 53 | mod convert; |
| 59 | pub use convert::*; | 54 | pub use convert::*; |
| 60 | 55 | ||
| 61 | mod util; | 56 | mod util; |
| 62 | pub use util::is_absolute; | 57 | pub use util::is_absolute; |
| 63 | 58 | ||
| 64 | mod spec; | ||
| 65 | |||
| 66 | /// | 59 | /// |
| 67 | pub mod realpath; | 60 | pub mod realpath; |
| 68 | pub use realpath::function::{realpath, realpath_opts}; | 61 | pub use realpath::function::{realpath, realpath_opts}; |
| @@ -1,53 +0,0 @@ | |||
| 1 | use std::ffi::OsStr; | ||
| 2 | |||
| 3 | use bstr::{BStr, ByteSlice, ByteVec}; | ||
| 4 | |||
| 5 | use crate::Spec; | ||
| 6 | |||
| 7 | impl std::convert::TryFrom<&OsStr> for Spec { | ||
| 8 | type Error = crate::Utf8Error; | ||
| 9 | |||
| 10 | fn try_from(value: &OsStr) -> Result<Self, Self::Error> { | ||
| 11 | crate::os_str_into_bstr(value).map(|value| { | ||
| 12 | assert_valid_hack(value); | ||
| 13 | Spec(value.into()) | ||
| 14 | }) | ||
| 15 | } | ||
| 16 | } | ||
| 17 | |||
| 18 | fn assert_valid_hack(input: &BStr) { | ||
| 19 | assert!(!input.contains_str(b"/../")); | ||
| 20 | assert!(!input.contains_str(b"/./")); | ||
| 21 | assert!(!input.starts_with_str(b"../")); | ||
| 22 | assert!(!input.starts_with_str(b"./")); | ||
| 23 | assert!(!input.starts_with_str(b"/")); | ||
| 24 | } | ||
| 25 | |||
| 26 | impl Spec { | ||
| 27 | /// Parse `input` into a `Spec` or `None` if it could not be parsed | ||
| 28 | // TODO: tests, actual implementation probably via `gix-pathspec` to make use of the crate after all. | ||
| 29 | pub fn from_bytes(input: &BStr) -> Option<Self> { | ||
| 30 | assert_valid_hack(input); | ||
| 31 | Spec(input.into()).into() | ||
| 32 | } | ||
| 33 | /// Return all paths described by this path spec, using slashes on all platforms. | ||
| 34 | pub fn items(&self) -> impl Iterator<Item = &BStr> { | ||
| 35 | std::iter::once(self.0.as_bstr()) | ||
| 36 | } | ||
| 37 | /// Adjust this path specification according to the given `prefix`, which may be empty to indicate we are the at work-tree root. | ||
| 38 | // TODO: this is a hack, needs test and time to do according to spec. This is just a minimum version to have -something-. | ||
| 39 | pub fn apply_prefix(&mut self, prefix: &std::path::Path) -> &Self { | ||
| 40 | // many more things we can't handle. `Path` never ends with trailing path separator. | ||
| 41 | let prefix = crate::into_bstr(prefix); | ||
| 42 | if !prefix.is_empty() { | ||
| 43 | let mut prefix = crate::to_unix_separators_on_windows(prefix); | ||
| 44 | { | ||
| 45 | let path = prefix.to_mut(); | ||
| 46 | path.push_byte(b'/'); | ||
| 47 | path.extend_from_slice(&self.0); | ||
| 48 | } | ||
| 49 | self.0 = prefix.into_owned(); | ||
| 50 | } | ||
| 51 | self | ||
| 52 | } | ||
| 53 | } | ||
| @@ -117,6 +117,7 @@ fn trailing_relative_components_are_resolved() { · 116 unmodified lines | |||
| 117 | ("./a/./b/..", "./a/."), | 117 | ("./a/./b/..", "./a/."), |
| 118 | ("/a/./b/c/.././../.", "/a"), | 118 | ("/a/./b/c/.././../.", "/a"), |
| 119 | ("/a/./b", "/a/./b"), | 119 | ("/a/./b", "/a/./b"), |
| 120 | ("././/a/./b", "././/a/./b"), | ||
| 120 | ("/a/././c/.././../.", "/"), | 121 | ("/a/././c/.././../.", "/"), |
| 121 | ("/a/b/../c/../..", "/"), | 122 | ("/a/b/../c/../..", "/"), |
| 122 | ("C:/hello/../a", "C:/a"), | 123 | ("C:/hello/../a", "C:/a"), |
| @@ -13,6 +13,7 @@ doctest = false · 12 unmodified lines | |||
| 13 | 13 | ||
| 14 | [dependencies] | 14 | [dependencies] |
| 15 | gix-glob = { version = "^0.10.2", path = "../gix-glob" } | 15 | gix-glob = { version = "^0.10.2", path = "../gix-glob" } |
| 16 | gix-path = { version = "^0.8.4", path = "../gix-path" } | ||
| 16 | gix-attributes = { version = "^0.16.0", path = "../gix-attributes" } | 17 | gix-attributes = { version = "^0.16.0", path = "../gix-attributes" } |
| 17 | 18 | ||
| 18 | bstr = { version = "1.3.0", default-features = false, features = ["std"]} | 19 | bstr = { version = "1.3.0", default-features = false, features = ["std"]} |
| @@ -2,5 +2,5 @@ · 1 unmodified line | |||
| 2 | use libfuzzer_sys::fuzz_target; | 2 | use libfuzzer_sys::fuzz_target; |
| 3 | 3 | ||
| 4 | fuzz_target!(|data: &[u8]| { | 4 | fuzz_target!(|data: &[u8]| { |
| 5 | let _a = gix_pathspec::parse(data); | 5 | let _a = gix_pathspec::parse(data, Default::default()); |
| 6 | }); | 6 | }); |
| @@ -5,17 +5,61 @@ · 4 unmodified lines | |||
| 5 | 5 | ||
| 6 | use bitflags::bitflags; | 6 | use bitflags::bitflags; |
| 7 | use bstr::BString; | 7 | use bstr::BString; |
| 8 | use std::path::PathBuf; | ||
| 9 | |||
| 10 | /// | ||
| 11 | pub mod normalize { | ||
| 12 | use std::path::PathBuf; | ||
| 13 | |||
| 14 | /// The error returned by [Pattern::normalize()](super::Pattern::normalize()). | ||
| 15 | #[derive(Debug, thiserror::Error)] | ||
| 16 | #[allow(missing_docs)] | ||
| 17 | pub enum Error { | ||
| 18 | #[error("The path '{}' is not inside of the worktree '{}'", path.display(), worktree_path.display())] | ||
| 19 | AbsolutePathOutsideOfWorktree { path: PathBuf, worktree_path: PathBuf }, | ||
| 20 | #[error("The path '{}' leaves the repository", path.display())] | ||
| 21 | OutsideOfWorktree { path: PathBuf }, | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | mod pattern; | ||
| 26 | |||
| 27 | /// | ||
| 28 | pub mod search; | ||
| 8 | 29 | ||
| 9 | /// | 30 | /// |
| 10 | pub mod parse; | 31 | pub mod parse; |
| 11 | 32 | ||
| 33 | /// A lists of pathspec patterns, possibly from a file. | ||
| 34 | /// | ||
| 35 | /// Pathspecs are generally relative to the root of the repository. | ||
| 36 | #[derive(Debug, Clone)] | ||
| 37 | pub struct Search { | ||
| 38 | /// Patterns and their associated data in the order they were loaded in or specified, | ||
| 39 | /// the line number in its source file or its sequence number (_`(pattern, value, line_number)`_). | ||
| 40 | /// | ||
| 41 | /// During matching, this order is reversed. | ||
| 42 | patterns: Vec<gix_glob::search::pattern::Mapping<search::Spec>>, | ||
| 43 | |||
| 44 | /// The path from which the patterns were read, or `None` if the patterns | ||
| 45 | /// don't originate in a file on disk. | ||
| 46 | pub source: Option<PathBuf>, | ||
| 47 | |||
| 48 | /// If `true`, this means all `patterns` are exclude patterns. This means that if there is no match | ||
| 49 | /// (which would exclude an item), we would actually match it for lack of exclusion. | ||
| 50 | all_patterns_are_excluded: bool, | ||
| 51 | /// The amount of bytes that are in common among all `patterns` and that aren't matched case-insensitively | ||
| 52 | common_prefix_len: usize, | ||
| 53 | } | ||
| 54 | |||
| 12 | /// The output of a pathspec [parsing][parse()] operation. It can be used to match against a one or more paths. | 55 | /// The output of a pathspec [parsing][parse()] operation. It can be used to match against a one or more paths. |
| 13 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | 56 | #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] |
| 14 | pub struct Pattern { | 57 | pub struct Pattern { |
| 15 | /// The path part of a pathspec. | 58 | /// The path part of a pathspec, which is typically a path possibly mixed with glob patterns. |
| 59 | /// Note that it might be an empty string as well. | ||
| 16 | /// | 60 | /// |
| 17 | /// `:(top,literal,icase,attr,exclude)some/path` would yield `some/path`. | 61 | /// For example, `:(top,literal,icase,attr,exclude)some/path` would yield `some/path`. |
| 18 | pub path: BString, | 62 | path: BString, |
| 19 | /// All magic signatures that were included in the pathspec. | 63 | /// All magic signatures that were included in the pathspec. |
| 20 | pub signature: MagicSignature, | 64 | pub signature: MagicSignature, |
| 21 | /// The search mode of the pathspec. | 65 | /// The search mode of the pathspec. |
| @@ -24,11 +68,18 @@ pub struct Pattern { · 2 unmodified lines | |||
| 24 | /// | 68 | /// |
| 25 | /// `:(attr:a=one b=):path` would yield attribute `a` and `b`. | 69 | /// `:(attr:a=one b=):path` would yield attribute `a` and `b`. |
| 26 | pub attributes: Vec<gix_attributes::Assignment>, | 70 | pub attributes: Vec<gix_attributes::Assignment>, |
| 71 | /// If `true`, we are a special Nil pattern and always match. | ||
| 72 | nil: bool, | ||
| 73 | /// The length of bytes in `path` that belong to the prefix, which will always be matched case-insensitively. | ||
| 74 | /// That way, even though pathspecs are applied from the top, we can emulate having changed directory into | ||
| 75 | /// a specific sub-directory in a case-sensitive file-system. | ||
| 76 | /// Is set by [Pattern::normalize()]. | ||
| 77 | prefix_len: usize, | ||
| 27 | } | 78 | } |
| 28 | 79 | ||
| 29 | bitflags! { | 80 | bitflags! { |
| 30 | /// Flags to represent 'magic signatures' which are parsed behind colons, like `:top:`. | 81 | /// Flags to represent 'magic signatures' which are parsed behind colons, like `:top:`. |
| 31 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] | 82 | #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] |
| 32 | pub struct MagicSignature: u32 { | 83 | pub struct MagicSignature: u32 { |
| 33 | /// Matches patterns from the root of the repository | 84 | /// Matches patterns from the root of the repository |
| 34 | const TOP = 1 << 0; | 85 | const TOP = 1 << 0; |
| @@ -36,12 +87,15 @@ bitflags! { · 1 unmodified line | |||
| 36 | const ICASE = 1 << 1; | 87 | const ICASE = 1 << 1; |
| 37 | /// Excludes the matching patterns from the previous results | 88 | /// Excludes the matching patterns from the previous results |
| 38 | const EXCLUDE = 1 << 2; | 89 | const EXCLUDE = 1 << 2; |
| 90 | /// The pattern must match a directory, and not a file. | ||
| 91 | /// This is equivalent to how it's handled in `gix-glob` | ||
| 92 | const MUST_BE_DIR = 1 << 3; | ||
| 39 | } | 93 | } |
| 40 | } | 94 | } |
| 41 | 95 | ||
| 42 | /// Parts of [magic signatures][MagicSignature] which don't stack as they all configure | 96 | /// Parts of [magic signatures][MagicSignature] which don't stack as they all configure |
| 43 | /// the way path specs are matched. | 97 | /// the way path specs are matched. |
| 44 | #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | 98 | #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] |
| 45 | pub enum MatchMode { | 99 | pub enum MatchMode { |
| 46 | /// Expand special characters like `*` similar to how the shell would do it. | 100 | /// Expand special characters like `*` similar to how the shell would do it. |
| 47 | /// | 101 | /// |
| @@ -54,7 +108,10 @@ pub enum MatchMode { · 6 unmodified lines | |||
| 54 | PathAwareGlob, | 108 | PathAwareGlob, |
| 55 | } | 109 | } |
| 56 | 110 | ||
| 57 | /// Parse a git-style pathspec into a [`Pattern`][Pattern]. | 111 | /// Parse a git-style pathspec into a [`Pattern`][Pattern], |
| 58 | pub fn parse(input: &[u8]) -> Result<Pattern, parse::Error> { | 112 | /// setting the given `default` values in case these aren't specified in `input`. |
| 59 | Pattern::from_bytes(input) | 113 | /// |
| 114 | /// Note that empty [paths](Pattern::path) are allowed here, and generally some processing has to be performed. | ||
| 115 | pub fn parse(input: &[u8], default: parse::Defaults) -> Result<Pattern, parse::Error> { | ||
| 116 | Pattern::from_bytes(input, default) | ||
| 60 | } | 117 | } |
| @@ -4,6 +4,21 @@ use bstr::{BStr, BString, ByteSlice, ByteVec}; · 3 unmodified lines | |||
| 4 | 4 | ||
| 5 | use crate::{MagicSignature, MatchMode, Pattern}; | 5 | use crate::{MagicSignature, MatchMode, Pattern}; |
| 6 | 6 | ||
| 7 | /// Default settings for some fields of a [`Pattern`]. | ||
| 8 | /// | ||
| 9 | /// These can be used to represent `GIT_*_PATHSPECS` environment variables, for example. | ||
| 10 | #[derive(Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] | ||
| 11 | pub struct Defaults { | ||
| 12 | /// The default signature. | ||
| 13 | pub signature: MagicSignature, | ||
| 14 | /// The default search-mode. | ||
| 15 | /// | ||
| 16 | /// Note that even if it's [`MatchMode::Literal`], the pathspecs will be parsed as usual, but matched verbatim afterwards. | ||
| 17 | pub search_mode: MatchMode, | ||
| 18 | /// If set, the pathspec will not be parsed but used verbatim. Implies [`MatchMode::Literal`] for `search_mode`. | ||
| 19 | pub literal: bool, | ||
| 20 | } | ||
| 21 | |||
| 7 | /// The error returned by [parse()][crate::parse()]. | 22 | /// The error returned by [parse()][crate::parse()]. |
| 8 | #[derive(thiserror::Error, Debug)] | 23 | #[derive(thiserror::Error, Debug)] |
| 9 | #[allow(missing_docs)] | 24 | #[allow(missing_docs)] |
| @@ -32,16 +47,31 @@ pub enum Error { · 22 unmodified lines | |||
| 32 | 47 | ||
| 33 | impl Pattern { | 48 | impl Pattern { |
| 34 | /// Try to parse a path-spec pattern from the given `input` bytes. | 49 | /// Try to parse a path-spec pattern from the given `input` bytes. |
| 35 | pub fn from_bytes(input: &[u8]) -> Result<Self, Error> { | 50 | pub fn from_bytes( |
| 51 | input: &[u8], | ||
| 52 | Defaults { | ||
| 53 | signature, | ||
| 54 | search_mode, | ||
| 55 | literal, | ||
| 56 | }: Defaults, | ||
| 57 | ) -> Result<Self, Error> { | ||
| 36 | if input.is_empty() { | 58 | if input.is_empty() { |
| 37 | return Err(Error::EmptyString); | 59 | return Err(Error::EmptyString); |
| 38 | } | 60 | } |
| 61 | if literal { | ||
| 62 | return Ok(Self::from_literal(input, signature)); | ||
| 63 | } | ||
| 64 | if input.as_bstr() == ":" { | ||
| 65 | return Ok(Pattern { | ||
| 66 | nil: true, | ||
| 67 | ..Default::default() | ||
| 68 | }); | ||
| 69 | } | ||
| 39 | 70 | ||
| 40 | let mut p = Pattern { | 71 | let mut p = Pattern { |
| 41 | path: BString::default(), | 72 | signature, |
| 42 | signature: MagicSignature::empty(), | 73 | search_mode, |
| 43 | search_mode: MatchMode::ShellGlob, | 74 | ..Default::default() |
| 44 | attributes: Vec::new(), | ||
| 45 | }; | 75 | }; |
| 46 | 76 | ||
| 47 | let mut cursor = 0; | 77 | let mut cursor = 0; |
| @@ -54,9 +84,25 @@ impl Pattern { · 6 unmodified lines | |||
| 54 | } | 84 | } |
| 55 | } | 85 | } |
| 56 | 86 | ||
| 57 | p.path = BString::from(&input[cursor..]); | 87 | let mut path = &input[cursor..]; |
| 88 | if path.last() == Some(&b'/') { | ||
| 89 | p.signature |= MagicSignature::MUST_BE_DIR; | ||
| 90 | path = &path[..path.len() - 1]; | ||
| 91 | } | ||
| 92 | p.path = path.into(); | ||
| 58 | Ok(p) | 93 | Ok(p) |
| 59 | } | 94 | } |
| 95 | |||
| 96 | /// Take `input` literally without parsing anything. This will also set our mode to `literal` to allow this pathspec to match `input` verbatim, and | ||
| 97 | /// use `default_signature` as magic signature. | ||
| 98 | pub fn from_literal(input: &[u8], default_signature: MagicSignature) -> Self { | ||
| 99 | Pattern { | ||
| 100 | path: input.into(), | ||
| 101 | signature: default_signature, | ||
| 102 | search_mode: MatchMode::Literal, | ||
| 103 | ..Default::default() | ||
| 104 | } | ||
| 105 | } | ||
| 60 | } | 106 | } |
| 61 | 107 | ||
| 62 | fn parse_short_keywords(input: &[u8], cursor: &mut usize) -> Result<MagicSignature, Error> { | 108 | fn parse_short_keywords(input: &[u8], cursor: &mut usize) -> Result<MagicSignature, Error> { |
| @@ -90,8 +136,6 @@ fn parse_long_keywords(input: &[u8], p: &mut Pattern, cursor: &mut usize) -> Res · 27 unmodified lines | |||
| 90 | let input = &input[*cursor..end]; | 136 | let input = &input[*cursor..end]; |
| 91 | *cursor = end + 1; | 137 | *cursor = end + 1; |
| 92 | 138 | ||
| 93 | debug_assert_eq!(p.search_mode, MatchMode::default()); | ||
| 94 | |||
| 95 | if input.is_empty() { | 139 | if input.is_empty() { |
| 96 | return Ok(()); | 140 | return Ok(()); |
| 97 | } | 141 | } |
| @@ -0,0 +1,192 @@ | |||
| 1 | use crate::{normalize, MagicSignature, MatchMode, Pattern}; | ||
| 2 | use bstr::{BStr, BString, ByteSlice, ByteVec}; | ||
| 3 | use std::path::{Component, Path, PathBuf}; | ||
| 4 | |||
| 5 | /// Access | ||
| 6 | impl Pattern { | ||
| 7 | /// Returns `true` if this seems to be a pathspec that indicates that 'there is no pathspec'. | ||
| 8 | /// | ||
| 9 | /// Note that such a spec is `:`. | ||
| 10 | pub fn is_nil(&self) -> bool { | ||
| 11 | self.nil | ||
| 12 | } | ||
| 13 | |||
| 14 | /// Return the prefix-portion of the `path` of this spec, which is a *directory*. | ||
| 15 | /// It can be empty if there is no prefix. | ||
| 16 | /// | ||
| 17 | /// A prefix is effectively the CWD seen as relative to the working tree, and it's assumed to | ||
| 18 | /// match case-sensitively. This makes it useful for skipping over large portions of input by | ||
| 19 | /// directly comparing them. | ||
| 20 | pub fn prefix_directory(&self) -> &BStr { | ||
| 21 | self.path[..self.prefix_len].as_bstr() | ||
| 22 | } | ||
| 23 | |||
| 24 | /// Return the path of this spec, typically used for matching. | ||
| 25 | pub fn path(&self) -> &BStr { | ||
| 26 | self.path.as_ref() | ||
| 27 | } | ||
| 28 | } | ||
| 29 | |||
| 30 | /// Mutation | ||
| 31 | impl Pattern { | ||
| 32 | /// Normalize the pattern's path by assuring it's relative to the root of the working tree, and contains | ||
| 33 | /// no relative path components. Further, it assures that `/` are used as path separator. | ||
| 34 | /// | ||
| 35 | /// If `self.path` is a relative path, it will be put in front of the pattern path if `self.signature` isn't indicating `TOP` already. | ||
| 36 | /// If `self.path` is an absolute path, we will use `root` to make it worktree relative if possible. | ||
| 37 | /// | ||
| 38 | /// `prefix` can be empty, we will still normalize this pathspec to resolve relative path components, and | ||
| 39 | /// it is assumed not to contain any relative path components, e.g. '', 'a', 'a/b' are valid. | ||
| 40 | /// `root` is the absolute path to the root of either the worktree or the repository's `git_dir`. | ||
| 41 | pub fn normalize(&mut self, prefix: &Path, root: &Path) -> Result<&mut Self, normalize::Error> { | ||
| 42 | fn prefix_components_to_subtract(path: &Path) -> usize { | ||
| 43 | let parent_component_end_bound = path.components().enumerate().fold(None::<usize>, |acc, (idx, c)| { | ||
| 44 | matches!(c, Component::ParentDir).then_some(idx + 1).or(acc) | ||
| 45 | }); | ||
| 46 | let count = path | ||
| 47 | .components() | ||
| 48 | .take(parent_component_end_bound.unwrap_or(0)) | ||
| 49 | .map(|c| match c { | ||
| 50 | Component::ParentDir => 1_isize, | ||
| 51 | Component::Normal(_) => -1, | ||
| 52 | _ => 0, | ||
| 53 | }) | ||
| 54 | .sum::<isize>(); | ||
| 55 | (count > 0).then_some(count as usize).unwrap_or_default() | ||
| 56 | } | ||
| 57 | |||
| 58 | let mut path = gix_path::from_bstr(self.path.as_ref()); | ||
| 59 | let mut num_prefix_components = 0; | ||
| 60 | let mut was_absolute = false; | ||
| 61 | if gix_path::is_absolute(path.as_ref()) { | ||
| 62 | was_absolute = true; | ||
| 63 | let rela_path = match path.strip_prefix(root) { | ||
| 64 | Ok(path) => path, | ||
| 65 | Err(_) => { | ||
| 66 | return Err(normalize::Error::AbsolutePathOutsideOfWorktree { | ||
| 67 | path: path.into_owned(), | ||
| 68 | worktree_path: root.into(), | ||
| 69 | }) | ||
| 70 | } | ||
| 71 | }; | ||
| 72 | path = rela_path.to_owned().into(); | ||
| 73 | } else if !prefix.as_os_str().is_empty() && !self.signature.contains(MagicSignature::TOP) { | ||
| 74 | debug_assert_eq!( | ||
| 75 | prefix | ||
| 76 | .components() | ||
| 77 | .filter(|c| matches!(c, Component::Normal(_))) | ||
| 78 | .count(), | ||
| 79 | prefix.components().count(), | ||
| 80 | "BUG: prefixes must not have relative path components, or calculations here will be wrong so pattern won't match" | ||
| 81 | ); | ||
| 82 | num_prefix_components = prefix | ||
| 83 | .components() | ||
| 84 | .count() | ||
| 85 | .saturating_sub(prefix_components_to_subtract(path.as_ref())); | ||
| 86 | path = prefix.join(path).into(); | ||
| 87 | } | ||
| 88 | |||
| 89 | let assure_path_cannot_break_out_upwards = Path::new(""); | ||
| 90 | let path = match gix_path::normalize(path.as_ref(), assure_path_cannot_break_out_upwards) { | ||
| 91 | Some(path) => { | ||
| 92 | if was_absolute { | ||
| 93 | num_prefix_components = path.components().count().saturating_sub( | ||
| 94 | if self.signature.contains(MagicSignature::MUST_BE_DIR) { | ||
| 95 | 0 | ||
| 96 | } else { | ||
| 97 | 1 | ||
| 98 | }, | ||
| 99 | ); | ||
| 100 | } | ||
| 101 | path | ||
| 102 | } | ||
| 103 | None => { | ||
| 104 | return Err(normalize::Error::OutsideOfWorktree { | ||
| 105 | path: path.into_owned(), | ||
| 106 | }) | ||
| 107 | } | ||
| 108 | }; | ||
| 109 | |||
| 110 | self.path = if path == Path::new(".") { | ||
| 111 | BString::from(".") | ||
| 112 | } else { | ||
| 113 | let cleaned = PathBuf::from_iter(path.components().filter(|c| !matches!(c, Component::CurDir))); | ||
| 114 | let mut out = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(cleaned)).into_owned(); | ||
| 115 | self.prefix_len = { | ||
| 116 | if self.signature.contains(MagicSignature::MUST_BE_DIR) { | ||
| 117 | out.push(b'/'); | ||
| 118 | } | ||
| 119 | let len = out | ||
| 120 | .find_iter(b"/") | ||
| 121 | .take(num_prefix_components) | ||
| 122 | .last() | ||
| 123 | .unwrap_or_default(); | ||
| 124 | if self.signature.contains(MagicSignature::MUST_BE_DIR) { | ||
| 125 | out.pop(); | ||
| 126 | } | ||
| 127 | len | ||
| 128 | }; | ||
| 129 | out | ||
| 130 | }; | ||
| 131 | |||
| 132 | Ok(self) | ||
| 133 | } | ||
| 134 | } | ||
| 135 | |||
| 136 | /// Access | ||
| 137 | impl Pattern { | ||
| 138 | /// Return `true` if this pathspec is negated, which means it will exclude an item from the result set instead of including it. | ||
| 139 | pub fn is_excluded(&self) -> bool { | ||
| 140 | self.signature.contains(MagicSignature::EXCLUDE) | ||
| 141 | } | ||
| 142 | |||
| 143 | /// Translate ourselves to a long display format, that when parsed back will yield the same pattern. | ||
| 144 | /// | ||
| 145 | /// Note that the | ||
| 146 | pub fn to_bstring(&self) -> BString { | ||
| 147 | if self.is_nil() { | ||
| 148 | ":".into() | ||
| 149 | } else { | ||
| 150 | let mut buf: BString = ":(".into(); | ||
| 151 | if self.signature.contains(MagicSignature::TOP) { | ||
| 152 | buf.push_str("top,"); | ||
| 153 | } | ||
| 154 | if self.signature.contains(MagicSignature::EXCLUDE) { | ||
| 155 | buf.push_str("exclude,"); | ||
| 156 | } | ||
| 157 | if self.signature.contains(MagicSignature::ICASE) { | ||
| 158 | buf.push_str("icase,"); | ||
| 159 | } | ||
| 160 | match self.search_mode { | ||
| 161 | MatchMode::ShellGlob => {} | ||
| 162 | MatchMode::Literal => buf.push_str("literal,"), | ||
| 163 | MatchMode::PathAwareGlob => buf.push_str("glob,"), | ||
| 164 | } | ||
| 165 | if self.attributes.is_empty() { | ||
| 166 | if buf.last() == Some(&b',') { | ||
| 167 | buf.pop(); | ||
| 168 | } | ||
| 169 | } else { | ||
| 170 | buf.push_str("attr:"); | ||
| 171 | for attr in &self.attributes { | ||
| 172 | let attr = attr.as_ref().to_string().replace(',', "\\,"); | ||
| 173 | buf.push_str(&attr); | ||
| 174 | buf.push(b' '); | ||
| 175 | } | ||
| 176 | buf.pop(); // trailing ' ' | ||
| 177 | } | ||
| 178 | buf.push(b')'); | ||
| 179 | buf.extend_from_slice(&self.path); | ||
| 180 | if self.signature.contains(MagicSignature::MUST_BE_DIR) { | ||
| 181 | buf.push(b'/'); | ||
| 182 | } | ||
| 183 | buf | ||
| 184 | } | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | impl std::fmt::Display for Pattern { | ||
| 189 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 190 | self.to_bstring().fmt(f) | ||
| 191 | } | ||
| 192 | } | ||
| @@ -0,0 +1,134 @@ | |||
| 1 | use crate::search::Spec; | ||
| 2 | use crate::{MagicSignature, Pattern, Search}; | ||
| 3 | use std::path::Path; | ||
| 4 | |||
| 5 | /// Create a new specification to support matches from `pathspec`, [normalizing](Pattern::normalize()) it with `prefix` and `root`. | ||
| 6 | fn mapping_from_pattern( | ||
| 7 | mut pathspec: Pattern, | ||
| 8 | prefix: &Path, | ||
| 9 | root: &Path, | ||
| 10 | sequence_number: usize, | ||
| 11 | ) -> Result<gix_glob::search::pattern::Mapping<Spec>, crate::normalize::Error> { | ||
| 12 | pathspec.normalize(prefix, root)?; | ||
| 13 | let mut match_all = pathspec.is_nil(); | ||
| 14 | let glob = { | ||
| 15 | let mut g = gix_glob::Pattern::from_bytes_without_negation(&pathspec.path).unwrap_or_else(|| { | ||
| 16 | match_all = true; | ||
| 17 | // This pattern is setup to match literally as all-whitespace. | ||
| 18 | gix_glob::Pattern { | ||
| 19 | text: pathspec.path.clone(), | ||
| 20 | mode: gix_glob::pattern::Mode::empty(), | ||
| 21 | first_wildcard_pos: None, | ||
| 22 | } | ||
| 23 | }); | ||
| 24 | g.mode |= gix_glob::pattern::Mode::ABSOLUTE; | ||
| 25 | if pathspec.signature.contains(MagicSignature::MUST_BE_DIR) { | ||
| 26 | g.mode |= gix_glob::pattern::Mode::MUST_BE_DIR; | ||
| 27 | } | ||
| 28 | g | ||
| 29 | }; | ||
| 30 | |||
| 31 | Ok(gix_glob::search::pattern::Mapping { | ||
| 32 | pattern: glob, | ||
| 33 | value: Spec { | ||
| 34 | attrs_match: { | ||
| 35 | (!pathspec.attributes.is_empty()).then(|| { | ||
| 36 | let mut out = gix_attributes::search::Outcome::default(); | ||
| 37 | out.initialize_with_selection( | ||
| 38 | &Default::default(), | ||
| 39 | pathspec.attributes.iter().map(|a| a.name.as_str()), | ||
| 40 | ); | ||
| 41 | out | ||
| 42 | }) | ||
| 43 | }, | ||
| 44 | pattern: pathspec, | ||
| 45 | }, | ||
| 46 | sequence_number, | ||
| 47 | }) | ||
| 48 | } | ||
| 49 | |||
| 50 | fn common_prefix_len(patterns: &[gix_glob::search::pattern::Mapping<Spec>]) -> usize { | ||
| 51 | let len = patterns | ||
| 52 | .iter() | ||
| 53 | .filter(|p| !p.value.pattern.is_excluded()) | ||
| 54 | .map(|p| { | ||
| 55 | if p.value.pattern.signature.contains(MagicSignature::ICASE) { | ||
| 56 | p.value.pattern.prefix_len | ||
| 57 | } else { | ||
| 58 | p.pattern.first_wildcard_pos.unwrap_or(p.pattern.text.len()) | ||
| 59 | } | ||
| 60 | }) | ||
| 61 | .min() | ||
| 62 | .unwrap_or_default(); | ||
| 63 | |||
| 64 | if len == 0 { | ||
| 65 | return 0; | ||
| 66 | } | ||
| 67 | |||
| 68 | let mut max_len = len; | ||
| 69 | let base = &patterns[0].value.pattern.path; | ||
| 70 | for path in patterns[1..] | ||
| 71 | .iter() | ||
| 72 | .filter(|p| !p.value.pattern.is_excluded()) | ||
| 73 | .map(|p| &p.value.pattern.path) | ||
| 74 | { | ||
| 75 | for (idx, (a, b)) in base[..max_len].iter().zip(path[..max_len].iter()).enumerate() { | ||
| 76 | if *a != *b { | ||
| 77 | max_len = idx; | ||
| 78 | break; | ||
| 79 | } | ||
| 80 | } | ||
| 81 | } | ||
| 82 | max_len | ||
| 83 | } | ||
| 84 | |||
| 85 | /// Lifecycle | ||
| 86 | impl Search { | ||
| 87 | /// Create a search from ready-made `pathspecs`, and [normalize](Pattern::normalize()) them with `prefix` and `root`. | ||
| 88 | /// `root` is the absolute path to the worktree root, if available, or the `git_dir` in case of bare repositories. | ||
| 89 | /// If `pathspecs` doesn't yield any pattern, we will match everything automatically. If `prefix` is also provided and not empty, | ||
| 90 | /// an artificial pattern will be added to yield all. | ||
| 91 | pub fn from_specs( | ||
| 92 | pathspecs: impl IntoIterator<Item = Pattern>, | ||
| 93 | prefix: Option<&std::path::Path>, | ||
| 94 | root: &std::path::Path, | ||
| 95 | ) -> Result<Self, crate::normalize::Error> { | ||
| 96 | let prefix = prefix.unwrap_or(std::path::Path::new("")); | ||
| 97 | let mut patterns = pathspecs | ||
| 98 | .into_iter() | ||
| 99 | .enumerate() | ||
| 100 | .map(|(idx, pattern)| mapping_from_pattern(pattern, prefix, root, idx)) | ||
| 101 | .collect::<Result<Vec<_>, _>>()?; | ||
| 102 | |||
| 103 | if patterns.is_empty() && !prefix.as_os_str().is_empty() { | ||
| 104 | patterns.push(mapping_from_pattern( | ||
| 105 | Pattern::from_literal(&[], MagicSignature::MUST_BE_DIR), | ||
| 106 | prefix, | ||
| 107 | root, | ||
| 108 | 0, | ||
| 109 | )?); | ||
| 110 | } | ||
| 111 | |||
| 112 | // Excludes should always happen first so we know a match is authoritative (otherwise we could find a non-excluding match first). | ||
| 113 | patterns.sort_by(|a, b| { | ||
| 114 | a.value | ||
| 115 | .pattern | ||
| 116 | .is_excluded() | ||
| 117 | .cmp(&b.value.pattern.is_excluded()) | ||
| 118 | .reverse() | ||
| 119 | }); | ||
| 120 | |||
| 121 | let common_prefix_len = common_prefix_len(&patterns); | ||
| 122 | Ok(Search { | ||
| 123 | all_patterns_are_excluded: patterns.iter().all(|s| s.value.pattern.is_excluded()), | ||
| 124 | patterns, | ||
| 125 | source: None, | ||
| 126 | common_prefix_len, | ||
| 127 | }) | ||
| 128 | } | ||
| 129 | |||
| 130 | /// Obtain ownership of the normalized pathspec patterns that were used for the search. | ||
| 131 | pub fn into_patterns(self) -> impl Iterator<Item = Pattern> { | ||
| 132 | self.patterns.into_iter().map(|p| p.value.pattern) | ||
| 133 | } | ||
| 134 | } | ||
| @@ -0,0 +1,147 @@ | |||
| 1 | use crate::search::{Match, Spec}; | ||
| 2 | use crate::{MagicSignature, MatchMode, Pattern, Search}; | ||
| 3 | use bstr::{BStr, BString, ByteSlice}; | ||
| 4 | use gix_glob::pattern::Case; | ||
| 5 | |||
| 6 | impl Search { | ||
| 7 | /// Return the first [`Match`] of `relative_path`, or `None`. | ||
| 8 | /// `is_dir` is true if `relative_path` is a directory. | ||
| 9 | /// `attributes` is called as `attributes(relative_path, case, is_dir, outcome) -> has_match` to obtain for attributes for `relative_path`, if | ||
| 10 | /// the underlying pathspec defined an attribute filter, to be stored in `outcome`, returning true if there was a match. | ||
| 11 | /// All attributes of the pathspec have to be present in the defined value for the pathspec to match. | ||
| 12 | /// | ||
| 13 | /// Note that `relative_path` is expected to be starting at the same root as is assumed for this pattern, see [`Pattern::normalize()`]. | ||
| 14 | /// Further, empty searches match everything, as if `:` was provided. | ||
| 15 | /// | ||
| 16 | /// ### Deviation | ||
| 17 | /// | ||
| 18 | /// The case-sensivity of the attribute match is controlled by the sensitivity of the pathspec, instead of being based on the | ||
| 19 | /// case folding settings of the repository. That way we assure that the matching is consistent. | ||
| 20 | /// Higher-level crates should control this default case folding of pathspecs when instantiating them, which is when they can | ||
| 21 | /// set it to match the repository setting for more natural behaviour when, for instance, adding files to a repository: | ||
| 22 | /// as it stands, on a case-insensitive file system, `touch File && git add file` will not add the file, but also not error. | ||
| 23 | pub fn pattern_matching_relative_path<'a>( | ||
| 24 | &mut self, | ||
| 25 | relative_path: impl Into<&'a BStr>, | ||
| 26 | is_dir: Option<bool>, | ||
| 27 | mut attributes: impl FnMut(&BStr, Case, bool, &mut gix_attributes::search::Outcome) -> bool, | ||
| 28 | ) -> Option<Match<'_>> { | ||
| 29 | let relative_path = relative_path.into(); | ||
| 30 | let basename_not_important = None; | ||
| 31 | if relative_path | ||
| 32 | .get(..self.common_prefix_len) | ||
| 33 | .map_or(true, |rela_path_prefix| rela_path_prefix != self.common_prefix()) | ||
| 34 | { | ||
| 35 | return None; | ||
| 36 | } | ||
| 37 | |||
| 38 | let is_dir = is_dir.unwrap_or(false); | ||
| 39 | let patterns_len = self.patterns.len(); | ||
| 40 | let res = self.patterns.iter_mut().find_map(|mapping| { | ||
| 41 | let ignore_case = mapping.value.pattern.signature.contains(MagicSignature::ICASE); | ||
| 42 | let prefix = mapping.value.pattern.prefix_directory(); | ||
| 43 | if ignore_case && !prefix.is_empty() { | ||
| 44 | let pattern_requirement_is_met = relative_path.get(prefix.len()).map_or_else(|| is_dir, |b| *b == b'/'); | ||
| 45 | if !pattern_requirement_is_met | ||
| 46 | || relative_path.get(..prefix.len()).map(ByteSlice::as_bstr) != Some(prefix) | ||
| 47 | { | ||
| 48 | return None; | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | let case = if ignore_case { Case::Fold } else { Case::Sensitive }; | ||
| 53 | let mut is_match = mapping.value.pattern.is_nil() || mapping.value.pattern.path.is_empty(); | ||
| 54 | if !is_match { | ||
| 55 | is_match = if mapping.pattern.first_wildcard_pos.is_none() { | ||
| 56 | match_verbatim(mapping, relative_path, is_dir, case) | ||
| 57 | } else { | ||
| 58 | let wildmatch_mode = match mapping.value.pattern.search_mode { | ||
| 59 | MatchMode::ShellGlob => Some(gix_glob::wildmatch::Mode::empty()), | ||
| 60 | MatchMode::Literal => None, | ||
| 61 | MatchMode::PathAwareGlob => Some(gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL), | ||
| 62 | }; | ||
| 63 | match wildmatch_mode { | ||
| 64 | Some(wildmatch_mode) => { | ||
| 65 | let is_match = mapping.pattern.matches_repo_relative_path( | ||
| 66 | relative_path, | ||
| 67 | basename_not_important, | ||
| 68 | Some(is_dir), | ||
| 69 | case, | ||
| 70 | wildmatch_mode, | ||
| 71 | ); | ||
| 72 | if !is_match { | ||
| 73 | match_verbatim(mapping, relative_path, is_dir, case) | ||
| 74 | } else { | ||
| 75 | true | ||
| 76 | } | ||
| 77 | } | ||
| 78 | None => match_verbatim(mapping, relative_path, is_dir, case), | ||
| 79 | } | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | if let Some(attrs) = mapping.value.attrs_match.as_mut() { | ||
| 84 | if !attributes(relative_path, Case::Sensitive, is_dir, attrs) { | ||
| 85 | // we have attrs, but it didn't match any | ||
| 86 | return None; | ||
| 87 | } | ||
| 88 | for (actual, expected) in attrs.iter_selected().zip(mapping.value.pattern.attributes.iter()) { | ||
| 89 | if actual.assignment != expected.as_ref() { | ||
| 90 | return None; | ||
| 91 | } | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | is_match.then_some(Match { | ||
| 96 | pattern: &mapping.value.pattern, | ||
| 97 | sequence_number: mapping.sequence_number, | ||
| 98 | }) | ||
| 99 | }); | ||
| 100 | |||
| 101 | if res.is_none() && self.all_patterns_are_excluded { | ||
| 102 | static MATCH_ALL_STAND_IN: Pattern = Pattern { | ||
| 103 | path: BString::new(Vec::new()), | ||
| 104 | signature: MagicSignature::empty(), | ||
| 105 | search_mode: MatchMode::ShellGlob, | ||
| 106 | attributes: Vec::new(), | ||
| 107 | prefix_len: 0, | ||
| 108 | nil: true, | ||
| 109 | }; | ||
| 110 | Some(Match { | ||
| 111 | pattern: &MATCH_ALL_STAND_IN, | ||
| 112 | sequence_number: patterns_len, | ||
| 113 | }) | ||
| 114 | } else { | ||
| 115 | res | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | fn match_verbatim( | ||
| 121 | mapping: &gix_glob::search::pattern::Mapping<Spec>, | ||
| 122 | relative_path: &BStr, | ||
| 123 | is_dir: bool, | ||
| 124 | case: Case, | ||
| 125 | ) -> bool { | ||
| 126 | let pattern_len = mapping.value.pattern.path.len(); | ||
| 127 | let mut relative_path_ends_with_slash_at_pattern_len = false; | ||
| 128 | let match_is_allowed = relative_path.get(pattern_len).map_or_else( | ||
| 129 | || relative_path.len() == pattern_len, | ||
| 130 | |b| { | ||
| 131 | relative_path_ends_with_slash_at_pattern_len = *b == b'/'; | ||
| 132 | relative_path_ends_with_slash_at_pattern_len | ||
| 133 | }, | ||
| 134 | ); | ||
| 135 | let pattern_requirement_is_met = !mapping.pattern.mode.contains(gix_glob::pattern::Mode::MUST_BE_DIR) | ||
| 136 | || (relative_path_ends_with_slash_at_pattern_len || is_dir); | ||
| 137 | |||
| 138 | if match_is_allowed && pattern_requirement_is_met { | ||
| 139 | let dir_or_file = &relative_path[..mapping.value.pattern.path.len()]; | ||
| 140 | match case { | ||
| 141 | Case::Sensitive => mapping.value.pattern.path == dir_or_file, | ||
| 142 | Case::Fold => mapping.value.pattern.path.eq_ignore_ascii_case(dir_or_file), | ||
| 143 | } | ||
| 144 | } else { | ||
| 145 | false | ||
| 146 | } | ||
| 147 | } | ||
| @@ -0,0 +1,47 @@ | |||
| 1 | use crate::{Pattern, Search}; | ||
| 2 | use bstr::{BStr, ByteSlice}; | ||
| 3 | |||
| 4 | /// Describes a matching pattern within a search for ignored paths. | ||
| 5 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 6 | pub struct Match<'a> { | ||
| 7 | /// The matching search specification, which contains the pathspec as well. | ||
| 8 | pub pattern: &'a Pattern, | ||
| 9 | /// The number of the sequence the matching pathspec was in, or the line of pathspec file it was read from if [Search::source] is not `None`. | ||
| 10 | pub sequence_number: usize, | ||
| 11 | } | ||
| 12 | |||
| 13 | mod init; | ||
| 14 | |||
| 15 | impl Match<'_> { | ||
| 16 | /// Return `true` if the pathspec that matched was negative, which excludes this item from the set. | ||
| 17 | pub fn is_excluded(&self) -> bool { | ||
| 18 | self.pattern.is_excluded() | ||
| 19 | } | ||
| 20 | } | ||
| 21 | |||
| 22 | /// Access | ||
| 23 | impl Search { | ||
| 24 | /// Return an iterator over the patterns that participate in the search. | ||
| 25 | pub fn patterns(&self) -> impl Iterator<Item = &Pattern> + '_ { | ||
| 26 | self.patterns.iter().map(|m| &m.value.pattern) | ||
| 27 | } | ||
| 28 | |||
| 29 | /// Return the portion of the prefix among all of the pathspecs involved in this search, or an empty string if | ||
| 30 | /// there is none. It doesn't have to end at a directory boundary though, nor does it denote a directory. | ||
| 31 | /// | ||
| 32 | /// Note that the common_prefix can be matched case-insensitively, which makes it useful to skip large portions of input. | ||
| 33 | /// Further, excluded pathspecs don't participate which makes this common prefix inclusive. | ||
| 34 | pub fn common_prefix(&self) -> &BStr { | ||
| 35 | self.patterns | ||
| 36 | .get(0) | ||
| 37 | .map_or("".into(), |m| m.value.pattern.path[..self.common_prefix_len].as_bstr()) | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | #[derive(Default, Clone, Debug)] | ||
| 42 | pub(crate) struct Spec { | ||
| 43 | pub pattern: Pattern, | ||
| 44 | pub attrs_match: Option<gix_attributes::search::Outcome>, | ||
| 45 | } | ||
| 46 | |||
| 47 | mod matching; | ||
| @@ -1,3 +0,0 @@ | |||
| 1 | version https://git-lfs.github.com/spec/v1 | ||
| 2 | oid sha256:505e0eed20808737e0f3f8b993e54e3b2b9b9d124e1615a003336ce34c60b939 | ||
| 3 | size 9484 | ||
| @@ -0,0 +1,3 @@ | |||
| 1 | version https://git-lfs.github.com/spec/v1 | ||
| 2 | oid sha256:0ba46801dfab65bcc77c8ce0dc9144d77fd142be847f3340af6ae8fcba44c82d | ||
| 3 | size 16928 | ||
| @@ -0,0 +1,3 @@ | |||
| 1 | version https://git-lfs.github.com/spec/v1 | ||
| 2 | oid sha256:343ad9b362536abf88d6fc4519b6532e53e53f8a1b94436c9ec3f88b6874e4df | ||
| 3 | size 15972 | ||
| @@ -0,0 +1,3 @@ | |||
| 1 | version https://git-lfs.github.com/spec/v1 | ||
| 2 | oid sha256:b0c722ab08de2ab63b9f222747cf898f5b6bb6b76ec55ccd176bab3b854ef7bb | ||
| 3 | size 10700 | ||
| @@ -0,0 +1,3 @@ | |||
| 1 | version https://git-lfs.github.com/spec/v1 | ||
| 2 | oid sha256:123da727b33802a3a2dc80e948b07cbf133c2d66129693f4896cba2f6b92b02f | ||
| 3 | size 9528 | ||
| @@ -0,0 +1,86 @@ | |||
| 1 | #!/bin/bash | ||
| 2 | set -eu -o pipefail | ||
| 3 | |||
| 4 | git init; | ||
| 5 | git init sub | ||
| 6 | (cd sub | ||
| 7 | : >empty | ||
| 8 | git add empty | ||
| 9 | git commit -m init-for-submodule | ||
| 10 | ) | ||
| 11 | |||
| 12 | git init parent | ||
| 13 | (cd parent | ||
| 14 | function baseline() { | ||
| 15 | local args="" | ||
| 16 | local specs="" | ||
| 17 | |||
| 18 | for arg in "$@"; do | ||
| 19 | if [[ $arg == *"+"* ]]; then | ||
| 20 | echo "BUG: Argument '$arg' contains a space - we use that for separating pathspecs right now" >&2 | ||
| 21 | exit 1 | ||
| 22 | fi | ||
| 23 | args="$args -c submodule.active=$arg" | ||
| 24 | specs="${specs}+${arg}" | ||
| 25 | done | ||
| 26 | |||
| 27 | { | ||
| 28 | echo "$specs" | ||
| 29 | git $args submodule | ||
| 30 | echo -n ';' | ||
| 31 | } >> baseline.git | ||
| 32 | } | ||
| 33 | |||
| 34 | for p in a bb dir/b dir/bb dir/nested/c cc; do | ||
| 35 | git submodule add ../sub $p | ||
| 36 | git config --unset submodule.$p.active | ||
| 37 | done | ||
| 38 | git commit -m "init" | ||
| 39 | |||
| 40 | git submodule > paths | ||
| 41 | |||
| 42 | baseline ':' | ||
| 43 | baseline ':!' | ||
| 44 | baseline 'a' | ||
| 45 | baseline ':(icase)A' | ||
| 46 | baseline ':(icase,exclude)A' | ||
| 47 | baseline ':(icase,exclude)*/B*' | ||
| 48 | baseline ':(icase,exclude)*/B?' | ||
| 49 | baseline 'di' | ||
| 50 | baseline 'di?' | ||
| 51 | baseline 'di?/' | ||
| 52 | baseline 'dir*' | ||
| 53 | baseline 'dir/*' | ||
| 54 | baseline ':(glob)dir*' | ||
| 55 | baseline ':(glob,icase,exclude)dir*' | ||
| 56 | baseline ':(glob)dir/*' | ||
| 57 | baseline 'dir' | ||
| 58 | baseline 'dir/' | ||
| 59 | baseline ':(literal)dir' | ||
| 60 | baseline ':(literal)dir/' | ||
| 61 | baseline 'dir/nested' | ||
| 62 | baseline 'dir/nested/' | ||
| 63 | baseline ':(exclude)dir/' | ||
| 64 | baseline ':(icase)DIR' | ||
| 65 | baseline ':(icase)DIR/' | ||
| 66 | baseline ':!a' | ||
| 67 | baseline ':' ':!bb' | ||
| 68 | baseline ':!bb' | ||
| 69 | baseline 'a/' | ||
| 70 | baseline 'bb' | ||
| 71 | baseline 'dir/b' | ||
| 72 | baseline 'dir/b/' | ||
| 73 | # ["dir/b"] == [] | ||
| 74 | baseline '*/b/' git-inconsistency | ||
| 75 | baseline '*b' | ||
| 76 | baseline '*b*' | ||
| 77 | baseline '*b?' | ||
| 78 | baseline '*/b' | ||
| 79 | baseline '*/b?' | ||
| 80 | baseline '*/b*' | ||
| 81 | baseline '*c' | ||
| 82 | baseline '*/c' | ||
| 83 | baseline ':(glob)**/c' | ||
| 84 | baseline ':(glob)**/c?' | ||
| 85 | baseline ':(glob)**/c*' | ||
| 86 | ) | ||
| @@ -0,0 +1,94 @@ | |||
| 1 | #!/bin/bash | ||
| 2 | set -eu -o pipefail | ||
| 3 | |||
| 4 | git init; | ||
| 5 | |||
| 6 | function baseline() { | ||
| 7 | local specs="" | ||
| 8 | for arg in "$@"; do | ||
| 9 | if [[ $arg == *"+"* ]]; then | ||
| 10 | echo "BUG: Argument '$arg' contains a space - we use that for separating pathspecs right now" >&2 | ||
| 11 | exit 1 | ||
| 12 | fi | ||
| 13 | specs="${specs}+${arg}" | ||
| 14 | done | ||
| 15 | |||
| 16 | { | ||
| 17 | echo "$specs" | ||
| 18 | git ls-files "$@" | ||
| 19 | echo -n ';' | ||
| 20 | } >> baseline.git | ||
| 21 | } | ||
| 22 | |||
| 23 | : >goo | ||
| 24 | : >'g[o][o]' | ||
| 25 | : >bar | ||
| 26 | echo 'goo a !b c=v -d' > .gitattributes | ||
| 27 | mkdir sub && :>sub/bar | ||
| 28 | git add . && git commit -m init | ||
| 29 | # this is to avoid issues on windows, which might not be able to manifest these files. | ||
| 30 | git -c core.protectNTFS=false update-index --add --cacheinfo 100644 "$(git rev-parse HEAD:goo)" "g*" | ||
| 31 | git update-index --add --cacheinfo 100644 "$(git rev-parse HEAD:goo)" "!a" | ||
| 32 | for p in bar bAr BAR foo/bar foo/bAr foo/BAR fOo/bar fOo/bAr fOo/BAR FOO/bar FOO/bAr FOO/BAR; do | ||
| 33 | git -c core.ignoreCase=false update-index --add --cacheinfo 100644 "$(git rev-parse HEAD:goo)" "$p" | ||
| 34 | done | ||
| 35 | git -c core.ignoreCase=false update-index --add --cacheinfo 100644 "$(git rev-parse HEAD:goo)" " " # 4 x space | ||
| 36 | git -c core.ignoreCase=false update-index --add --cacheinfo 100644 "$(git rev-parse HEAD:goo)" " hi " # 2 x space hi 2 x space | ||
| 37 | |||
| 38 | git ls-files > paths | ||
| 39 | |||
| 40 | baseline ':(attr:a)goo' | ||
| 41 | baseline ':(attr:a)Goo' | ||
| 42 | baseline ':(icase,attr:a)Goo' | ||
| 43 | baseline ':(attr:!b)goo' | ||
| 44 | baseline ':(attr:c=v)goo' | ||
| 45 | baseline ':(attr:-d)goo' | ||
| 46 | baseline ':(attr:a !b c=v -d)goo' | ||
| 47 | baseline ':(icase,attr:a !b c=v -d)GOO' | ||
| 48 | baseline ':(attr:a !b c=v -d)g*' | ||
| 49 | baseline ':(attr:none)goo' | ||
| 50 | baseline ':(literal)g*' | ||
| 51 | baseline 'sub/' | ||
| 52 | baseline 'sub' | ||
| 53 | baseline 'sub/*' | ||
| 54 | baseline 'sub*' | ||
| 55 | baseline ':(literal)g*' | ||
| 56 | baseline ':(glob)g*' | ||
| 57 | baseline ':(exclude,literal)g*' | ||
| 58 | baseline 'g*' | ||
| 59 | baseline ':(exclude)g*' | ||
| 60 | baseline ':(literal)?*' | ||
| 61 | baseline ':(exclude,literal)?*' | ||
| 62 | baseline '?*' | ||
| 63 | baseline ':(exclude)?*' | ||
| 64 | baseline 'g[o][o]' | ||
| 65 | # ["g[o][o]", "goo"] == ["g[o][o]"] | ||
| 66 | baseline ':(icase)G[O][o]' git-inconsistency | ||
| 67 | baseline ':(literal)g[o][o]' | ||
| 68 | baseline ':(literal,icase)G[o][O]' | ||
| 69 | baseline ':(glob)g[o][o]' | ||
| 70 | # ["g[o][o]", "goo"] == ["g[o][o]"] | ||
| 71 | baseline ':(glob,icase)g[o][O]' git-inconsistency | ||
| 72 | baseline ':(glob)**/bar' | ||
| 73 | baseline ':(literal)**/bar' | ||
| 74 | baseline '**/bar' | ||
| 75 | baseline '*/bar' | ||
| 76 | baseline ':(glob)*bar' | ||
| 77 | baseline ':(glob)**bar' | ||
| 78 | baseline '*bar' | ||
| 79 | baseline '*bar*' | ||
| 80 | baseline 'bar' | ||
| 81 | baseline 'bar/' | ||
| 82 | baseline 'sub/bar/' | ||
| 83 | baseline 'sub/bar' | ||
| 84 | baseline '!a' | ||
| 85 | baseline '?a' | ||
| 86 | baseline 'foo/' | ||
| 87 | baseline 'foo' | ||
| 88 | baseline 'foo/*' | ||
| 89 | baseline 'foo*' | ||
| 90 | baseline ':(icase)foo/' | ||
| 91 | baseline ':(icase)foo' | ||
| 92 | baseline ':(icase)foo/*' | ||
| 93 | baseline ':(icase)foo*' | ||
| 94 | baseline ':(icase)foo/bar' | ||
| @@ -28,6 +28,7 @@ baseline ':!^(exclude,exclude)' · 27 unmodified lines | |||
| 28 | 28 | ||
| 29 | # empty_signatures | 29 | # empty_signatures |
| 30 | baseline '.' | 30 | baseline '.' |
| 31 | baseline ':' | ||
| 31 | baseline 'some/path' | 32 | baseline 'some/path' |
| 32 | baseline ':some/path' | 33 | baseline ':some/path' |
| 33 | baseline ':()some/path' | 34 | baseline ':()some/path' |
| @@ -47,8 +48,11 @@ baseline ':! some/path' · 13 unmodified lines | |||
| 47 | 48 | ||
| 48 | # short_signatures | 49 | # short_signatures |
| 49 | baseline ':/some/path' | 50 | baseline ':/some/path' |
| 51 | baseline '://some/path' | ||
| 50 | baseline ':^some/path' | 52 | baseline ':^some/path' |
| 53 | baseline ':^^some/path' | ||
| 51 | baseline ':!some/path' | 54 | baseline ':!some/path' |
| 55 | baseline ':!!some/path' | ||
| 52 | baseline ':/!some/path' | 56 | baseline ':/!some/path' |
| 53 | baseline ':!/^/:some/path' | 57 | baseline ':!/^/:some/path' |
| 54 | 58 | ||
| @@ -143,3 +147,12 @@ baseline ':(top' · 88 unmodified lines | |||
| 143 | 147 | ||
| 144 | # glob_and_literal_keywords_present | 148 | # glob_and_literal_keywords_present |
| 145 | baseline ':(glob,literal)some/path' | 149 | baseline ':(glob,literal)some/path' |
| 150 | # trailing slash | ||
| 151 | baseline ':(glob,literal)some/path/' | ||
| 152 | baseline 'some/path/' | ||
| 153 | baseline 'path/' | ||
| 154 | |||
| 155 | baseline 'a/b/' | ||
| 156 | baseline 'a/' | ||
| 157 | baseline '!a' | ||
| 158 | baseline '\!a' | ||
| @@ -0,0 +1,116 @@ | |||
| 1 | use std::path::Path; | ||
| 2 | |||
| 3 | #[test] | ||
| 4 | fn removes_relative_path_components() -> crate::Result { | ||
| 5 | for (input_path, expected_path, expected_prefix) in [ | ||
| 6 | ("c", "a/b/c", "a/b"), | ||
| 7 | ("../c", "a/c", "a"), | ||
| 8 | ("../b/c", "a/b/c", "a"), // this is a feature - prefix components once consumed by .. are lost. Important as paths can contain globs | ||
| 9 | ("../*c/d", "a/*c/d", "a"), | ||
| 10 | ("../../c/d", "c/d", ""), | ||
| 11 | ("../../c/d/", "c/d", ""), | ||
| 12 | ("./c", "a/b/c", "a/b"), | ||
| 13 | ("../../c", "c", ""), | ||
| 14 | ("../..", ".", ""), | ||
| 15 | ("../././c", "a/c", "a"), | ||
| 16 | ("././/./c", "a/b/c", "a/b"), | ||
| 17 | ("././/./c/", "a/b/c", "a/b"), | ||
| 18 | ("././/./../c/d/", "a/c/d", "a"), | ||
| 19 | ] { | ||
| 20 | let spec = normalized_spec(input_path, "a/b", "")?; | ||
| 21 | assert_eq!(spec.path(), expected_path); | ||
| 22 | assert_eq!( | ||
| 23 | spec.prefix_directory(), | ||
| 24 | expected_prefix, | ||
| 25 | "{input_path} -> {expected_path}" | ||
| 26 | ); | ||
| 27 | } | ||
| 28 | Ok(()) | ||
| 29 | } | ||
| 30 | |||
| 31 | #[test] | ||
| 32 | fn single_dot_is_special_and_directory_is_implied_without_trailing_slash() -> crate::Result { | ||
| 33 | for (input_path, expected) in [(".", "."), ("./", ".")] { | ||
| 34 | let spec = normalized_spec(input_path, "", "/repo")?; | ||
| 35 | assert_eq!(spec.path(), expected); | ||
| 36 | assert_eq!(spec.prefix_directory(), ""); | ||
| 37 | } | ||
| 38 | Ok(()) | ||
| 39 | } | ||
| 40 | |||
| 41 | #[test] | ||
| 42 | fn absolute_path_made_relative() -> crate::Result { | ||
| 43 | for (input_path, expected, prefix_dir) in [ | ||
| 44 | ("/repo/a", "a", ""), | ||
| 45 | ("/repo/a/..//.///b", "b", ""), | ||
| 46 | ("/repo/a/", "a", "a"), | ||
| 47 | ("/repo/*/", "*", "*"), | ||
| 48 | ("/repo/a/b", "a/b", "a"), | ||
| 49 | ("/repo/*/b", "*/b", "*"), // we assume literal paths if specs are absolute | ||
| 50 | ("/repo/a/*/", "a/*", "a/*"), | ||
| 51 | ("/repo/a/b/", "a/b", "a/b"), | ||
| 52 | ("/repo/a/b/*", "a/b/*", "a/b"), | ||
| 53 | ("/repo/a/b/c/..", "a/b", "a"), | ||
| 54 | ] { | ||
| 55 | let spec = normalized_spec(input_path, "", "/repo")?; | ||
| 56 | assert_eq!(spec.path(), expected); | ||
| 57 | assert_eq!(spec.prefix_directory(), prefix_dir, "{input_path}"); | ||
| 58 | } | ||
| 59 | Ok(()) | ||
| 60 | } | ||
| 61 | |||
| 62 | #[test] | ||
| 63 | fn relative_top_patterns_ignore_the_prefix() -> crate::Result { | ||
| 64 | let spec = normalized_spec(":(top)c", "a/b", "")?; | ||
| 65 | assert_eq!(spec.path(), "c"); | ||
| 66 | assert_eq!(spec.prefix_directory(), ""); | ||
| 67 | Ok(()) | ||
| 68 | } | ||
| 69 | |||
| 70 | #[test] | ||
| 71 | fn absolute_top_patterns_ignore_the_prefix_but_are_made_relative() -> crate::Result { | ||
| 72 | let spec = normalized_spec(":(top)/a/b", "prefix-ignored", "/a")?; | ||
| 73 | assert_eq!(spec.path(), "b"); | ||
| 74 | assert_eq!(spec.prefix_directory(), ""); | ||
| 75 | Ok(()) | ||
| 76 | } | ||
| 77 | |||
| 78 | #[test] | ||
| 79 | fn relative_path_breaks_out_of_working_tree() { | ||
| 80 | let err = normalized_spec("../a", "", "").unwrap_err(); | ||
| 81 | assert_eq!(err.to_string(), "The path '../a' leaves the repository"); | ||
| 82 | let err = normalized_spec("../../b", "a", "").unwrap_err(); | ||
| 83 | assert_eq!( | ||
| 84 | err.to_string(), | ||
| 85 | format!( | ||
| 86 | "The path '{}' leaves the repository", | ||
| 87 | if cfg!(windows) { "a\\../../b" } else { "a/../../b" } | ||
| 88 | ) | ||
| 89 | ); | ||
| 90 | } | ||
| 91 | |||
| 92 | #[test] | ||
| 93 | fn absolute_path_breaks_out_of_working_tree() { | ||
| 94 | let err = normalized_spec("/path/to/repo/..///./a", "", "/path/to/repo").unwrap_err(); | ||
| 95 | assert_eq!(err.to_string(), "The path '..///./a' leaves the repository"); | ||
| 96 | let err = normalized_spec("/path/to/repo/../../../dev", "", "/path/to/repo").unwrap_err(); | ||
| 97 | assert_eq!(err.to_string(), "The path '../../../dev' leaves the repository"); | ||
| 98 | } | ||
| 99 | |||
| 100 | #[test] | ||
| 101 | fn absolute_path_escapes_worktree() { | ||
| 102 | assert_eq!( | ||
| 103 | normalized_spec("/dev", "", "/path/to/repo").unwrap_err().to_string(), | ||
| 104 | "The path '/dev' is not inside of the worktree '/path/to/repo'" | ||
| 105 | ); | ||
| 106 | } | ||
| 107 | |||
| 108 | fn normalized_spec( | ||
| 109 | path: &str, | ||
| 110 | prefix: &str, | ||
| 111 | root: &str, | ||
| 112 | ) -> Result<gix_pathspec::Pattern, gix_pathspec::normalize::Error> { | ||
| 113 | let mut spec = gix_pathspec::parse(path.as_bytes(), Default::default()).expect("valid"); | ||
| 114 | spec.normalize(Path::new(prefix), Path::new(root))?; | ||
| 115 | Ok(spec) | ||
| 116 | } | ||
| @@ -0,0 +1,152 @@ | |||
| 1 | use gix_pathspec::parse::Error; | ||
| 2 | |||
| 3 | use crate::parse::check_against_baseline; | ||
| 4 | |||
| 5 | #[test] | ||
| 6 | fn empty_input() { | ||
| 7 | let input = ""; | ||
| 8 | |||
| 9 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 10 | |||
| 11 | let output = gix_pathspec::parse(input.as_bytes(), Default::default()); | ||
| 12 | assert!(output.is_err()); | ||
| 13 | assert!(matches!(output.unwrap_err(), Error::EmptyString)); | ||
| 14 | } | ||
| 15 | |||
| 16 | #[test] | ||
| 17 | fn invalid_short_signatures() { | ||
| 18 | let inputs = vec![ | ||
| 19 | ":\"()", ":#()", ":%()", ":&()", ":'()", ":,()", ":-()", ":;()", ":<()", ":=()", ":>()", ":@()", ":_()", | ||
| 20 | ":`()", ":~()", | ||
| 21 | ]; | ||
| 22 | |||
| 23 | for input in inputs.into_iter() { | ||
| 24 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 25 | |||
| 26 | let output = gix_pathspec::parse(input.as_bytes(), Default::default()); | ||
| 27 | assert!(output.is_err()); | ||
| 28 | assert!(matches!(output.unwrap_err(), Error::Unimplemented { .. })); | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | #[test] | ||
| 33 | fn invalid_keywords() { | ||
| 34 | let inputs = vec![ | ||
| 35 | ":( )some/path", | ||
| 36 | ":(tp)some/path", | ||
| 37 | ":(top, exclude)some/path", | ||
| 38 | ":(top,exclude,icse)some/path", | ||
| 39 | ]; | ||
| 40 | |||
| 41 | for input in inputs.into_iter() { | ||
| 42 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 43 | |||
| 44 | let output = gix_pathspec::parse(input.as_bytes(), Default::default()); | ||
| 45 | assert!(output.is_err()); | ||
| 46 | assert!(matches!(output.unwrap_err(), Error::InvalidKeyword { .. })); | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | #[test] | ||
| 51 | fn invalid_attributes() { | ||
| 52 | let inputs = vec![ | ||
| 53 | ":(attr:+invalidAttr)some/path", | ||
| 54 | ":(attr:validAttr +invalidAttr)some/path", | ||
| 55 | ":(attr:+invalidAttr,attr:valid)some/path", | ||
| 56 | r":(attr:inva\lid)some/path", | ||
| 57 | ]; | ||
| 58 | |||
| 59 | for input in inputs { | ||
| 60 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 61 | |||
| 62 | let output = gix_pathspec::parse(input.as_bytes(), Default::default()); | ||
| 63 | assert!(output.is_err(), "This pathspec did not produce an error {input}"); | ||
| 64 | assert!(matches!(output.unwrap_err(), Error::InvalidAttribute { .. })); | ||
| 65 | } | ||
| 66 | } | ||
| 67 | |||
| 68 | #[test] | ||
| 69 | fn invalid_attribute_values() { | ||
| 70 | let inputs = vec![ | ||
| 71 | r":(attr:v=inva#lid)some/path", | ||
| 72 | r":(attr:v=inva\\lid)some/path", | ||
| 73 | r":(attr:v=invalid\\)some/path", | ||
| 74 | r":(attr:v=invalid\#)some/path", | ||
| 75 | r":(attr:v=inva\=lid)some/path", | ||
| 76 | r":(attr:a=valid b=inva\#lid)some/path", | ||
| 77 | ":(attr:v=val��)", | ||
| 78 | ":(attr:pr=pre��x:,)�", | ||
| 79 | ]; | ||
| 80 | |||
| 81 | for input in inputs { | ||
| 82 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 83 | |||
| 84 | let output = gix_pathspec::parse(input.as_bytes(), Default::default()); | ||
| 85 | assert!(output.is_err(), "This pathspec did not produce an error {input}"); | ||
| 86 | assert!( | ||
| 87 | matches!(output.unwrap_err(), Error::InvalidAttributeValue { .. }), | ||
| 88 | "Errors did not match for pathspec: {input}" | ||
| 89 | ); | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | #[test] | ||
| 94 | fn escape_character_at_end_of_attribute_value() { | ||
| 95 | let inputs = vec![ | ||
| 96 | r":(attr:v=invalid\)some/path", | ||
| 97 | r":(attr:v=invalid\ )some/path", | ||
| 98 | r":(attr:v=invalid\ valid)some/path", | ||
| 99 | ]; | ||
| 100 | |||
| 101 | for input in inputs { | ||
| 102 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 103 | |||
| 104 | let output = gix_pathspec::parse(input.as_bytes(), Default::default()); | ||
| 105 | assert!(output.is_err(), "This pathspec did not produce an error {input}"); | ||
| 106 | assert!(matches!(output.unwrap_err(), Error::TrailingEscapeCharacter)); | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | #[test] | ||
| 111 | fn empty_attribute_specification() { | ||
| 112 | let input = ":(attr:)"; | ||
| 113 | |||
| 114 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 115 | |||
| 116 | let output = gix_pathspec::parse(input.as_bytes(), Default::default()); | ||
| 117 | assert!(output.is_err()); | ||
| 118 | assert!(matches!(output.unwrap_err(), Error::EmptyAttribute)); | ||
| 119 | } | ||
| 120 | |||
| 121 | #[test] | ||
| 122 | fn multiple_attribute_specifications() { | ||
| 123 | let input = ":(attr:one,attr:two)some/path"; | ||
| 124 | |||
| 125 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 126 | |||
| 127 | let output = gix_pathspec::parse(input.as_bytes(), Default::default()); | ||
| 128 | assert!(output.is_err()); | ||
| 129 | assert!(matches!(output.unwrap_err(), Error::MultipleAttributeSpecifications)); | ||
| 130 | } | ||
| 131 | |||
| 132 | #[test] | ||
| 133 | fn missing_parentheses() { | ||
| 134 | let input = ":(top"; | ||
| 135 | |||
| 136 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 137 | |||
| 138 | let output = gix_pathspec::parse(input.as_bytes(), Default::default()); | ||
| 139 | assert!(output.is_err()); | ||
| 140 | assert!(matches!(output.unwrap_err(), Error::MissingClosingParenthesis { .. })); | ||
| 141 | } | ||
| 142 | |||
| 143 | #[test] | ||
| 144 | fn glob_and_literal_keywords_present() { | ||
| 145 | let input = ":(glob,literal)some/path"; | ||
| 146 | |||
| 147 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 148 | |||
| 149 | let output = gix_pathspec::parse(input.as_bytes(), Default::default()); | ||
| 150 | assert!(output.is_err()); | ||
| 151 | assert!(matches!(output.unwrap_err(), Error::IncompatibleSearchModes)); | ||
| 152 | } | ||
| @@ -0,0 +1,93 @@ | |||
| 1 | use std::collections::HashMap; | ||
| 2 | |||
| 3 | use bstr::{BStr, BString, ByteSlice}; | ||
| 4 | use gix_attributes::State; | ||
| 5 | use gix_pathspec::{MagicSignature, MatchMode, Pattern}; | ||
| 6 | use once_cell::sync::Lazy; | ||
| 7 | |||
| 8 | #[test] | ||
| 9 | fn baseline() { | ||
| 10 | for (pattern, exit_code) in BASELINE.iter() { | ||
| 11 | let res = gix_pathspec::parse(pattern, Default::default()); | ||
| 12 | assert_eq!( | ||
| 13 | res.is_ok(), | ||
| 14 | *exit_code == 0, | ||
| 15 | "{pattern:?} disagrees with baseline: {res:?}" | ||
| 16 | ); | ||
| 17 | if let Ok(pat) = res { | ||
| 18 | let actual = pat.to_bstring(); | ||
| 19 | assert_eq!( | ||
| 20 | pat, | ||
| 21 | gix_pathspec::parse(actual.as_ref(), Default::default()).expect("still valid"), | ||
| 22 | "{pattern} != {actual}: display must roundtrip into actual pattern" | ||
| 23 | ); | ||
| 24 | } | ||
| 25 | let p = gix_pathspec::Pattern::from_literal(pattern, Default::default()); | ||
| 26 | assert!(matches!(p.search_mode, MatchMode::Literal)); | ||
| 27 | } | ||
| 28 | } | ||
| 29 | |||
| 30 | mod invalid; | ||
| 31 | mod valid; | ||
| 32 | |||
| 33 | /// A way to specify expectations more easily by simplifying assignments. | ||
| 34 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 35 | struct NormalizedPattern { | ||
| 36 | path: BString, | ||
| 37 | signature: MagicSignature, | ||
| 38 | search_mode: MatchMode, | ||
| 39 | attributes: Vec<(BString, State)>, | ||
| 40 | } | ||
| 41 | |||
| 42 | impl From<Pattern> for NormalizedPattern { | ||
| 43 | fn from(p: Pattern) -> Self { | ||
| 44 | NormalizedPattern { | ||
| 45 | path: p.path().to_owned(), | ||
| 46 | signature: p.signature, | ||
| 47 | search_mode: p.search_mode, | ||
| 48 | attributes: p | ||
| 49 | .attributes | ||
| 50 | .into_iter() | ||
| 51 | .map(|attr| (attr.name.as_str().into(), attr.state)) | ||
| 52 | .collect(), | ||
| 53 | } | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | static BASELINE: Lazy<HashMap<BString, usize>> = Lazy::new(|| { | ||
| 58 | let base = gix_testtools::scripted_fixture_read_only("parse_baseline.sh").unwrap(); | ||
| 59 | |||
| 60 | (|| -> crate::Result<_> { | ||
| 61 | let mut map = HashMap::new(); | ||
| 62 | let baseline = std::fs::read(base.join("baseline.git"))?; | ||
| 63 | let mut lines = baseline.lines(); | ||
| 64 | while let Some(spec) = lines.next() { | ||
| 65 | let exit_code = lines.next().expect("two lines per baseline").to_str()?.parse()?; | ||
| 66 | map.insert(spec.into(), exit_code); | ||
| 67 | } | ||
| 68 | Ok(map) | ||
| 69 | })() | ||
| 70 | .unwrap() | ||
| 71 | }); | ||
| 72 | |||
| 73 | fn check_valid_inputs<'a>(inputs: impl IntoIterator<Item = (&'a str, NormalizedPattern)>) { | ||
| 74 | for (input, expected) in inputs.into_iter() { | ||
| 75 | assert!( | ||
| 76 | check_against_baseline(input), | ||
| 77 | "This pathspec is invalid in git: {input}" | ||
| 78 | ); | ||
| 79 | |||
| 80 | let pattern = gix_pathspec::parse(input.as_bytes(), Default::default()) | ||
| 81 | .unwrap_or_else(|_| panic!("parsing should not fail with pathspec {input}")); | ||
| 82 | let pattern: NormalizedPattern = pattern.into(); | ||
| 83 | assert_eq!(pattern, expected, "while checking input: \"{input}\""); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | fn check_against_baseline(pathspec: &str) -> bool { | ||
| 88 | let key: &BStr = pathspec.into(); | ||
| 89 | let base = BASELINE | ||
| 90 | .get(key) | ||
| 91 | .unwrap_or_else(|| panic!("missing baseline for pathspec: {pathspec:?}")); | ||
| 92 | *base == 0 | ||
| 93 | } | ||
| @@ -0,0 +1,329 @@ | |||
| 1 | use gix_attributes::State; | ||
| 2 | use gix_pathspec::{MagicSignature, MatchMode}; | ||
| 3 | |||
| 4 | use crate::parse::{check_against_baseline, check_valid_inputs, NormalizedPattern}; | ||
| 5 | |||
| 6 | #[test] | ||
| 7 | fn repeated_matcher_keywords() { | ||
| 8 | let input = vec![ | ||
| 9 | (":(glob,glob)", pat_with_search_mode(MatchMode::PathAwareGlob)), | ||
| 10 | (":(literal,literal)", pat_with_search_mode(MatchMode::Literal)), | ||
| 11 | (":(top,top)", pat_with_sig(MagicSignature::TOP)), | ||
| 12 | (":(icase,icase)", pat_with_sig(MagicSignature::ICASE)), | ||
| 13 | (":(attr,attr)", pat_with_attrs(vec![])), | ||
| 14 | (":!^(exclude,exclude)", pat_with_sig(MagicSignature::EXCLUDE)), | ||
| 15 | ]; | ||
| 16 | |||
| 17 | check_valid_inputs(input); | ||
| 18 | } | ||
| 19 | |||
| 20 | #[test] | ||
| 21 | fn glob_negations_are_always_literal() { | ||
| 22 | check_valid_inputs([("!a", pat_with_path("!a")), ("\\!a", pat_with_path("\\!a"))]); | ||
| 23 | } | ||
| 24 | |||
| 25 | #[test] | ||
| 26 | fn literal_default_prevents_parsing() { | ||
| 27 | let pattern = gix_pathspec::parse( | ||
| 28 | ":".as_bytes(), | ||
| 29 | gix_pathspec::parse::Defaults { | ||
| 30 | signature: MagicSignature::EXCLUDE, | ||
| 31 | search_mode: MatchMode::PathAwareGlob, | ||
| 32 | literal: true, | ||
| 33 | }, | ||
| 34 | ) | ||
| 35 | .expect("valid"); | ||
| 36 | assert!(!pattern.is_nil()); | ||
| 37 | assert_eq!(pattern.path(), ":"); | ||
| 38 | assert!(matches!(pattern.search_mode, MatchMode::Literal)); | ||
| 39 | |||
| 40 | let input = ":(literal)f[o][o]"; | ||
| 41 | let pattern = gix_pathspec::parse( | ||
| 42 | input.as_bytes(), | ||
| 43 | gix_pathspec::parse::Defaults { | ||
| 44 | signature: MagicSignature::TOP, | ||
| 45 | search_mode: MatchMode::Literal, | ||
| 46 | literal: true, | ||
| 47 | }, | ||
| 48 | ) | ||
| 49 | .expect("valid"); | ||
| 50 | assert_eq!(pattern.path(), input, "no parsing happens at all"); | ||
| 51 | assert!(matches!(pattern.search_mode, MatchMode::Literal)); | ||
| 52 | |||
| 53 | let pattern = gix_pathspec::parse( | ||
| 54 | input.as_bytes(), | ||
| 55 | gix_pathspec::parse::Defaults { | ||
| 56 | signature: MagicSignature::TOP, | ||
| 57 | search_mode: MatchMode::Literal, | ||
| 58 | literal: false, | ||
| 59 | }, | ||
| 60 | ) | ||
| 61 | .expect("valid"); | ||
| 62 | assert_eq!(pattern.path(), "f[o][o]", "in literal default mode, we still parse"); | ||
| 63 | assert!(matches!(pattern.search_mode, MatchMode::Literal)); | ||
| 64 | } | ||
| 65 | |||
| 66 | #[test] | ||
| 67 | fn there_is_no_pathspec_pathspec() { | ||
| 68 | check_against_baseline(":"); | ||
| 69 | let pattern = gix_pathspec::parse(":".as_bytes(), Default::default()).expect("valid"); | ||
| 70 | assert!(pattern.is_nil()); | ||
| 71 | |||
| 72 | let actual: NormalizedPattern = pattern.into(); | ||
| 73 | assert_eq!(actual, pat_with_path("")); | ||
| 74 | |||
| 75 | let pattern = gix_pathspec::parse( | ||
| 76 | ":".as_bytes(), | ||
| 77 | gix_pathspec::parse::Defaults { | ||
| 78 | signature: MagicSignature::EXCLUDE, | ||
| 79 | search_mode: MatchMode::PathAwareGlob, | ||
| 80 | literal: false, | ||
| 81 | }, | ||
| 82 | ) | ||
| 83 | .expect("valid"); | ||
| 84 | assert!(pattern.is_nil()); | ||
| 85 | } | ||
| 86 | |||
| 87 | #[test] | ||
| 88 | fn defaults_are_used() -> crate::Result { | ||
| 89 | let defaults = gix_pathspec::parse::Defaults { | ||
| 90 | signature: MagicSignature::EXCLUDE, | ||
| 91 | search_mode: MatchMode::Literal, | ||
| 92 | literal: false, | ||
| 93 | }; | ||
| 94 | let p = gix_pathspec::parse(".".as_bytes(), defaults)?; | ||
| 95 | assert_eq!(p.path(), "."); | ||
| 96 | assert_eq!(p.signature, defaults.signature); | ||
| 97 | assert_eq!(p.search_mode, defaults.search_mode); | ||
| 98 | assert!(p.attributes.is_empty()); | ||
| 99 | assert!(!p.is_nil()); | ||
| 100 | Ok(()) | ||
| 101 | } | ||
| 102 | |||
| 103 | #[test] | ||
| 104 | fn empty_signatures() { | ||
| 105 | let inputs = vec![ | ||
| 106 | (".", pat_with_path(".")), | ||
| 107 | ("some/path", pat_with_path("some/path")), | ||
| 108 | (":some/path", pat_with_path("some/path")), | ||
| 109 | (":()some/path", pat_with_path("some/path")), | ||
| 110 | ("::some/path", pat_with_path("some/path")), | ||
| 111 | (":::some/path", pat_with_path(":some/path")), | ||
| 112 | (":():some/path", pat_with_path(":some/path")), | ||
| 113 | ]; | ||
| 114 | |||
| 115 | check_valid_inputs(inputs) | ||
| 116 | } | ||
| 117 | |||
| 118 | #[test] | ||
| 119 | fn whitespace_in_pathspec() { | ||
| 120 | let inputs = vec![ | ||
| 121 | (" some/path", pat_with_path(" some/path")), | ||
| 122 | ("some/ path", pat_with_path("some/ path")), | ||
| 123 | ("some/path ", pat_with_path("some/path ")), | ||
| 124 | (": some/path", pat_with_path(" some/path")), | ||
| 125 | (": !some/path", pat_with_path(" !some/path")), | ||
| 126 | (": :some/path", pat_with_path(" :some/path")), | ||
| 127 | (": ()some/path", pat_with_path(" ()some/path")), | ||
| 128 | ( | ||
| 129 | ":! some/path", | ||
| 130 | pat_with_path_and_sig(" some/path", MagicSignature::EXCLUDE), | ||
| 131 | ), | ||
| 132 | ( | ||
| 133 | ":!!some/path", | ||
| 134 | pat_with_path_and_sig("some/path", MagicSignature::EXCLUDE), | ||
| 135 | ), | ||
| 136 | ]; | ||
| 137 | |||
| 138 | check_valid_inputs(inputs) | ||
| 139 | } | ||
| 140 | |||
| 141 | #[test] | ||
| 142 | fn short_signatures() { | ||
| 143 | let inputs = vec![ | ||
| 144 | (":/some/path", pat_with_path_and_sig("some/path", MagicSignature::TOP)), | ||
| 145 | ( | ||
| 146 | ":^some/path", | ||
| 147 | pat_with_path_and_sig("some/path", MagicSignature::EXCLUDE), | ||
| 148 | ), | ||
| 149 | ( | ||
| 150 | ":!some/path", | ||
| 151 | pat_with_path_and_sig("some/path", MagicSignature::EXCLUDE), | ||
| 152 | ), | ||
| 153 | ( | ||
| 154 | ":/!some/path", | ||
| 155 | pat_with_path_and_sig("some/path", MagicSignature::TOP | MagicSignature::EXCLUDE), | ||
| 156 | ), | ||
| 157 | ( | ||
| 158 | ":!/^/:some/path", | ||
| 159 | pat_with_path_and_sig("some/path", MagicSignature::TOP | MagicSignature::EXCLUDE), | ||
| 160 | ), | ||
| 161 | ]; | ||
| 162 | |||
| 163 | check_valid_inputs(inputs) | ||
| 164 | } | ||
| 165 | |||
| 166 | #[test] | ||
| 167 | fn trailing_slash_is_turned_into_magic_signature_and_removed() { | ||
| 168 | check_valid_inputs([ | ||
| 169 | ("a/b/", pat_with_path_and_sig("a/b", MagicSignature::MUST_BE_DIR)), | ||
| 170 | ("a/", pat_with_path_and_sig("a", MagicSignature::MUST_BE_DIR)), | ||
| 171 | ]); | ||
| 172 | } | ||
| 173 | |||
| 174 | #[test] | ||
| 175 | fn signatures_and_searchmodes() { | ||
| 176 | let inputs = vec![ | ||
| 177 | (":(top)", pat_with_sig(MagicSignature::TOP)), | ||
| 178 | (":(icase)", pat_with_sig(MagicSignature::ICASE)), | ||
| 179 | (":(attr)", pat_with_path("")), | ||
| 180 | (":(exclude)", pat_with_sig(MagicSignature::EXCLUDE)), | ||
| 181 | (":(literal)", pat_with_search_mode(MatchMode::Literal)), | ||
| 182 | (":(glob)", pat_with_search_mode(MatchMode::PathAwareGlob)), | ||
| 183 | ( | ||
| 184 | ":(top,exclude)", | ||
| 185 | pat_with_sig(MagicSignature::TOP | MagicSignature::EXCLUDE), | ||
| 186 | ), | ||
| 187 | ( | ||
| 188 | ":(icase,literal)", | ||
| 189 | pat("", MagicSignature::ICASE, MatchMode::Literal, vec![]), | ||
| 190 | ), | ||
| 191 | ( | ||
| 192 | ":!(literal)some/*path", | ||
| 193 | pat("some/*path", MagicSignature::EXCLUDE, MatchMode::Literal, vec![]), | ||
| 194 | ), | ||
| 195 | ( | ||
| 196 | ":(top,literal,icase,attr,exclude)some/path", | ||
| 197 | pat( | ||
| 198 | "some/path", | ||
| 199 | MagicSignature::TOP | MagicSignature::EXCLUDE | MagicSignature::ICASE, | ||
| 200 | MatchMode::Literal, | ||
| 201 | vec![], | ||
| 202 | ), | ||
| 203 | ), | ||
| 204 | ( | ||
| 205 | ":(top,glob,icase,attr,exclude)some/path", | ||
| 206 | pat( | ||
| 207 | "some/path", | ||
| 208 | MagicSignature::TOP | MagicSignature::EXCLUDE | MagicSignature::ICASE, | ||
| 209 | MatchMode::PathAwareGlob, | ||
| 210 | vec![], | ||
| 211 | ), | ||
| 212 | ), | ||
| 213 | ]; | ||
| 214 | |||
| 215 | check_valid_inputs(inputs); | ||
| 216 | } | ||
| 217 | |||
| 218 | #[test] | ||
| 219 | fn attributes_in_signature() { | ||
| 220 | let inputs = vec![ | ||
| 221 | (":(attr:someAttr)", pat_with_attrs(vec![("someAttr", State::Set)])), | ||
| 222 | ( | ||
| 223 | ":(attr:!someAttr)", | ||
| 224 | pat_with_attrs(vec![("someAttr", State::Unspecified)]), | ||
| 225 | ), | ||
| 226 | (":(attr:-someAttr)", pat_with_attrs(vec![("someAttr", State::Unset)])), | ||
| 227 | ( | ||
| 228 | ":(attr:someAttr=value)", | ||
| 229 | pat_with_attrs(vec![("someAttr", State::Value("value".into()))]), | ||
| 230 | ), | ||
| 231 | ( | ||
| 232 | ":(attr:a=one b=)", | ||
| 233 | pat_with_attrs(vec![("a", State::Value("one".into())), ("b", State::Value("".into()))]), | ||
| 234 | ), | ||
| 235 | ( | ||
| 236 | ":(attr:a= b=two)", | ||
| 237 | pat_with_attrs(vec![("a", State::Value("".into())), ("b", State::Value("two".into()))]), | ||
| 238 | ), | ||
| 239 | ( | ||
| 240 | ":(attr:a=one b=two)", | ||
| 241 | pat_with_attrs(vec![ | ||
| 242 | ("a", State::Value("one".into())), | ||
| 243 | ("b", State::Value("two".into())), | ||
| 244 | ]), | ||
| 245 | ), | ||
| 246 | ( | ||
| 247 | ":(attr:a=one b=two)", | ||
| 248 | pat_with_attrs(vec![ | ||
| 249 | ("a", State::Value("one".into())), | ||
| 250 | ("b", State::Value("two".into())), | ||
| 251 | ]), | ||
| 252 | ), | ||
| 253 | ( | ||
| 254 | ":(attr:someAttr anotherAttr)", | ||
| 255 | pat_with_attrs(vec![("someAttr", State::Set), ("anotherAttr", State::Set)]), | ||
| 256 | ), | ||
| 257 | ]; | ||
| 258 | |||
| 259 | check_valid_inputs(inputs) | ||
| 260 | } | ||
| 261 | |||
| 262 | #[test] | ||
| 263 | fn attributes_with_escape_chars_in_state_values() { | ||
| 264 | let inputs = vec![ | ||
| 265 | ( | ||
| 266 | r":(attr:v=one\-)", | ||
| 267 | pat_with_attrs(vec![("v", State::Value(r"one-".into()))]), | ||
| 268 | ), | ||
| 269 | ( | ||
| 270 | r":(attr:v=one\_)", | ||
| 271 | pat_with_attrs(vec![("v", State::Value(r"one_".into()))]), | ||
| 272 | ), | ||
| 273 | ( | ||
| 274 | r":(attr:v=one\,)", | ||
| 275 | pat_with_attrs(vec![("v", State::Value(r"one,".into()))]), | ||
| 276 | ), | ||
| 277 | ( | ||
| 278 | r":(attr:v=one\,two\,three)", | ||
| 279 | pat_with_attrs(vec![("v", State::Value(r"one,two,three".into()))]), | ||
| 280 | ), | ||
| 281 | ( | ||
| 282 | r":(attr:a=\d b= c=\d)", | ||
| 283 | pat_with_attrs(vec![ | ||
| 284 | ("a", State::Value(r"d".into())), | ||
| 285 | ("b", State::Value(r"".into())), | ||
| 286 | ("c", State::Value(r"d".into())), | ||
| 287 | ]), | ||
| 288 | ), | ||
| 289 | ]; | ||
| 290 | |||
| 291 | check_valid_inputs(inputs) | ||
| 292 | } | ||
| 293 | |||
| 294 | fn pat_with_path(path: &str) -> NormalizedPattern { | ||
| 295 | pat_with_path_and_sig(path, MagicSignature::empty()) | ||
| 296 | } | ||
| 297 | |||
| 298 | fn pat_with_path_and_sig(path: &str, signature: MagicSignature) -> NormalizedPattern { | ||
| 299 | pat(path, signature, MatchMode::ShellGlob, vec![]) | ||
| 300 | } | ||
| 301 | |||
| 302 | fn pat_with_sig(signature: MagicSignature) -> NormalizedPattern { | ||
| 303 | pat("", signature, MatchMode::ShellGlob, vec![]) | ||
| 304 | } | ||
| 305 | |||
| 306 | fn pat_with_attrs(attrs: Vec<(&'static str, State)>) -> NormalizedPattern { | ||
| 307 | pat("", MagicSignature::empty(), MatchMode::ShellGlob, attrs) | ||
| 308 | } | ||
| 309 | |||
| 310 | fn pat_with_search_mode(search_mode: MatchMode) -> NormalizedPattern { | ||
| 311 | pat("", MagicSignature::empty(), search_mode, vec![]) | ||
| 312 | } | ||
| 313 | |||
| 314 | fn pat( | ||
| 315 | path: &str, | ||
| 316 | signature: MagicSignature, | ||
| 317 | search_mode: MatchMode, | ||
| 318 | attributes: Vec<(&str, State)>, | ||
| 319 | ) -> NormalizedPattern { | ||
| 320 | NormalizedPattern { | ||
| 321 | path: path.into(), | ||
| 322 | signature, | ||
| 323 | search_mode, | ||
| 324 | attributes: attributes | ||
| 325 | .into_iter() | ||
| 326 | .map(|(attr, state)| (attr.into(), state)) | ||
| 327 | .collect(), | ||
| 328 | } | ||
| 329 | } | ||
| @@ -1,473 +1,5 @@ | |||
| 1 | pub use gix_testtools::Result; | 1 | pub use gix_testtools::Result; |
| 2 | 2 | ||
| 3 | mod parse { | 3 | mod normalize; |
| 4 | use std::collections::HashMap; | 4 | mod parse; |
| 5 | 5 | mod search; | |
| 6 | use bstr::{BStr, BString, ByteSlice}; | ||
| 7 | use gix_attributes::State; | ||
| 8 | use gix_pathspec::{MagicSignature, MatchMode, Pattern}; | ||
| 9 | use once_cell::sync::Lazy; | ||
| 10 | |||
| 11 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 12 | struct PatternForTesting { | ||
| 13 | path: BString, | ||
| 14 | signature: MagicSignature, | ||
| 15 | search_mode: MatchMode, | ||
| 16 | attributes: Vec<(BString, State)>, | ||
| 17 | } | ||
| 18 | |||
| 19 | impl From<Pattern> for PatternForTesting { | ||
| 20 | fn from(p: Pattern) -> Self { | ||
| 21 | PatternForTesting { | ||
| 22 | path: p.path, | ||
| 23 | signature: p.signature, | ||
| 24 | search_mode: p.search_mode, | ||
| 25 | attributes: p | ||
| 26 | .attributes | ||
| 27 | .into_iter() | ||
| 28 | .map(|attr| (attr.name.as_str().into(), attr.state)) | ||
| 29 | .collect(), | ||
| 30 | } | ||
| 31 | } | ||
| 32 | } | ||
| 33 | |||
| 34 | static BASELINE: Lazy<HashMap<BString, usize>> = Lazy::new(|| { | ||
| 35 | let base = gix_testtools::scripted_fixture_read_only("generate_pathspec_baseline.sh").unwrap(); | ||
| 36 | |||
| 37 | (|| -> crate::Result<_> { | ||
| 38 | let mut map = HashMap::new(); | ||
| 39 | let baseline = std::fs::read(base.join("baseline.git"))?; | ||
| 40 | let mut lines = baseline.lines(); | ||
| 41 | while let Some(spec) = lines.next() { | ||
| 42 | let exit_code = lines.next().expect("two lines per baseline").to_str()?.parse()?; | ||
| 43 | map.insert(spec.into(), exit_code); | ||
| 44 | } | ||
| 45 | Ok(map) | ||
| 46 | })() | ||
| 47 | .unwrap() | ||
| 48 | }); | ||
| 49 | |||
| 50 | #[test] | ||
| 51 | fn baseline() { | ||
| 52 | for (pattern, exit_code) in BASELINE.iter() { | ||
| 53 | let res = gix_pathspec::parse(pattern); | ||
| 54 | assert_eq!( | ||
| 55 | res.is_ok(), | ||
| 56 | *exit_code == 0, | ||
| 57 | "{pattern:?} disagrees with baseline: {res:?}" | ||
| 58 | ) | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | mod succeed { | ||
| 63 | use gix_attributes::State; | ||
| 64 | use gix_pathspec::{MagicSignature, MatchMode}; | ||
| 65 | |||
| 66 | use crate::parse::{ | ||
| 67 | check_valid_inputs, pat, pat_with_attrs, pat_with_path, pat_with_path_and_sig, pat_with_search_mode, | ||
| 68 | pat_with_sig, | ||
| 69 | }; | ||
| 70 | |||
| 71 | #[test] | ||
| 72 | fn there_is_no_pathspec_pathspec() { | ||
| 73 | check_valid_inputs(Some((":", pat_with_attrs(vec![])))); | ||
| 74 | } | ||
| 75 | |||
| 76 | #[test] | ||
| 77 | fn repeated_matcher_keywords() { | ||
| 78 | let input = vec![ | ||
| 79 | (":(glob,glob)", pat_with_search_mode(MatchMode::PathAwareGlob)), | ||
| 80 | (":(literal,literal)", pat_with_search_mode(MatchMode::Literal)), | ||
| 81 | (":(top,top)", pat_with_sig(MagicSignature::TOP)), | ||
| 82 | (":(icase,icase)", pat_with_sig(MagicSignature::ICASE)), | ||
| 83 | (":(attr,attr)", pat_with_attrs(vec![])), | ||
| 84 | (":!^(exclude,exclude)", pat_with_sig(MagicSignature::EXCLUDE)), | ||
| 85 | ]; | ||
| 86 | |||
| 87 | check_valid_inputs(input); | ||
| 88 | } | ||
| 89 | |||
| 90 | #[test] | ||
| 91 | fn empty_signatures() { | ||
| 92 | let inputs = vec![ | ||
| 93 | (".", pat_with_path(".")), | ||
| 94 | ("some/path", pat_with_path("some/path")), | ||
| 95 | (":some/path", pat_with_path("some/path")), | ||
| 96 | (":()some/path", pat_with_path("some/path")), | ||
| 97 | ("::some/path", pat_with_path("some/path")), | ||
| 98 | (":::some/path", pat_with_path(":some/path")), | ||
| 99 | (":():some/path", pat_with_path(":some/path")), | ||
| 100 | ]; | ||
| 101 | |||
| 102 | check_valid_inputs(inputs) | ||
| 103 | } | ||
| 104 | |||
| 105 | #[test] | ||
| 106 | fn whitespace_in_pathspec() { | ||
| 107 | let inputs = vec![ | ||
| 108 | (" some/path", pat_with_path(" some/path")), | ||
| 109 | ("some/ path", pat_with_path("some/ path")), | ||
| 110 | ("some/path ", pat_with_path("some/path ")), | ||
| 111 | (": some/path", pat_with_path(" some/path")), | ||
| 112 | (": !some/path", pat_with_path(" !some/path")), | ||
| 113 | (": :some/path", pat_with_path(" :some/path")), | ||
| 114 | (": ()some/path", pat_with_path(" ()some/path")), | ||
| 115 | ( | ||
| 116 | ":! some/path", | ||
| 117 | pat_with_path_and_sig(" some/path", MagicSignature::EXCLUDE), | ||
| 118 | ), | ||
| 119 | ]; | ||
| 120 | |||
| 121 | check_valid_inputs(inputs) | ||
| 122 | } | ||
| 123 | |||
| 124 | #[test] | ||
| 125 | fn short_signatures() { | ||
| 126 | let inputs = vec![ | ||
| 127 | (":/some/path", pat_with_path_and_sig("some/path", MagicSignature::TOP)), | ||
| 128 | ( | ||
| 129 | ":^some/path", | ||
| 130 | pat_with_path_and_sig("some/path", MagicSignature::EXCLUDE), | ||
| 131 | ), | ||
| 132 | ( | ||
| 133 | ":!some/path", | ||
| 134 | pat_with_path_and_sig("some/path", MagicSignature::EXCLUDE), | ||
| 135 | ), | ||
| 136 | ( | ||
| 137 | ":/!some/path", | ||
| 138 | pat_with_path_and_sig("some/path", MagicSignature::TOP | MagicSignature::EXCLUDE), | ||
| 139 | ), | ||
| 140 | ( | ||
| 141 | ":!/^/:some/path", | ||
| 142 | pat_with_path_and_sig("some/path", MagicSignature::TOP | MagicSignature::EXCLUDE), | ||
| 143 | ), | ||
| 144 | ]; | ||
| 145 | |||
| 146 | check_valid_inputs(inputs) | ||
| 147 | } | ||
| 148 | |||
| 149 | #[test] | ||
| 150 | fn signatures_and_searchmodes() { | ||
| 151 | let inputs = vec![ | ||
| 152 | (":(top)", pat_with_sig(MagicSignature::TOP)), | ||
| 153 | (":(icase)", pat_with_sig(MagicSignature::ICASE)), | ||
| 154 | (":(attr)", pat_with_path("")), | ||
| 155 | (":(exclude)", pat_with_sig(MagicSignature::EXCLUDE)), | ||
| 156 | (":(literal)", pat_with_search_mode(MatchMode::Literal)), | ||
| 157 | (":(glob)", pat_with_search_mode(MatchMode::PathAwareGlob)), | ||
| 158 | ( | ||
| 159 | ":(top,exclude)", | ||
| 160 | pat_with_sig(MagicSignature::TOP | MagicSignature::EXCLUDE), | ||
| 161 | ), | ||
| 162 | ( | ||
| 163 | ":(icase,literal)", | ||
| 164 | pat("", MagicSignature::ICASE, MatchMode::Literal, vec![]), | ||
| 165 | ), | ||
| 166 | ( | ||
| 167 | ":!(literal)some/*path", | ||
| 168 | pat("some/*path", MagicSignature::EXCLUDE, MatchMode::Literal, vec![]), | ||
| 169 | ), | ||
| 170 | ( | ||
| 171 | ":(top,literal,icase,attr,exclude)some/path", | ||
| 172 | pat("some/path", MagicSignature::all(), MatchMode::Literal, vec![]), | ||
| 173 | ), | ||
| 174 | ( | ||
| 175 | ":(top,glob,icase,attr,exclude)some/path", | ||
| 176 | pat("some/path", MagicSignature::all(), MatchMode::PathAwareGlob, vec![]), | ||
| 177 | ), | ||
| 178 | ]; | ||
| 179 | |||
| 180 | check_valid_inputs(inputs); | ||
| 181 | } | ||
| 182 | |||
| 183 | #[test] | ||
| 184 | fn attributes_in_signature() { | ||
| 185 | let inputs = vec![ | ||
| 186 | (":(attr:someAttr)", pat_with_attrs(vec![("someAttr", State::Set)])), | ||
| 187 | ( | ||
| 188 | ":(attr:!someAttr)", | ||
| 189 | pat_with_attrs(vec![("someAttr", State::Unspecified)]), | ||
| 190 | ), | ||
| 191 | (":(attr:-someAttr)", pat_with_attrs(vec![("someAttr", State::Unset)])), | ||
| 192 | ( | ||
| 193 | ":(attr:someAttr=value)", | ||
| 194 | pat_with_attrs(vec![("someAttr", State::Value("value".into()))]), | ||
| 195 | ), | ||
| 196 | ( | ||
| 197 | ":(attr:a=one b=)", | ||
| 198 | pat_with_attrs(vec![("a", State::Value("one".into())), ("b", State::Value("".into()))]), | ||
| 199 | ), | ||
| 200 | ( | ||
| 201 | ":(attr:a= b=two)", | ||
| 202 | pat_with_attrs(vec![("a", State::Value("".into())), ("b", State::Value("two".into()))]), | ||
| 203 | ), | ||
| 204 | ( | ||
| 205 | ":(attr:a=one b=two)", | ||
| 206 | pat_with_attrs(vec![ | ||
| 207 | ("a", State::Value("one".into())), | ||
| 208 | ("b", State::Value("two".into())), | ||
| 209 | ]), | ||
| 210 | ), | ||
| 211 | ( | ||
| 212 | ":(attr:a=one b=two)", | ||
| 213 | pat_with_attrs(vec![ | ||
| 214 | ("a", State::Value("one".into())), | ||
| 215 | ("b", State::Value("two".into())), | ||
| 216 | ]), | ||
| 217 | ), | ||
| 218 | ( | ||
| 219 | ":(attr:someAttr anotherAttr)", | ||
| 220 | pat_with_attrs(vec![("someAttr", State::Set), ("anotherAttr", State::Set)]), | ||
| 221 | ), | ||
| 222 | ]; | ||
| 223 | |||
| 224 | check_valid_inputs(inputs) | ||
| 225 | } | ||
| 226 | |||
| 227 | #[test] | ||
| 228 | fn attributes_with_escape_chars_in_state_values() { | ||
| 229 | let inputs = vec![ | ||
| 230 | ( | ||
| 231 | r":(attr:v=one\-)", | ||
| 232 | pat_with_attrs(vec![("v", State::Value(r"one-".into()))]), | ||
| 233 | ), | ||
| 234 | ( | ||
| 235 | r":(attr:v=one\_)", | ||
| 236 | pat_with_attrs(vec![("v", State::Value(r"one_".into()))]), | ||
| 237 | ), | ||
| 238 | ( | ||
| 239 | r":(attr:v=one\,)", | ||
| 240 | pat_with_attrs(vec![("v", State::Value(r"one,".into()))]), | ||
| 241 | ), | ||
| 242 | ( | ||
| 243 | r":(attr:v=one\,two\,three)", | ||
| 244 | pat_with_attrs(vec![("v", State::Value(r"one,two,three".into()))]), | ||
| 245 | ), | ||
| 246 | ( | ||
| 247 | r":(attr:a=\d b= c=\d)", | ||
| 248 | pat_with_attrs(vec![ | ||
| 249 | ("a", State::Value(r"d".into())), | ||
| 250 | ("b", State::Value(r"".into())), | ||
| 251 | ("c", State::Value(r"d".into())), | ||
| 252 | ]), | ||
| 253 | ), | ||
| 254 | ]; | ||
| 255 | |||
| 256 | check_valid_inputs(inputs) | ||
| 257 | } | ||
| 258 | } | ||
| 259 | |||
| 260 | mod fail { | ||
| 261 | use gix_pathspec::parse::Error; | ||
| 262 | |||
| 263 | use crate::parse::check_against_baseline; | ||
| 264 | |||
| 265 | #[test] | ||
| 266 | fn empty_input() { | ||
| 267 | let input = ""; | ||
| 268 | |||
| 269 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 270 | |||
| 271 | let output = gix_pathspec::parse(input.as_bytes()); | ||
| 272 | assert!(output.is_err()); | ||
| 273 | assert!(matches!(output.unwrap_err(), Error::EmptyString)); | ||
| 274 | } | ||
| 275 | |||
| 276 | #[test] | ||
| 277 | fn invalid_short_signatures() { | ||
| 278 | let inputs = vec![ | ||
| 279 | ":\"()", ":#()", ":%()", ":&()", ":'()", ":,()", ":-()", ":;()", ":<()", ":=()", ":>()", ":@()", | ||
| 280 | ":_()", ":`()", ":~()", | ||
| 281 | ]; | ||
| 282 | |||
| 283 | for input in inputs.into_iter() { | ||
| 284 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 285 | |||
| 286 | let output = gix_pathspec::parse(input.as_bytes()); | ||
| 287 | assert!(output.is_err()); | ||
| 288 | assert!(matches!(output.unwrap_err(), Error::Unimplemented { .. })); | ||
| 289 | } | ||
| 290 | } | ||
| 291 | |||
| 292 | #[test] | ||
| 293 | fn invalid_keywords() { | ||
| 294 | let inputs = vec![ | ||
| 295 | ":( )some/path", | ||
| 296 | ":(tp)some/path", | ||
| 297 | ":(top, exclude)some/path", | ||
| 298 | ":(top,exclude,icse)some/path", | ||
| 299 | ]; | ||
| 300 | |||
| 301 | for input in inputs.into_iter() { | ||
| 302 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 303 | |||
| 304 | let output = gix_pathspec::parse(input.as_bytes()); | ||
| 305 | assert!(output.is_err()); | ||
| 306 | assert!(matches!(output.unwrap_err(), Error::InvalidKeyword { .. })); | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | #[test] | ||
| 311 | fn invalid_attributes() { | ||
| 312 | let inputs = vec![ | ||
| 313 | ":(attr:+invalidAttr)some/path", | ||
| 314 | ":(attr:validAttr +invalidAttr)some/path", | ||
| 315 | ":(attr:+invalidAttr,attr:valid)some/path", | ||
| 316 | r":(attr:inva\lid)some/path", | ||
| 317 | ]; | ||
| 318 | |||
| 319 | for input in inputs { | ||
| 320 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 321 | |||
| 322 | let output = gix_pathspec::parse(input.as_bytes()); | ||
| 323 | assert!(output.is_err(), "This pathspec did not produce an error {input}"); | ||
| 324 | assert!(matches!(output.unwrap_err(), Error::InvalidAttribute { .. })); | ||
| 325 | } | ||
| 326 | } | ||
| 327 | |||
| 328 | #[test] | ||
| 329 | fn invalid_attribute_values() { | ||
| 330 | let inputs = vec![ | ||
| 331 | r":(attr:v=inva#lid)some/path", | ||
| 332 | r":(attr:v=inva\\lid)some/path", | ||
| 333 | r":(attr:v=invalid\\)some/path", | ||
| 334 | r":(attr:v=invalid\#)some/path", | ||
| 335 | r":(attr:v=inva\=lid)some/path", | ||
| 336 | r":(attr:a=valid b=inva\#lid)some/path", | ||
| 337 | ":(attr:v=val��)", | ||
| 338 | ":(attr:pr=pre��x:,)�", | ||
| 339 | ]; | ||
| 340 | |||
| 341 | for input in inputs { | ||
| 342 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 343 | |||
| 344 | let output = gix_pathspec::parse(input.as_bytes()); | ||
| 345 | assert!(output.is_err(), "This pathspec did not produce an error {input}"); | ||
| 346 | assert!( | ||
| 347 | matches!(output.unwrap_err(), Error::InvalidAttributeValue { .. }), | ||
| 348 | "Errors did not match for pathspec: {input}" | ||
| 349 | ); | ||
| 350 | } | ||
| 351 | } | ||
| 352 | |||
| 353 | #[test] | ||
| 354 | fn escape_character_at_end_of_attribute_value() { | ||
| 355 | let inputs = vec![ | ||
| 356 | r":(attr:v=invalid\)some/path", | ||
| 357 | r":(attr:v=invalid\ )some/path", | ||
| 358 | r":(attr:v=invalid\ valid)some/path", | ||
| 359 | ]; | ||
| 360 | |||
| 361 | for input in inputs { | ||
| 362 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 363 | |||
| 364 | let output = gix_pathspec::parse(input.as_bytes()); | ||
| 365 | assert!(output.is_err(), "This pathspec did not produce an error {input}"); | ||
| 366 | assert!(matches!(output.unwrap_err(), Error::TrailingEscapeCharacter)); | ||
| 367 | } | ||
| 368 | } | ||
| 369 | |||
| 370 | #[test] | ||
| 371 | fn empty_attribute_specification() { | ||
| 372 | let input = ":(attr:)"; | ||
| 373 | |||
| 374 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 375 | |||
| 376 | let output = gix_pathspec::parse(input.as_bytes()); | ||
| 377 | assert!(output.is_err()); | ||
| 378 | assert!(matches!(output.unwrap_err(), Error::EmptyAttribute)); | ||
| 379 | } | ||
| 380 | |||
| 381 | #[test] | ||
| 382 | fn multiple_attribute_specifications() { | ||
| 383 | let input = ":(attr:one,attr:two)some/path"; | ||
| 384 | |||
| 385 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 386 | |||
| 387 | let output = gix_pathspec::parse(input.as_bytes()); | ||
| 388 | assert!(output.is_err()); | ||
| 389 | assert!(matches!(output.unwrap_err(), Error::MultipleAttributeSpecifications)); | ||
| 390 | } | ||
| 391 | |||
| 392 | #[test] | ||
| 393 | fn missing_parentheses() { | ||
| 394 | let input = ":(top"; | ||
| 395 | |||
| 396 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 397 | |||
| 398 | let output = gix_pathspec::parse(input.as_bytes()); | ||
| 399 | assert!(output.is_err()); | ||
| 400 | assert!(matches!(output.unwrap_err(), Error::MissingClosingParenthesis { .. })); | ||
| 401 | } | ||
| 402 | |||
| 403 | #[test] | ||
| 404 | fn glob_and_literal_keywords_present() { | ||
| 405 | let input = ":(glob,literal)some/path"; | ||
| 406 | |||
| 407 | assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); | ||
| 408 | |||
| 409 | let output = gix_pathspec::parse(input.as_bytes()); | ||
| 410 | assert!(output.is_err()); | ||
| 411 | assert!(matches!(output.unwrap_err(), Error::IncompatibleSearchModes)); | ||
| 412 | } | ||
| 413 | } | ||
| 414 | |||
| 415 | fn check_valid_inputs<'a>(inputs: impl IntoIterator<Item = (&'a str, PatternForTesting)>) { | ||
| 416 | for (input, expected) in inputs.into_iter() { | ||
| 417 | assert!( | ||
| 418 | check_against_baseline(input), | ||
| 419 | "This pathspec is invalid in git: {input}" | ||
| 420 | ); | ||
| 421 | |||
| 422 | let pattern: PatternForTesting = gix_pathspec::parse(input.as_bytes()) | ||
| 423 | .unwrap_or_else(|_| panic!("parsing should not fail with pathspec {input}")) | ||
| 424 | .into(); | ||
| 425 | assert_eq!(pattern, expected, "while checking input: \"{input}\""); | ||
| 426 | } | ||
| 427 | } | ||
| 428 | |||
| 429 | fn pat_with_path(path: &str) -> PatternForTesting { | ||
| 430 | pat_with_path_and_sig(path, MagicSignature::empty()) | ||
| 431 | } | ||
| 432 | |||
| 433 | fn pat_with_path_and_sig(path: &str, signature: MagicSignature) -> PatternForTesting { | ||
| 434 | pat(path, signature, MatchMode::ShellGlob, vec![]) | ||
| 435 | } | ||
| 436 | |||
| 437 | fn pat_with_sig(signature: MagicSignature) -> PatternForTesting { | ||
| 438 | pat("", signature, MatchMode::ShellGlob, vec![]) | ||
| 439 | } | ||
| 440 | |||
| 441 | fn pat_with_attrs(attrs: Vec<(&'static str, State)>) -> PatternForTesting { | ||
| 442 | pat("", MagicSignature::empty(), MatchMode::ShellGlob, attrs) | ||
| 443 | } | ||
| 444 | |||
| 445 | fn pat_with_search_mode(search_mode: MatchMode) -> PatternForTesting { | ||
| 446 | pat("", MagicSignature::empty(), search_mode, vec![]) | ||
| 447 | } | ||
| 448 | |||
| 449 | fn pat( | ||
| 450 | path: &str, | ||
| 451 | signature: MagicSignature, | ||
| 452 | search_mode: MatchMode, | ||
| 453 | attributes: Vec<(&str, State)>, | ||
| 454 | ) -> PatternForTesting { | ||
| 455 | PatternForTesting { | ||
| 456 | path: path.into(), | ||
| 457 | signature, | ||
| 458 | search_mode, | ||
| 459 | attributes: attributes | ||
| 460 | .into_iter() | ||
| 461 | .map(|(attr, state)| (attr.into(), state)) | ||
| 462 | .collect(), | ||
| 463 | } | ||
| 464 | } | ||
| 465 | |||
| 466 | fn check_against_baseline(pathspec: &str) -> bool { | ||
| 467 | let key: &BStr = pathspec.into(); | ||
| 468 | let base = BASELINE | ||
| 469 | .get(key) | ||
| 470 | .unwrap_or_else(|| panic!("missing baseline for pathspec: {pathspec:?}")); | ||
| 471 | *base == 0 | ||
| 472 | } | ||
| 473 | } | ||
| @@ -0,0 +1,257 @@ | |||
| 1 | use std::path::Path; | ||
| 2 | |||
| 3 | #[test] | ||
| 4 | fn directories() -> crate::Result { | ||
| 5 | baseline::run("directory", true, baseline::directories) | ||
| 6 | } | ||
| 7 | |||
| 8 | #[test] | ||
| 9 | fn no_pathspecs_match_everything() -> crate::Result { | ||
| 10 | let mut search = gix_pathspec::Search::from_specs([], None, Path::new(""))?; | ||
| 11 | assert_eq!(search.patterns().count(), 0, "nothing artificial is added"); | ||
| 12 | let m = search | ||
| 13 | .pattern_matching_relative_path("hello", None, |_, _, _, _| unreachable!("must not be called")) | ||
| 14 | .expect("matches"); | ||
| 15 | assert_eq!(m.pattern.prefix_directory(), "", "there is no prefix as none was given"); | ||
| 16 | |||
| 17 | Ok(()) | ||
| 18 | } | ||
| 19 | |||
| 20 | #[test] | ||
| 21 | fn no_pathspecs_respect_prefix() -> crate::Result { | ||
| 22 | let mut search = gix_pathspec::Search::from_specs([], Some(Path::new("a")), Path::new(""))?; | ||
| 23 | assert_eq!( | ||
| 24 | search.patterns().count(), | ||
| 25 | 1, | ||
| 26 | "we get an artificial pattern to get the prefix" | ||
| 27 | ); | ||
| 28 | assert!( | ||
| 29 | search | ||
| 30 | .pattern_matching_relative_path("hello", None, |_, _, _, _| unreachable!("must not be called")) | ||
| 31 | .is_none(), | ||
| 32 | "not the right prefix" | ||
| 33 | ); | ||
| 34 | let m = search | ||
| 35 | .pattern_matching_relative_path("a/b", None, |_, _, _, _| unreachable!("must not be called")) | ||
| 36 | .expect("match"); | ||
| 37 | assert_eq!( | ||
| 38 | m.pattern.prefix_directory(), | ||
| 39 | "a", | ||
| 40 | "the prefix directory matched verbatim" | ||
| 41 | ); | ||
| 42 | |||
| 43 | Ok(()) | ||
| 44 | } | ||
| 45 | |||
| 46 | #[test] | ||
| 47 | fn prefixes_are_always_case_insensitive() -> crate::Result { | ||
| 48 | let path = gix_testtools::scripted_fixture_read_only("match_baseline_files.sh")?.join("paths"); | ||
| 49 | let items = baseline::parse_paths(path)?; | ||
| 50 | |||
| 51 | for (spec, prefix, common_prefix, expected) in [ | ||
| 52 | (":(icase)bar", "FOO", "FOO", &["FOO/BAR", "FOO/bAr", "FOO/bar"] as &[_]), | ||
| 53 | (":(icase)bar", "F", "F", &[]), | ||
| 54 | (":(icase)bar", "FO", "FO", &[]), | ||
| 55 | (":(icase)../bar", "fOo", "", &["BAR", "bAr", "bar"]), | ||
| 56 | ("../bar", "fOo", "bar", &["bar"]), | ||
| 57 | (" ", "", " ", &[" "]), // whitespace can match verbatim | ||
| 58 | (" hi*", "", " hi", &[" hi "]), // whitespace can match with globs as well | ||
| 59 | (":(icase)../bar", "fO", "", &["BAR", "bAr", "bar"]), // prefixes are virtual, and don't have to exist at all. | ||
| 60 | ( | ||
| 61 | ":(icase)../foo/bar", | ||
| 62 | "FOO", | ||
| 63 | "", | ||
| 64 | &[ | ||
| 65 | "FOO/BAR", "FOO/bAr", "FOO/bar", "fOo/BAR", "fOo/bAr", "fOo/bar", "foo/BAR", "foo/bAr", "foo/bar", | ||
| 66 | ], | ||
| 67 | ), | ||
| 68 | ("../foo/bar", "FOO", "foo/bar", &["foo/bar"]), | ||
| 69 | ( | ||
| 70 | ":(icase)../foo/../fOo/bar", | ||
| 71 | "FOO", | ||
| 72 | "", | ||
| 73 | &[ | ||
| 74 | "FOO/BAR", "FOO/bAr", "FOO/bar", "fOo/BAR", "fOo/bAr", "fOo/bar", "foo/BAR", "foo/bAr", "foo/bar", | ||
| 75 | ], | ||
| 76 | ), | ||
| 77 | ("../foo/../fOo/BAR", "FOO", "fOo/BAR", &["fOo/BAR"]), | ||
| 78 | ] { | ||
| 79 | let mut search = gix_pathspec::Search::from_specs( | ||
| 80 | gix_pathspec::parse(spec.as_bytes(), Default::default()), | ||
| 81 | Some(Path::new(prefix)), | ||
| 82 | Path::new(""), | ||
| 83 | )?; | ||
| 84 | assert_eq!(search.common_prefix(), common_prefix, "{spec} {prefix}"); | ||
| 85 | let actual: Vec<_> = items | ||
| 86 | .iter() | ||
| 87 | .filter(|relative_path| { | ||
| 88 | search | ||
| 89 | .pattern_matching_relative_path(relative_path.as_str(), Some(false), |_, _, _, _| false) | ||
| 90 | .is_some() | ||
| 91 | }) | ||
| 92 | .collect(); | ||
| 93 | assert_eq!(actual, expected, "{spec} {prefix}"); | ||
| 94 | } | ||
| 95 | Ok(()) | ||
| 96 | } | ||
| 97 | |||
| 98 | #[test] | ||
| 99 | fn common_prefix() -> crate::Result { | ||
| 100 | for (specs, prefix, expected) in [ | ||
| 101 | (&["foo/bar", ":(icase)foo/bar"] as &[_], None, ""), | ||
| 102 | (&["foo/bar", "foo"], None, "foo"), | ||
| 103 | (&["foo/bar/baz", "foo/bar/"], None, "foo/bar"), // directory trailing slashes are ignored, but that prefix shouldn't care anyway | ||
| 104 | (&[":(icase)bar", ":(icase)bart"], Some("foo"), "foo"), // only case-sensitive portions count | ||
| 105 | (&["bar", "bart"], Some("foo"), "foo/bar"), // otherwise everything that matches counts | ||
| 106 | (&["bar", "bart", "ba"], Some("foo"), "foo/ba"), | ||
| 107 | ] { | ||
| 108 | let search = gix_pathspec::Search::from_specs( | ||
| 109 | specs | ||
| 110 | .iter() | ||
| 111 | .map(|s| gix_pathspec::parse(s.as_bytes(), Default::default()).expect("valid")), | ||
| 112 | prefix.map(Path::new), | ||
| 113 | Path::new(""), | ||
| 114 | )?; | ||
| 115 | assert_eq!(search.common_prefix(), expected, "{specs:?} {prefix:?}"); | ||
| 116 | } | ||
| 117 | Ok(()) | ||
| 118 | } | ||
| 119 | |||
| 120 | #[test] | ||
| 121 | fn files() -> crate::Result { | ||
| 122 | baseline::run("file", false, baseline::files) | ||
| 123 | } | ||
| 124 | |||
| 125 | mod baseline { | ||
| 126 | use bstr::{BString, ByteSlice}; | ||
| 127 | use std::path::{Path, PathBuf}; | ||
| 128 | |||
| 129 | pub fn run( | ||
| 130 | name: &str, | ||
| 131 | items_are_dirs: bool, | ||
| 132 | init: impl FnOnce() -> crate::Result<(PathBuf, Vec<String>, Vec<Expected>)>, | ||
| 133 | ) -> crate::Result { | ||
| 134 | let (root, items, expected) = init()?; | ||
| 135 | let mut collection = Default::default(); | ||
| 136 | let attrs = | ||
| 137 | gix_attributes::Search::new_globals(Some(root.join(".gitattributes")), &mut Vec::new(), &mut collection)?; | ||
| 138 | let tests = expected.len(); | ||
| 139 | for expected in expected { | ||
| 140 | let mut search = gix_pathspec::Search::from_specs(expected.pathspecs, None, Path::new(""))?; | ||
| 141 | let actual: Vec<_> = items | ||
| 142 | .iter() | ||
| 143 | .filter(|path| { | ||
| 144 | search | ||
| 145 | .pattern_matching_relative_path( | ||
| 146 | path.as_str(), | ||
| 147 | Some(items_are_dirs), | ||
| 148 | |rela_path, case, is_dir, out| { | ||
| 149 | out.initialize(&collection); | ||
| 150 | attrs.pattern_matching_relative_path(rela_path, case, Some(is_dir), out) | ||
| 151 | }, | ||
| 152 | ) | ||
| 153 | .map_or(false, |m| !m.is_excluded()) | ||
| 154 | }) | ||
| 155 | .cloned() | ||
| 156 | .collect(); | ||
| 157 | let matches_expectation = actual == expected.matches; | ||
| 158 | assert_eq!( | ||
| 159 | matches_expectation, | ||
| 160 | expected.is_consistent, | ||
| 161 | "{} - {actual:?} == {:?}", | ||
| 162 | search.patterns().map(|p| format!("{p}")).collect::<Vec<_>>().join(", "), | ||
| 163 | expected.matches | ||
| 164 | ); | ||
| 165 | } | ||
| 166 | eprintln!("{tests} {name} matches OK"); | ||
| 167 | Ok(()) | ||
| 168 | } | ||
| 169 | |||
| 170 | #[derive(Debug)] | ||
| 171 | pub struct Expected { | ||
| 172 | pub pathspecs: Vec<gix_pathspec::Pattern>, | ||
| 173 | pub matches: Vec<String>, | ||
| 174 | /// If true, this means that the baseline is different from what we get, and that our solution is consistent with the rules. | ||
| 175 | pub is_consistent: bool, | ||
| 176 | } | ||
| 177 | |||
| 178 | pub fn parse_paths(path: PathBuf) -> std::io::Result<Vec<String>> { | ||
| 179 | let buf = std::fs::read(path)?; | ||
| 180 | Ok(buf.lines().map(BString::from).map(|s| s.to_string()).collect()) | ||
| 181 | } | ||
| 182 | |||
| 183 | fn parse_blocks(input: &[u8], parse_matches: impl Fn(&[u8]) -> Vec<String>) -> Vec<Expected> { | ||
| 184 | input | ||
| 185 | .split(|b| *b == b';') | ||
| 186 | .filter(|b| !b.is_empty()) | ||
| 187 | .map(move |block| { | ||
| 188 | let mut lines = block.lines(); | ||
| 189 | let mut is_inconsistent = false; | ||
| 190 | let pathspecs = lines | ||
| 191 | .next() | ||
| 192 | .expect("pathspec") | ||
| 193 | .split(|b| *b == b'+') | ||
| 194 | .filter(|spec| { | ||
| 195 | is_inconsistent = spec.as_bstr() == "git-inconsistency"; | ||
| 196 | !is_inconsistent | ||
| 197 | }) | ||
| 198 | .filter_map(|s| (!s.trim().is_empty()).then(|| s.trim())) | ||
| 199 | .map(|pathspec| gix_pathspec::parse(pathspec, Default::default()).expect("valid pathspec")) | ||
| 200 | .collect(); | ||
| 201 | Expected { | ||
| 202 | pathspecs, | ||
| 203 | matches: parse_matches(lines.as_bytes()), | ||
| 204 | is_consistent: !is_inconsistent, | ||
| 205 | } | ||
| 206 | }) | ||
| 207 | .collect() | ||
| 208 | } | ||
| 209 | |||
| 210 | mod submodule { | ||
| 211 | use bstr::ByteSlice; | ||
| 212 | |||
| 213 | pub fn matches_from_status(input: &[u8]) -> impl Iterator<Item = (bool, String)> + '_ { | ||
| 214 | input.lines().map(|line| { | ||
| 215 | let matches = line[0] == b' '; | ||
| 216 | assert_eq!(!matches, line[0] == b'-'); | ||
| 217 | let mut tokens = line[1..].split(|b| *b == b' ').skip(1); | ||
| 218 | let path = tokens.next().expect("path").to_str().expect("valid UTF-8"); | ||
| 219 | (matches, path.to_owned()) | ||
| 220 | }) | ||
| 221 | } | ||
| 222 | |||
| 223 | pub fn parse_expected(input: &[u8]) -> Vec<super::Expected> { | ||
| 224 | super::parse_blocks(input, |block| { | ||
| 225 | matches_from_status(block) | ||
| 226 | .filter_map(|(matches, module_path)| matches.then_some(module_path)) | ||
| 227 | .collect() | ||
| 228 | }) | ||
| 229 | } | ||
| 230 | } | ||
| 231 | |||
| 232 | mod files { | ||
| 233 | use bstr::{BString, ByteSlice}; | ||
| 234 | pub fn parse_expected(input: &[u8]) -> Vec<super::Expected> { | ||
| 235 | super::parse_blocks(input, |block| { | ||
| 236 | block.lines().map(BString::from).map(|s| s.to_string()).collect() | ||
| 237 | }) | ||
| 238 | } | ||
| 239 | } | ||
| 240 | |||
| 241 | pub fn directories() -> crate::Result<(PathBuf, Vec<String>, Vec<Expected>)> { | ||
| 242 | let root = gix_testtools::scripted_fixture_read_only("match_baseline_dirs.sh")?.join("parent"); | ||
| 243 | let buf = std::fs::read(root.join("paths"))?; | ||
| 244 | let items = submodule::matches_from_status(&buf) | ||
| 245 | .map(|(_matches, path)| path) | ||
| 246 | .collect(); | ||
| 247 | let expected = submodule::parse_expected(&std::fs::read(root.join("baseline.git"))?); | ||
| 248 | Ok((root, items, expected)) | ||
| 249 | } | ||
| 250 | |||
| 251 | pub fn files() -> crate::Result<(PathBuf, Vec<String>, Vec<Expected>)> { | ||
| 252 | let root = gix_testtools::scripted_fixture_read_only("match_baseline_files.sh")?; | ||
| 253 | let items = parse_paths(root.join("paths"))?; | ||
| 254 | let expected = files::parse_expected(&std::fs::read(root.join("baseline.git"))?); | ||
| 255 | Ok((root, items, expected)) | ||
| 256 | } | ||
| 257 | } | ||
| @@ -164,6 +164,7 @@ gix-index = { version = "^0.22.0", path = "../gix-index" } · 163 unmodified lines | |||
| 164 | gix-worktree = { version = "^0.24.0", path = "../gix-worktree" } | 164 | gix-worktree = { version = "^0.24.0", path = "../gix-worktree" } |
| 165 | gix-hashtable = { version = "^0.2.4", path = "../gix-hashtable" } | 165 | gix-hashtable = { version = "^0.2.4", path = "../gix-hashtable" } |
| 166 | gix-commitgraph = { version = "^0.18.2", path = "../gix-commitgraph" } | 166 | gix-commitgraph = { version = "^0.18.2", path = "../gix-commitgraph" } |
| 167 | gix-pathspec = { version = "^0.1.0", path = "../gix-pathspec" } | ||
| 167 | 168 | ||
| 168 | gix-worktree-stream = { version = "^0.3.0", path = "../gix-worktree-stream", optional = true } | 169 | gix-worktree-stream = { version = "^0.3.0", path = "../gix-worktree-stream", optional = true } |
| 169 | gix-archive = { version = "^0.3.0", path = "../gix-archive", default-features = false, optional = true } | 170 | gix-archive = { version = "^0.3.0", path = "../gix-archive", default-features = false, optional = true } |
| @@ -99,6 +99,7 @@ pub use gix_negotiate as negotiate; · 98 unmodified lines | |||
| 99 | pub use gix_object as objs; | 99 | pub use gix_object as objs; |
| 100 | pub use gix_object::bstr; | 100 | pub use gix_object::bstr; |
| 101 | pub use gix_odb as odb; | 101 | pub use gix_odb as odb; |
| 102 | pub use gix_pathspec as pathspec; | ||
| 102 | pub use gix_prompt as prompt; | 103 | pub use gix_prompt as prompt; |
| 103 | #[cfg(feature = "gix-protocol")] | 104 | #[cfg(feature = "gix-protocol")] |
| 104 | pub use gix_protocol as protocol; | 105 | pub use gix_protocol as protocol; |
| @@ -45,27 +45,30 @@ impl crate::Repository { · 44 unmodified lines | |||
| 45 | /// Note that there may be `None` if there is no work tree, even though the `PathBuf` will be empty | 45 | /// Note that there may be `None` if there is no work tree, even though the `PathBuf` will be empty |
| 46 | /// if the CWD is at the root of the work tree. | 46 | /// if the CWD is at the root of the work tree. |
| 47 | // TODO: tests, details - there is a lot about environment variables to change things around. | 47 | // TODO: tests, details - there is a lot about environment variables to change things around. |
| 48 | pub fn prefix(&self) -> Option<std::io::Result<PathBuf>> { | 48 | pub fn prefix(&self) -> std::io::Result<Option<PathBuf>> { |
| 49 | self.work_tree.as_ref().map(|root| { | 49 | self.work_tree |
| 50 | std::env::current_dir().and_then(|cwd| { | 50 | .as_ref() |
| 51 | gix_path::realpath_opts(root, &cwd, MAX_SYMLINKS) | 51 | .map(|root| { |
| 52 | .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) | 52 | std::env::current_dir().and_then(|cwd| { |
| 53 | .and_then(|root| { | 53 | gix_path::realpath_opts(root, &cwd, MAX_SYMLINKS) |
| 54 | cwd.strip_prefix(&root) | 54 | .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) |
| 55 | .map_err(|_| { | 55 | .and_then(|root| { |
| 56 | std::io::Error::new( | 56 | cwd.strip_prefix(&root) |
| 57 | std::io::ErrorKind::Other, | 57 | .map_err(|_| { |
| 58 | format!( | 58 | std::io::Error::new( |
| 59 | "CWD '{}' isn't within the work tree '{}'", | 59 | std::io::ErrorKind::Other, |
| 60 | cwd.display(), | 60 | format!( |
| 61 | root.display() | 61 | "CWD '{}' isn't within the work tree '{}'", |
| 62 | ), | 62 | cwd.display(), |
| 63 | ) | 63 | root.display() |
| 64 | }) | 64 | ), |
| 65 | .map(ToOwned::to_owned) | 65 | ) |
| 66 | }) | 66 | }) |
| 67 | .map(ToOwned::to_owned) | ||
| 68 | }) | ||
| 69 | }) | ||
| 67 | }) | 70 | }) |
| 68 | }) | 71 | .transpose() |
| 69 | } | 72 | } |
| 70 | 73 | ||
| 71 | /// Return the kind of repository, either bare or one with a work tree. | 74 | /// Return the kind of repository, either bare or one with a work tree. |
| @@ -1018,7 +1018,7 @@ pub fn main() -> Result<()> { · 1017 unmodified lines | |||
| 1018 | ), | 1018 | ), |
| 1019 | }, | 1019 | }, |
| 1020 | Subcommands::Attributes(cmd) => match cmd { | 1020 | Subcommands::Attributes(cmd) => match cmd { |
| 1021 | attributes::Subcommands::Query { statistics, pathspecs } => prepare_and_run( | 1021 | attributes::Subcommands::Query { statistics, pathspec } => prepare_and_run( |
| 1022 | "attributes-query", | 1022 | "attributes-query", |
| 1023 | trace, | 1023 | trace, |
| 1024 | verbose, | 1024 | verbose, |
| @@ -1029,15 +1029,17 @@ pub fn main() -> Result<()> { · 4 unmodified lines | |||
| 1029 | use gix::bstr::ByteSlice; | 1029 | use gix::bstr::ByteSlice; |
| 1030 | core::repository::attributes::query( | 1030 | core::repository::attributes::query( |
| 1031 | repository(Mode::Strict)?, | 1031 | repository(Mode::Strict)?, |
| 1032 | if pathspecs.is_empty() { | 1032 | if pathspec.is_empty() { |
| 1033 | Box::new( | 1033 | Box::new(stdin_or_bail()?.byte_lines().filter_map(Result::ok).filter_map(|line| { |
| 1034 | stdin_or_bail()? | 1034 | gix::pathspec::parse( |
| 1035 | .byte_lines() | 1035 | line.as_bstr(), |
| 1036 | .filter_map(Result::ok) | 1036 | // TODO(pathspec): use `repo` actual global defaults when available (see following as well) |
| 1037 | .filter_map(|line| gix::path::Spec::from_bytes(line.as_bstr())), | 1037 | Default::default(), |
| 1038 | ) as Box<dyn Iterator<Item = gix::path::Spec>> | 1038 | ) |
| 1039 | .ok() | ||
| 1040 | })) as Box<dyn Iterator<Item = gix::pathspec::Pattern>> | ||
| 1039 | } else { | 1041 | } else { |
| 1040 | Box::new(pathspecs.into_iter()) | 1042 | Box::new(pathspec.into_iter()) |
| 1041 | }, | 1043 | }, |
| 1042 | out, | 1044 | out, |
| 1043 | err, | 1045 | err, |
| @@ -1053,15 +1055,11 @@ pub fn main() -> Result<()> { · 9 unmodified lines | |||
| 1053 | progress_keep_open, | 1055 | progress_keep_open, |
| 1054 | None, | 1056 | None, |
| 1055 | move |progress, out, err| { | 1057 | move |progress, out, err| { |
| 1056 | use gix::bstr::ByteSlice; | ||
| 1057 | core::repository::attributes::validate_baseline( | 1058 | core::repository::attributes::validate_baseline( |
| 1058 | repository(Mode::StrictWithGitInstallConfig)?, | 1059 | repository(Mode::StrictWithGitInstallConfig)?, |
| 1059 | stdin_or_bail().ok().map(|stdin| { | 1060 | stdin_or_bail() |
| 1060 | stdin | 1061 | .ok() |
| 1061 | .byte_lines() | 1062 | .map(|stdin| stdin.byte_lines().filter_map(Result::ok).map(gix::bstr::BString::from)), |
| 1062 | .filter_map(Result::ok) | ||
| 1063 | .filter_map(|line| gix::path::Spec::from_bytes(line.as_bstr())) | ||
| 1064 | }), | ||
| 1065 | progress, | 1063 | progress, |
| 1066 | out, | 1064 | out, |
| 1067 | err, | 1065 | err, |
| @@ -1078,7 +1076,7 @@ pub fn main() -> Result<()> { · 10 unmodified lines | |||
| 1078 | exclude::Subcommands::Query { | 1076 | exclude::Subcommands::Query { |
| 1079 | statistics, | 1077 | statistics, |
| 1080 | patterns, | 1078 | patterns, |
| 1081 | pathspecs, | 1079 | pathspec, |
| 1082 | show_ignore_patterns, | 1080 | show_ignore_patterns, |
| 1083 | } => prepare_and_run( | 1081 | } => prepare_and_run( |
| 1084 | "exclude-query", | 1082 | "exclude-query", |
| @@ -1088,18 +1086,17 @@ pub fn main() -> Result<()> { · 3 unmodified lines | |||
| 1088 | progress_keep_open, | 1086 | progress_keep_open, |
| 1089 | None, | 1087 | None, |
| 1090 | move |_progress, out, err| { | 1088 | move |_progress, out, err| { |
| 1091 | use gix::bstr::ByteSlice; | ||
| 1092 | core::repository::exclude::query( | 1089 | core::repository::exclude::query( |
| 1093 | repository(Mode::Strict)?, | 1090 | repository(Mode::Strict)?, |
| 1094 | if pathspecs.is_empty() { | 1091 | if pathspec.is_empty() { |
| 1095 | Box::new( | 1092 | Box::new( |
| 1096 | stdin_or_bail()? | 1093 | stdin_or_bail()? |
| 1097 | .byte_lines() | 1094 | .byte_lines() |
| 1098 | .filter_map(Result::ok) | 1095 | .filter_map(Result::ok) |
| 1099 | .filter_map(|line| gix::path::Spec::from_bytes(line.as_bstr())), | 1096 | .filter_map(|line| gix::pathspec::parse(&line, Default::default()).ok()), |
| 1100 | ) as Box<dyn Iterator<Item = gix::path::Spec>> | 1097 | ) as Box<dyn Iterator<Item = gix::pathspec::Pattern>> |
| 1101 | } else { | 1098 | } else { |
| 1102 | Box::new(pathspecs.into_iter()) | 1099 | Box::new(pathspec.into_iter()) |
| 1103 | }, | 1100 | }, |
| 1104 | out, | 1101 | out, |
| 1105 | err, | 1102 | err, |
| @@ -1115,9 +1112,11 @@ pub fn main() -> Result<()> { · 9 unmodified lines | |||
| 1115 | }, | 1112 | }, |
| 1116 | Subcommands::Index(cmd) => match cmd { | 1113 | Subcommands::Index(cmd) => match cmd { |
| 1117 | index::Subcommands::Entries { | 1114 | index::Subcommands::Entries { |
| 1115 | format: entry_format, | ||
| 1118 | no_attributes, | 1116 | no_attributes, |
| 1119 | attributes_from_index, | 1117 | attributes_from_index, |
| 1120 | statistics, | 1118 | statistics, |
| 1119 | pathspec, | ||
| 1121 | } => prepare_and_run( | 1120 | } => prepare_and_run( |
| 1122 | "index-entries", | 1121 | "index-entries", |
| 1123 | trace, | 1122 | trace, |
| @@ -1128,10 +1127,15 @@ pub fn main() -> Result<()> { · 4 unmodified lines | |||
| 1128 | move |_progress, out, err| { | 1127 | move |_progress, out, err| { |
| 1129 | core::repository::index::entries( | 1128 | core::repository::index::entries( |
| 1130 | repository(Mode::LenientWithGitInstallConfig)?, | 1129 | repository(Mode::LenientWithGitInstallConfig)?, |
| 1130 | pathspec, | ||
| 1131 | out, | 1131 | out, |
| 1132 | err, | 1132 | err, |
| 1133 | core::repository::index::entries::Options { | 1133 | core::repository::index::entries::Options { |
| 1134 | format, | 1134 | format, |
| 1135 | simple: match entry_format { | ||
| 1136 | index::entries::Format::Simple => true, | ||
| 1137 | index::entries::Format::Rich => false, | ||
| 1138 | }, | ||
| 1135 | attributes: if no_attributes { | 1139 | attributes: if no_attributes { |
| 1136 | None | 1140 | None |
| 1137 | } else { | 1141 | } else { |
| @@ -610,7 +610,7 @@ pub mod attributes { · 609 unmodified lines | |||
| 610 | statistics: bool, | 610 | statistics: bool, |
| 611 | /// The git path specifications to list attributes for, or unset to read from stdin one per line. | 611 | /// The git path specifications to list attributes for, or unset to read from stdin one per line. |
| 612 | #[clap(value_parser = AsPathSpec)] | 612 | #[clap(value_parser = AsPathSpec)] |
| 613 | pathspecs: Vec<gix::path::Spec>, | 613 | pathspec: Vec<gix::pathspec::Pattern>, |
| 614 | }, | 614 | }, |
| 615 | } | 615 | } |
| 616 | } | 616 | } |
| @@ -639,18 +639,33 @@ pub mod exclude { · 22 unmodified lines | |||
| 639 | patterns: Vec<OsString>, | 639 | patterns: Vec<OsString>, |
| 640 | /// The git path specifications to check for exclusion, or unset to read from stdin one per line. | 640 | /// The git path specifications to check for exclusion, or unset to read from stdin one per line. |
| 641 | #[clap(value_parser = AsPathSpec)] | 641 | #[clap(value_parser = AsPathSpec)] |
| 642 | pathspecs: Vec<gix::path::Spec>, | 642 | pathspec: Vec<gix::pathspec::Pattern>, |
| 643 | }, | 643 | }, |
| 644 | } | 644 | } |
| 645 | } | 645 | } |
| 646 | 646 | ||
| 647 | pub mod index { | 647 | pub mod index { |
| 648 | use crate::shared::AsPathSpec; | ||
| 648 | use std::path::PathBuf; | 649 | use std::path::PathBuf; |
| 649 | 650 | ||
| 651 | pub mod entries { | ||
| 652 | #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] | ||
| 653 | pub enum Format { | ||
| 654 | /// | ||
| 655 | #[default] | ||
| 656 | Simple, | ||
| 657 | /// Use the `.tar` file format, uncompressed. | ||
| 658 | Rich, | ||
| 659 | } | ||
| 660 | } | ||
| 661 | |||
| 650 | #[derive(Debug, clap::Subcommand)] | 662 | #[derive(Debug, clap::Subcommand)] |
| 651 | pub enum Subcommands { | 663 | pub enum Subcommands { |
| 652 | /// Print all entries to standard output | 664 | /// Print all entries to standard output |
| 653 | Entries { | 665 | Entries { |
| 666 | /// How to output index entries. | ||
| 667 | #[clap(long, short = 'f', default_value = "simple", value_enum)] | ||
| 668 | format: entries::Format, | ||
| 654 | /// Do not visualize excluded entries or attributes per path. | 669 | /// Do not visualize excluded entries or attributes per path. |
| 655 | #[clap(long)] | 670 | #[clap(long)] |
| 656 | no_attributes: bool, | 671 | no_attributes: bool, |
| @@ -662,6 +677,9 @@ pub mod index { · 5 unmodified lines | |||
| 662 | /// Print various statistics to stderr | 677 | /// Print various statistics to stderr |
| 663 | #[clap(long, short = 's')] | 678 | #[clap(long, short = 's')] |
| 664 | statistics: bool, | 679 | statistics: bool, |
| 680 | /// The git path specifications to match entries to print. | ||
| 681 | #[clap(value_parser = AsPathSpec)] | ||
| 682 | pathspec: Vec<gix::pathspec::Pattern>, | ||
| 665 | }, | 683 | }, |
| 666 | /// Create an index from a tree-ish. | 684 | /// Create an index from a tree-ish. |
| 667 | #[clap(visible_alias = "read-tree")] | 685 | #[clap(visible_alias = "read-tree")] |
| @@ -125,7 +125,7 @@ pub mod tools { · 124 unmodified lines | |||
| 125 | TracePath { | 125 | TracePath { |
| 126 | /// The path to trace through history. | 126 | /// The path to trace through history. |
| 127 | #[clap(value_parser = AsPathSpec)] | 127 | #[clap(value_parser = AsPathSpec)] |
| 128 | path: gix::path::Spec, | 128 | path: gix::pathspec::Pattern, |
| 129 | }, | 129 | }, |
| 130 | } | 130 | } |
| 131 | } | 131 | } |
| @@ -349,11 +349,19 @@ mod clap { · 348 unmodified lines | |||
| 349 | pub struct AsPathSpec; | 349 | pub struct AsPathSpec; |
| 350 | 350 | ||
| 351 | impl TypedValueParser for AsPathSpec { | 351 | impl TypedValueParser for AsPathSpec { |
| 352 | type Value = gix::path::Spec; | 352 | type Value = gix::pathspec::Pattern; |
| 353 | 353 | ||
| 354 | fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> { | 354 | fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> { |
| 355 | OsStringValueParser::new() | 355 | OsStringValueParser::new() |
| 356 | .try_map(|arg| gix::path::Spec::try_from(arg.as_os_str())) | 356 | .try_map(|arg| { |
| 357 | let arg: &std::path::Path = arg.as_os_str().as_ref(); | ||
| 358 | gix::pathspec::parse( | ||
| 359 | gix::path::into_bstr(arg).as_ref(), | ||
| 360 | // TODO(pathspec): it *should* be possible to obtain these defaults from the environment and then act correctly. | ||
| 361 | // gix should | ||
| 362 | Default::default(), | ||
| 363 | ) | ||
| 364 | }) | ||
| 357 | .parse_ref(cmd, arg, value) | 365 | .parse_ref(cmd, arg, value) |
| 358 | } | 366 | } |
| 359 | } | 367 | } |