Unified · Split
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | **/generated-archives/*.tar.xz filter=lfs diff=lfs merge=lfs -text |
| 2 | 2 | |
| 3 | 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 | 5 | /justfile text crlf=input eol=lf |
| @@ -1186,6 +1186,7 @@ dependencies = [ · 1185 unmodified lines | ||
| 1186 | 1186 | "gix-odb", |
| 1187 | 1187 | "gix-pack", |
| 1188 | 1188 | "gix-path 0.8.4", |
| 1189 | + "gix-pathspec", | |
| 1189 | 1190 | "gix-prompt", |
| 1190 | 1191 | "gix-protocol", |
| 1191 | 1192 | "gix-ref 0.34.0", |
| @@ -1969,6 +1970,7 @@ dependencies = [ · 777 unmodified lines | ||
| 1969 | 1970 | "bstr", |
| 1970 | 1971 | "gix-attributes 0.16.0", |
| 1971 | 1972 | "gix-glob 0.10.2", |
| 1973 | + "gix-path 0.8.4", | |
| 1972 | 1974 | "gix-testtools", |
| 1973 | 1975 | "once_cell", |
| 1974 | 1976 | "thiserror", |
| @@ -361,8 +361,9 @@ Check out the [performance discussion][gix-traverse-performance] as well. · 360 unmodified lines | ||
| 361 | 361 | * [ ] check for match |
| 362 | 362 | |
| 363 | 363 | ### gix-pathspec |
| 364 | −* [x] parse | |
| 365 | −* [ ] matching of paths | |
| 364 | +* [x] parse single | |
| 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 | 368 | ### gix-refspec |
| 368 | 369 | * [x] parse |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | use std::collections::HashMap; |
| 2 | 2 | |
| 3 | −use anyhow::Context; | |
| 3 | +use anyhow::{bail, Context}; | |
| 4 | 4 | use gix::{bstr::ByteSlice, prelude::ObjectIdExt, Progress}; |
| 5 | 5 | use rusqlite::{params, OptionalExtension}; |
| 6 | 6 | |
| @@ -18,10 +18,16 @@ impl query::Engine { · 11 unmodified lines | ||
| 18 | 18 | ) -> anyhow::Result<()> { |
| 19 | 19 | match cmd { |
| 20 | 20 | Command::TracePath { mut spec } => { |
| 21 | − if let Some(prefix) = self.repo.prefix() { | |
| 22 | − spec.apply_prefix(&prefix?); | |
| 23 | − }; | |
| 24 | − let relpath = spec.items().next().expect("spec has at least one item"); | |
| 21 | + let is_excluded = spec.is_excluded(); | |
| 22 | + let relpath = spec | |
| 23 | + .normalize( | |
| 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 | 31 | let file_id: usize = self |
| 26 | 32 | .con |
| 27 | 33 | .query_row( |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | pub enum Command { |
| 2 | 2 | TracePath { |
| 3 | 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 | 19 | pub fn query( |
| 20 | 20 | repo: gix::Repository, |
| 21 | − pathspecs: impl Iterator<Item = gix::path::Spec>, | |
| 21 | + pathspecs: impl Iterator<Item = gix::pathspec::Pattern>, | |
| 22 | 22 | mut out: impl io::Write, |
| 23 | 23 | mut err: impl io::Write, |
| 24 | 24 | Options { format, statistics }: Options, |
| @@ -28,28 +28,31 @@ pub(crate) mod function { · 3 unmodified lines | ||
| 28 | 28 | } |
| 29 | 29 | |
| 30 | 30 | let mut cache = attributes_cache(&repo)?; |
| 31 | − let prefix = repo.prefix().expect("worktree - we have an index by now")?; | |
| 32 | 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 { | |
| 35 | − for path in spec.apply_prefix(&prefix).items() { | |
| 36 | − let is_dir = gix::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); | |
| 37 | − let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; | |
| 39 | + for spec in search.into_patterns() { | |
| 40 | + let is_dir = gix::path::from_bstr(spec.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))?; | |
| 38 | 42 | |
| 39 | − if !entry.matching_attributes(&mut matches) { | |
| 40 | − continue; | |
| 41 | − } | |
| 42 | − for m in matches.iter() { | |
| 43 | − writeln!( | |
| 44 | − out, | |
| 45 | − "{}:{}:{}\t{}\t{}", | |
| 46 | − m.location.source.map(Path::to_string_lossy).unwrap_or_default(), | |
| 47 | − m.location.sequence_number, | |
| 48 | − m.pattern, | |
| 49 | − path, | |
| 50 | − m.assignment | |
| 51 | − )?; | |
| 52 | − } | |
| 43 | + if !entry.matching_attributes(&mut matches) { | |
| 44 | + continue; | |
| 45 | + } | |
| 46 | + for m in matches.iter() { | |
| 47 | + writeln!( | |
| 48 | + out, | |
| 49 | + "{}:{}:{}\t{}\t{}", | |
| 50 | + m.location.source.map(Path::to_string_lossy).unwrap_or_default(), | |
| 51 | + m.location.sequence_number, | |
| 52 | + m.pattern, | |
| 53 | + spec.path(), | |
| 54 | + m.assignment | |
| 55 | + )?; | |
| 53 | 56 | } |
| 54 | 57 | } |
| 55 | 58 | |
| @@ -18,6 +18,7 @@ pub(crate) mod function { · 17 unmodified lines | ||
| 18 | 18 | }; |
| 19 | 19 | |
| 20 | 20 | use anyhow::{anyhow, bail}; |
| 21 | + use gix::bstr::BString; | |
| 21 | 22 | use gix::{attrs::Assignment, odb::FindExt, Progress}; |
| 22 | 23 | |
| 23 | 24 | use crate::{ |
| @@ -27,7 +28,7 @@ pub(crate) mod function { · 3 unmodified lines | ||
| 27 | 28 | |
| 28 | 29 | pub fn validate_baseline( |
| 29 | 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 | 32 | mut progress: impl Progress + 'static, |
| 32 | 33 | mut out: impl io::Write, |
| 33 | 34 | mut err: impl io::Write, |
| @@ -51,7 +52,7 @@ pub(crate) mod function { · 17 unmodified lines | ||
| 51 | 52 | ignore = false; |
| 52 | 53 | } |
| 53 | 54 | let mut num_entries = None; |
| 54 | − let pathspecs = pathspecs.map_or_else( | |
| 55 | + let paths = paths.map_or_else( | |
| 55 | 56 | { |
| 56 | 57 | let repo = repo.clone(); |
| 57 | 58 | let num_entries = &mut num_entries; |
| @@ -59,18 +60,16 @@ pub(crate) mod function { · 1 unmodified line | ||
| 59 | 60 | let index = repo.index_or_load_from_head()?.into_owned(); |
| 60 | 61 | let (entries, path_backing) = index.into_parts().0.into_entries(); |
| 61 | 62 | *num_entries = Some(entries.len()); |
| 62 | − let iter = Box::new(entries.into_iter().map(move |e| { | |
| 63 | − gix::path::Spec::from_bytes(e.path_in(&path_backing)).expect("each entry path is a valid spec") | |
| 64 | − })); | |
| 65 | − Ok(iter as Box<dyn Iterator<Item = gix::path::Spec> + Send + 'static>) | |
| 63 | + let iter = Box::new(entries.into_iter().map(move |e| e.path_in(&path_backing).to_owned())); | |
| 64 | + Ok(iter as Box<dyn Iterator<Item = BString> + 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 | 70 | let (tx_base, rx_base) = std::sync::mpsc::channel::<(String, Baseline)>(); |
| 72 | 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 | 73 | std::thread::spawn({ |
| 75 | 74 | let path = repo.path().to_owned(); |
| 76 | 75 | let tx_base = tx_base.clone(); |
| @@ -89,12 +88,10 @@ pub(crate) mod function { · 12 unmodified lines | ||
| 89 | 88 | move || -> anyhow::Result<()> { |
| 90 | 89 | progress.init(num_entries, gix::progress::count("paths")); |
| 91 | 90 | let start = std::time::Instant::now(); |
| 92 | − for spec in rx { | |
| 91 | + for path in rx { | |
| 93 | 92 | progress.inc(); |
| 94 | − for path in spec.items() { | |
| 95 | − stdin.write_all(path.as_ref())?; | |
| 96 | − stdin.write_all(b"\n")?; | |
| 97 | − } | |
| 93 | + stdin.write_all(&path)?; | |
| 94 | + stdin.write_all(b"\n")?; | |
| 98 | 95 | } |
| 99 | 96 | progress.show_throughput(start); |
| 100 | 97 | Ok(()) |
| @@ -123,7 +120,7 @@ pub(crate) mod function { · 22 unmodified lines | ||
| 123 | 120 | }) |
| 124 | 121 | .transpose()?; |
| 125 | 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 | 124 | std::thread::spawn({ |
| 128 | 125 | let path = work_dir.expect("present if we are here"); |
| 129 | 126 | let tx_base = tx_base.clone(); |
| @@ -142,12 +139,10 @@ pub(crate) mod function { · 12 unmodified lines | ||
| 142 | 139 | move || -> anyhow::Result<()> { |
| 143 | 140 | progress.init(num_entries, gix::progress::count("paths")); |
| 144 | 141 | let start = std::time::Instant::now(); |
| 145 | − for spec in rx { | |
| 142 | + for path in rx { | |
| 146 | 143 | progress.inc(); |
| 147 | − for path in spec.items() { | |
| 148 | − stdin.write_all(path.as_ref())?; | |
| 149 | − stdin.write_all(b"\n")?; | |
| 150 | − } | |
| 144 | + stdin.write_all(path.as_ref())?; | |
| 145 | + stdin.write_all(b"\n")?; | |
| 151 | 146 | } |
| 152 | 147 | progress.show_throughput(start); |
| 153 | 148 | Ok(()) |
| @@ -175,12 +170,12 @@ pub(crate) mod function { · 21 unmodified lines | ||
| 175 | 170 | drop(tx_base); |
| 176 | 171 | |
| 177 | 172 | std::thread::spawn(move || { |
| 178 | − for spec in pathspecs { | |
| 179 | − if feed_attrs.send(spec.clone()).is_err() { | |
| 173 | + for path in paths { | |
| 174 | + if feed_attrs.send(path.clone()).is_err() { | |
| 180 | 175 | break; |
| 181 | 176 | } |
| 182 | 177 | if let Some(ch) = feed_excludes.as_ref() { |
| 183 | − if ch.send(spec).is_err() { | |
| 178 | + if ch.send(path).is_err() { | |
| 184 | 179 | break; |
| 185 | 180 | } |
| 186 | 181 | } |
| @@ -20,7 +20,7 @@ pub mod query { · 19 unmodified lines | ||
| 20 | 20 | |
| 21 | 21 | pub fn query( |
| 22 | 22 | repo: gix::Repository, |
| 23 | − pathspecs: impl Iterator<Item = gix::path::Spec>, | |
| 23 | + pathspecs: impl Iterator<Item = gix::pathspec::Pattern>, | |
| 24 | 24 | mut out: impl io::Write, |
| 25 | 25 | mut err: impl io::Write, |
| 26 | 26 | query::Options { |
| @@ -41,28 +41,30 @@ pub fn query( · 14 unmodified lines | ||
| 41 | 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 { | |
| 47 | − for path in spec.apply_prefix(&prefix).items() { | |
| 48 | − // TODO: what about paths that end in /? Pathspec might handle it, it's definitely something git considers | |
| 49 | − // even if the directory doesn't exist. Seems to work as long as these are kept in the spec. | |
| 50 | − let is_dir = gix::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); | |
| 51 | − let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; | |
| 52 | − let match_ = entry | |
| 53 | − .matching_exclude_pattern() | |
| 54 | − .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m)); | |
| 55 | − match match_ { | |
| 56 | − Some(m) => writeln!( | |
| 57 | − out, | |
| 58 | − "{}:{}:{}\t{}", | |
| 59 | − m.source.map(std::path::Path::to_string_lossy).unwrap_or_default(), | |
| 60 | − m.sequence_number, | |
| 61 | − m.pattern, | |
| 62 | − path | |
| 63 | − )?, | |
| 64 | − None => writeln!(out, "::\t{path}")?, | |
| 65 | − } | |
| 51 | + for spec in search.into_patterns() { | |
| 52 | + let path = spec.path(); | |
| 53 | + let is_dir = gix::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); | |
| 54 | + let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; | |
| 55 | + let match_ = entry | |
| 56 | + .matching_exclude_pattern() | |
| 57 | + .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m)); | |
| 58 | + match match_ { | |
| 59 | + Some(m) => writeln!( | |
| 60 | + out, | |
| 61 | + "{}:{}:{}\t{}", | |
| 62 | + m.source.map(std::path::Path::to_string_lossy).unwrap_or_default(), | |
| 63 | + m.sequence_number, | |
| 64 | + m.pattern, | |
| 65 | + path | |
| 66 | + )?, | |
| 67 | + None => writeln!(out, "::\t{path}")?, | |
| 66 | 68 | } |
| 67 | 69 | } |
| 68 | 70 | |
| @@ -4,9 +4,10 @@ pub struct Options { · 3 unmodified lines | ||
| 4 | 4 | /// If true, also show attributes |
| 5 | 5 | pub attributes: Option<Attributes>, |
| 6 | 6 | pub statistics: bool, |
| 7 | + pub simple: bool, | |
| 7 | 8 | } |
| 8 | 9 | |
| 9 | −#[derive(Debug)] | |
| 10 | +#[derive(Debug, Copy, Clone)] | |
| 10 | 11 | pub enum Attributes { |
| 11 | 12 | /// Look at worktree attributes and index as fallback. |
| 12 | 13 | WorktreeAndIndex, |
| @@ -15,6 +16,7 @@ pub enum Attributes { · 2 unmodified lines | ||
| 15 | 16 | } |
| 16 | 17 | |
| 17 | 18 | pub(crate) mod function { |
| 19 | + use std::collections::BTreeSet; | |
| 18 | 20 | use std::{ |
| 19 | 21 | borrow::Cow, |
| 20 | 22 | io::{BufWriter, Write}, |
| @@ -26,9 +28,11 @@ pub(crate) mod function { · 5 unmodified lines | ||
| 26 | 28 | |
| 27 | 29 | pub fn entries( |
| 28 | 30 | repo: gix::Repository, |
| 31 | + pathspecs: Vec<gix::pathspec::Pattern>, | |
| 29 | 32 | out: impl std::io::Write, |
| 30 | 33 | mut err: impl std::io::Write, |
| 31 | 34 | Options { |
| 35 | + simple, | |
| 32 | 36 | format, |
| 33 | 37 | attributes, |
| 34 | 38 | statistics, |
| @@ -37,6 +41,12 @@ pub(crate) mod function { · 2 unmodified lines | ||
| 37 | 41 | use crate::OutputFormat::*; |
| 38 | 42 | let index = repo.index_or_load_from_head()?; |
| 39 | 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 | 50 | .map(|attrs| { |
| 41 | 51 | repo.attributes( |
| 42 | 52 | &index, |
| @@ -70,52 +80,111 @@ pub(crate) mod function { · 27 unmodified lines | ||
| 70 | 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 | 84 | #[cfg(feature = "serde")] |
| 75 | 85 | if let Json = format { |
| 76 | 86 | out.write_all(b"[\n")?; |
| 77 | 87 | } |
| 78 | − let mut entries = index.entries().iter().peekable(); | |
| 79 | − while let Some(entry) = entries.next() { | |
| 80 | − let attrs = cache | |
| 81 | − .as_mut() | |
| 82 | − .map(|(attrs, cache)| { | |
| 83 | − cache | |
| 84 | − .at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf)) | |
| 85 | − .map(|entry| { | |
| 86 | − let is_excluded = entry.is_excluded(); | |
| 87 | − stats.excluded += usize::from(is_excluded); | |
| 88 | − let attributes: Vec<_> = { | |
| 89 | − entry.matching_attributes(attrs); | |
| 90 | − attrs.iter().map(|m| m.assignment.to_owned()).collect() | |
| 91 | − }; | |
| 92 | − stats.with_attributes += usize::from(!attributes.is_empty()); | |
| 93 | − Attrs { | |
| 94 | − is_excluded, | |
| 95 | − attributes, | |
| 96 | − } | |
| 88 | + let mut search = gix::pathspec::Search::from_specs( | |
| 89 | + pathspecs, | |
| 90 | + repo.prefix()?.as_deref(), | |
| 91 | + gix::path::realpath(repo.work_dir().unwrap_or_else(|| repo.git_dir()))?.as_ref(), // TODO(pathspec): this setup needs `gix`. | |
| 92 | + )?; | |
| 93 | + let mut all_attrs = statistics.then(BTreeSet::new); | |
| 94 | + if let Some(entries) = index.prefixed_entries(search.common_prefix()) { | |
| 95 | + stats.entries_after_prune = entries.len(); | |
| 96 | + let mut entries = entries.iter().peekable(); | |
| 97 | + while let Some(entry) = entries.next() { | |
| 98 | + let mut last_match = None; | |
| 99 | + let attrs = cache | |
| 100 | + .as_mut() | |
| 101 | + .and_then(|(attrs, cache)| { | |
| 102 | + // If the user wants to see assigned attributes, we always have to match. | |
| 103 | + attributes.is_some().then(|| { | |
| 104 | + cache | |
| 105 | + .at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf)) | |
| 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 | − }) | |
| 99 | − .transpose()?; | |
| 100 | − match format { | |
| 101 | − Human => to_human(&mut out, &index, entry, attrs)?, | |
| 102 | − #[cfg(feature = "serde")] | |
| 103 | − Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?, | |
| 126 | + }) | |
| 127 | + .transpose()?; | |
| 128 | + | |
| 129 | + // Note that we intentionally ignore `_case` so that we act like git does, attribute matching case is determined | |
| 130 | + // by the repository, not the pathspec. | |
| 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")] | |
| 108 | − if format == Json { | |
| 109 | − out.write_all(b"]\n")?; | |
| 110 | − out.flush()?; | |
| 111 | − if statistics { | |
| 112 | − serde_json::to_writer_pretty(&mut err, &stats)?; | |
| 169 | + #[cfg(feature = "serde")] | |
| 170 | + if format == Json { | |
| 171 | + out.write_all(b"]\n")?; | |
| 172 | + out.flush()?; | |
| 173 | + if statistics { | |
| 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 | 189 | Ok(()) |
| 121 | 190 | } |
| @@ -131,8 +200,10 @@ pub(crate) mod function { · 9 unmodified lines | ||
| 131 | 200 | struct Statistics { |
| 132 | 201 | #[allow(dead_code)] // Not really dead, but Debug doesn't count for it even though it's crucial. |
| 133 | 202 | pub entries: usize, |
| 203 | + pub entries_after_prune: usize, | |
| 134 | 204 | pub excluded: usize, |
| 135 | 205 | pub with_attributes: usize, |
| 206 | + pub max_attributes_per_path: usize, | |
| 136 | 207 | pub cache: Option<gix::worktree::cache::Statistics>, |
| 137 | 208 | } |
| 138 | 209 | |
| @@ -175,6 +246,22 @@ pub(crate) mod function { · 36 unmodified lines | ||
| 175 | 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 | 265 | fn to_human( |
| 179 | 266 | out: &mut impl std::io::Write, |
| 180 | 267 | file: &gix::index::File, |
| @@ -198,24 +285,28 @@ pub(crate) mod function { · 17 unmodified lines | ||
| 198 | 285 | entry.mode, |
| 199 | 286 | entry.id, |
| 200 | 287 | entry.path(file), |
| 201 | − attrs.map_or(Cow::Borrowed(""), |a| { | |
| 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 | − }) | |
| 288 | + print_attrs(attrs) | |
| 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 | 182 | .collect() |
| 183 | 183 | } |
| 184 | +} | |
| 184 | 185 | |
| 186 | +impl Attributes { | |
| 185 | 187 | fn may_use_glob_pattern(pattern: &gix_glob::Pattern) -> bool { |
| 186 | 188 | pattern.mode != macro_mode() |
| 187 | 189 | } |
| @@ -229,7 +231,13 @@ fn pattern_matching_relative_path( · 41 unmodified lines | ||
| 229 | 231 | Value::Assignments(attrs) => attrs, |
| 230 | 232 | }; |
| 231 | 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 | 242 | let all_filled = out.fill_attributes(attrs.iter(), pattern, list.source.as_ref(), *sequence_number); |
| 235 | 243 | if all_filled { |
| @@ -73,6 +73,26 @@ impl Outcome { · 72 unmodified lines | ||
| 73 | 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 | 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 | 269 | /// Mutation |
| 227 | 270 | impl MetadataCollection { |
| 228 | 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 | 21 | where |
| 22 | 22 | T: Hash + Clone, |
| 23 | 23 | { |
| 24 | + pub(crate) fn len(&self) -> usize { | |
| 25 | + self.0.len() | |
| 26 | + } | |
| 24 | 27 | pub(crate) fn insert(&mut self, value: &T) -> RefMapKey { |
| 25 | 28 | let mut s = DefaultHasher::new(); |
| 26 | 29 | value.hash(&mut s); |
| @@ -141,8 +141,10 @@ fn all_attributes_are_listed_in_declaration_order() -> crate::Result { · 140 unmodified lines | ||
| 141 | 141 | true, /* use macros */ |
| 142 | 142 | )?; |
| 143 | 143 | |
| 144 | − let mut out = gix_attributes::search::Outcome::default(); | |
| 144 | + let mut out = Outcome::default(); | |
| 145 | 145 | out.initialize(&collection); |
| 146 | + let mut alt = Outcome::default(); | |
| 147 | + alt.initialize(&collection); | |
| 146 | 148 | |
| 147 | 149 | let mut orders = collection |
| 148 | 150 | .iter() |
| @@ -206,10 +208,13 @@ fn all_attributes_are_listed_in_declaration_order() -> crate::Result { · 57 unmodified lines | ||
| 206 | 208 | assert_references(&out); |
| 207 | 209 | let actual: Vec<_> = out.iter().map(|m| m.assignment).collect(); |
| 208 | 210 | assert_eq!( |
| 209 | − by_name(actual), | |
| 211 | + by_name(actual.clone()), | |
| 210 | 212 | by_name(expected), |
| 211 | 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 | 219 | assert_eq!( |
| 215 | 220 | out.iter().count(), |
| @@ -234,8 +239,10 @@ fn given_attributes_are_made_available_in_given_order() -> crate::Result { · 18 unmodified lines | ||
| 234 | 239 | true, /* use macros */ |
| 235 | 240 | )?; |
| 236 | 241 | |
| 237 | − let mut out = gix_attributes::search::Outcome::default(); | |
| 242 | + let mut out = Outcome::default(); | |
| 238 | 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 | 247 | for (rela_path, expected) in (baseline::Expectations { lines: input.lines() }) { |
| 241 | 248 | out.reset(); |
| @@ -246,6 +253,10 @@ fn given_attributes_are_made_available_in_given_order() -> crate::Result { · 4 unmodified lines | ||
| 246 | 253 | actual, expected, |
| 247 | 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 | 261 | assert_eq!( |
| 251 | 262 | out.iter().count(), |
| @@ -255,6 +266,15 @@ fn given_attributes_are_made_available_in_given_order() -> crate::Result { · 3 unmodified lines | ||
| 255 | 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 | 278 | fn by_name(assignments: Vec<AssignmentRef>) -> BTreeMap<NameRef, StateRef> { |
| 259 | 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 | 6 | /// A sloppy parser that performs only the most basic checks, providing additional information |
| 7 | 7 | /// using `pattern::Mode` flags. |
| 8 | 8 | /// |
| 9 | +/// If `may_alter` is `false`, we won't parse leading `!` or its escaped form. | |
| 10 | +/// | |
| 9 | 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 | 13 | let mut mode = Mode::empty(); |
| 12 | 14 | if pat.is_empty() { |
| 13 | 15 | return None; |
| 14 | 16 | }; |
| 15 | − if pat.first() == Some(&b'!') { | |
| 16 | − mode |= Mode::NEGATIVE; | |
| 17 | − pat = &pat[1..]; | |
| 18 | − } else if pat.first() == Some(&b'\\') { | |
| 19 | − let second = pat.get(1); | |
| 20 | − if second == Some(&b'!') || second == Some(&b'#') { | |
| 17 | + if may_alter { | |
| 18 | + if pat.first() == Some(&b'!') { | |
| 19 | + mode |= Mode::NEGATIVE; | |
| 21 | 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 | 28 | if pat.iter().all(u8::is_ascii_whitespace) { |
| @@ -44,7 +44,18 @@ pub enum Case { · 43 unmodified lines | ||
| 44 | 44 | impl Pattern { |
| 45 | 45 | /// Parse the given `text` as pattern, or return `None` if `text` was empty. |
| 46 | 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 | 59 | text: text.into(), |
| 49 | 60 | mode, |
| 50 | 61 | first_wildcard_pos, |
| @@ -65,30 +76,37 @@ impl Pattern { · 14 unmodified lines | ||
| 65 | 76 | /// We may take various shortcuts which is when `basename_start_pos` and `is_dir` come into play. |
| 66 | 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 | 81 | pub fn matches_repo_relative_path<'a>( |
| 70 | 82 | &self, |
| 71 | 83 | path: impl Into<&'a BStr>, |
| 72 | 84 | basename_start_pos: Option<usize>, |
| 73 | 85 | is_dir: Option<bool>, |
| 74 | 86 | case: Case, |
| 87 | + mode: wildmatch::Mode, | |
| 75 | 88 | ) -> bool { |
| 76 | 89 | let is_dir = is_dir.unwrap_or(false); |
| 77 | 90 | if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) { |
| 78 | 91 | return false; |
| 79 | 92 | } |
| 80 | 93 | |
| 81 | − let flags = wildmatch::Mode::NO_MATCH_SLASH_LITERAL | |
| 94 | + let flags = mode | |
| 82 | 95 | | match case { |
| 83 | 96 | Case::Fold => wildmatch::Mode::IGNORE_CASE, |
| 84 | 97 | Case::Sensitive => wildmatch::Mode::empty(), |
| 85 | 98 | }; |
| 86 | 99 | let path = path.into(); |
| 87 | − debug_assert_eq!( | |
| 88 | − basename_start_pos, | |
| 89 | − path.rfind_byte(b'/').map(|p| p + 1), | |
| 90 | − "BUG: invalid cached basename_start_pos provided" | |
| 91 | − ); | |
| 100 | + #[cfg(debug_assertions)] | |
| 101 | + { | |
| 102 | + if basename_start_pos.is_some() { | |
| 103 | + debug_assert_eq!( | |
| 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 | 110 | debug_assert!(!path.starts_with(b"/"), "input path must be relative"); |
| 93 | 111 | |
| 94 | 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 | 17 | /// Parse all patterns in `bytes` line by line, ignoring lines with errors, and collect them. |
| 18 | 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 | 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 | 66 | gix_glob::parse(b"!hello"), |
| 67 | 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 | 76 | #[test] |
| 72 | 77 | fn leading_exclamation_marks_can_be_escaped_with_backslash() { |
| 73 | 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 | 86 | #[test] |
| @@ -68,7 +68,13 @@ fn compare_baseline_with_ours() { · 67 unmodified lines | ||
| 68 | 68 | ); |
| 69 | 69 | match std::panic::catch_unwind(|| { |
| 70 | 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 | 79 | Ok(actual_match) => { |
| 74 | 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 | 112 | let path = "hello"; |
| 107 | 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 | 121 | "non-dirs never match a dir pattern" |
| 110 | 122 | ); |
| 111 | 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 | 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 | 336 | fn match_path<'a>(pattern: &gix_glob::Pattern, path: impl Into<&'a BStr>, is_dir: Option<bool>, case: Case) -> bool { |
| 319 | 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 | 347 | fn basename_start_pos(value: &BStr) -> Option<usize> { |
| @@ -18,10 +18,6 @@ mod list { · 17 unmodified lines | ||
| 18 | 18 | fn bytes_to_patterns(_bytes: &[u8], _source: &Path) -> Vec<Mapping<Self::Value>> { |
| 19 | 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 | 23 | #[test] |
| @@ -367,7 +367,13 @@ impl Display for MatchResult { · 366 unmodified lines | ||
| 367 | 367 | } |
| 368 | 368 | |
| 369 | 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 | 378 | fn basename_of(path: &str) -> Option<usize> { |
| 373 | 379 | path.rfind('/').map(|pos| pos + 1) |
| @@ -35,10 +35,6 @@ impl Pattern for Ignore { · 34 unmodified lines | ||
| 35 | 35 | }) |
| 36 | 36 | .collect() |
| 37 | 37 | } |
| 38 | − | |
| 39 | − fn may_use_glob_pattern(_pattern: &gix_glob::Pattern) -> bool { | |
| 40 | − true | |
| 41 | − } | |
| 42 | 38 | } |
| 43 | 39 | |
| 44 | 40 | /// Instantiation of a search for ignore patterns. |
| @@ -115,25 +111,27 @@ pub fn pattern_matching_relative_path<'a>( · 70 unmodified lines | ||
| 115 | 111 | ) -> Option<Match<'a>> { |
| 116 | 112 | let (relative_path, basename_start_pos) = |
| 117 | 113 | list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case)?; |
| 118 | − list.patterns | |
| 119 | − .iter() | |
| 120 | − .rev() | |
| 121 | − .filter(|pm| Ignore::may_use_glob_pattern(&pm.pattern)) | |
| 122 | − .find_map( | |
| 123 | − |pattern::Mapping { | |
| 124 | − pattern, | |
| 125 | − value: (), | |
| 126 | − sequence_number, | |
| 127 | − }| { | |
| 128 | − pattern | |
| 129 | − .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) | |
| 130 | − .then_some(Match { | |
| 131 | − pattern, | |
| 132 | − source: list.source.as_deref(), | |
| 133 | − sequence_number: *sequence_number, | |
| 134 | − }) | |
| 135 | − }, | |
| 136 | − ) | |
| 114 | + list.patterns.iter().rev().find_map( | |
| 115 | + |pattern::Mapping { | |
| 116 | + pattern, | |
| 117 | + value: (), | |
| 118 | + sequence_number, | |
| 119 | + }| { | |
| 120 | + pattern | |
| 121 | + .matches_repo_relative_path( | |
| 122 | + relative_path, | |
| 123 | + basename_start_pos, | |
| 124 | + is_dir, | |
| 125 | + case, | |
| 126 | + gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, | |
| 127 | + ) | |
| 128 | + .then_some(Match { | |
| 129 | + pattern, | |
| 130 | + source: list.source.as_deref(), | |
| 131 | + sequence_number: *sequence_number, | |
| 132 | + }) | |
| 133 | + }, | |
| 134 | + ) | |
| 137 | 135 | } |
| 138 | 136 | |
| 139 | 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 | 145 | ) -> Option<usize> { |
| 148 | 146 | let (relative_path, basename_start_pos) = |
| 149 | 147 | list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case)?; |
| 150 | − list.patterns | |
| 151 | − .iter() | |
| 152 | − .enumerate() | |
| 153 | − .rev() | |
| 154 | − .filter(|(_, pm)| Ignore::may_use_glob_pattern(&pm.pattern)) | |
| 155 | − .find_map(|(idx, pm)| { | |
| 156 | − pm.pattern | |
| 157 | − .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) | |
| 158 | − .then_some(idx) | |
| 159 | − }) | |
| 148 | + list.patterns.iter().enumerate().rev().find_map(|(idx, pm)| { | |
| 149 | + pm.pattern | |
| 150 | + .matches_repo_relative_path( | |
| 151 | + relative_path, | |
| 152 | + basename_start_pos, | |
| 153 | + is_dir, | |
| 154 | + case, | |
| 155 | + gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, | |
| 156 | + ) | |
| 157 | + .then_some(idx) | |
| 158 | + }) | |
| 160 | 159 | } |
| 161 | 160 | |
| 162 | 161 | /// Matching of ignore patterns. |
| @@ -158,6 +158,34 @@ impl State { · 157 unmodified lines | ||
| 158 | 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 | 189 | /// Return the entry at `idx` or _panic_ if the index is out of bounds. |
| 162 | 190 | /// |
| 163 | 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 | 28 | file.entry_by_path("file".into()).expect("found").stage(), |
| 29 | 29 | 2, |
| 30 | 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 | 44 | #[test] |
| @@ -65,4 +75,31 @@ fn sort_entries() { · 30 unmodified lines | ||
| 65 | 75 | new_entry_path, |
| 66 | 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 | 239 | /// Resolve relative components virtually without accessing the file system, e.g. turn `a/./b/c/.././..` into `a`, |
| 240 | 240 | /// without keeping intermediate `..` and `/a/../b/..` becomes `/`. |
| 241 | 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 | 244 | /// This is particularly useful when manipulating paths that are based on user input, and not resolving intermediate |
| 244 | 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 | 50 | #![forbid(unsafe_code)] |
| 51 | 51 | |
| 52 | 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 | 53 | mod convert; |
| 59 | 54 | pub use convert::*; |
| 60 | 55 | |
| 61 | 56 | mod util; |
| 62 | 57 | pub use util::is_absolute; |
| 63 | 58 | |
| 64 | −mod spec; | |
| 65 | − | |
| 66 | 59 | /// |
| 67 | 60 | pub mod realpath; |
| 68 | 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 | 117 | ("./a/./b/..", "./a/."), |
| 118 | 118 | ("/a/./b/c/.././../.", "/a"), |
| 119 | 119 | ("/a/./b", "/a/./b"), |
| 120 | + ("././/a/./b", "././/a/./b"), | |
| 120 | 121 | ("/a/././c/.././../.", "/"), |
| 121 | 122 | ("/a/b/../c/../..", "/"), |
| 122 | 123 | ("C:/hello/../a", "C:/a"), |
| @@ -13,6 +13,7 @@ doctest = false · 12 unmodified lines | ||
| 13 | 13 | |
| 14 | 14 | [dependencies] |
| 15 | 15 | gix-glob = { version = "^0.10.2", path = "../gix-glob" } |
| 16 | +gix-path = { version = "^0.8.4", path = "../gix-path" } | |
| 16 | 17 | gix-attributes = { version = "^0.16.0", path = "../gix-attributes" } |
| 17 | 18 | |
| 18 | 19 | bstr = { version = "1.3.0", default-features = false, features = ["std"]} |
| @@ -2,5 +2,5 @@ · 1 unmodified line | ||
| 2 | 2 | use libfuzzer_sys::fuzz_target; |
| 3 | 3 | |
| 4 | 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 | 6 | use bitflags::bitflags; |
| 7 | 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 | 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 | 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 | 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`. | |
| 18 | − pub path: BString, | |
| 61 | + /// For example, `:(top,literal,icase,attr,exclude)some/path` would yield `some/path`. | |
| 62 | + path: BString, | |
| 19 | 63 | /// All magic signatures that were included in the pathspec. |
| 20 | 64 | pub signature: MagicSignature, |
| 21 | 65 | /// The search mode of the pathspec. |
| @@ -24,11 +68,18 @@ pub struct Pattern { · 2 unmodified lines | ||
| 24 | 68 | /// |
| 25 | 69 | /// `:(attr:a=one b=):path` would yield attribute `a` and `b`. |
| 26 | 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 | 80 | bitflags! { |
| 30 | 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 | 83 | pub struct MagicSignature: u32 { |
| 33 | 84 | /// Matches patterns from the root of the repository |
| 34 | 85 | const TOP = 1 << 0; |
| @@ -36,12 +87,15 @@ bitflags! { · 1 unmodified line | ||
| 36 | 87 | const ICASE = 1 << 1; |
| 37 | 88 | /// Excludes the matching patterns from the previous results |
| 38 | 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 | 96 | /// Parts of [magic signatures][MagicSignature] which don't stack as they all configure |
| 43 | 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 | 99 | pub enum MatchMode { |
| 46 | 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 | 108 | PathAwareGlob, |
| 55 | 109 | } |
| 56 | 110 | |
| 57 | −/// Parse a git-style pathspec into a [`Pattern`][Pattern]. | |
| 58 | −pub fn parse(input: &[u8]) -> Result<Pattern, parse::Error> { | |
| 59 | − Pattern::from_bytes(input) | |
| 111 | +/// Parse a git-style pathspec into a [`Pattern`][Pattern], | |
| 112 | +/// setting the given `default` values in case these aren't specified in `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 | 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 | 22 | /// The error returned by [parse()][crate::parse()]. |
| 8 | 23 | #[derive(thiserror::Error, Debug)] |
| 9 | 24 | #[allow(missing_docs)] |
| @@ -32,16 +47,31 @@ pub enum Error { · 22 unmodified lines | ||
| 32 | 47 | |
| 33 | 48 | impl Pattern { |
| 34 | 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 | 58 | if input.is_empty() { |
| 37 | 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 | 71 | let mut p = Pattern { |
| 41 | − path: BString::default(), | |
| 42 | − signature: MagicSignature::empty(), | |
| 43 | − search_mode: MatchMode::ShellGlob, | |
| 44 | − attributes: Vec::new(), | |
| 72 | + signature, | |
| 73 | + search_mode, | |
| 74 | + ..Default::default() | |
| 45 | 75 | }; |
| 46 | 76 | |
| 47 | 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 | 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 | 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 | 136 | let input = &input[*cursor..end]; |
| 91 | 137 | *cursor = end + 1; |
| 92 | 138 | |
| 93 | − debug_assert_eq!(p.search_mode, MatchMode::default()); | |
| 94 | − | |
| 95 | 139 | if input.is_empty() { |
| 96 | 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 | 29 | # empty_signatures |
| 30 | 30 | baseline '.' |
| 31 | +baseline ':' | |
| 31 | 32 | baseline 'some/path' |
| 32 | 33 | baseline ':some/path' |
| 33 | 34 | baseline ':()some/path' |
| @@ -47,8 +48,11 @@ baseline ':! some/path' · 13 unmodified lines | ||
| 47 | 48 | |
| 48 | 49 | # short_signatures |
| 49 | 50 | baseline ':/some/path' |
| 51 | +baseline '://some/path' | |
| 50 | 52 | baseline ':^some/path' |
| 53 | +baseline ':^^some/path' | |
| 51 | 54 | baseline ':!some/path' |
| 55 | +baseline ':!!some/path' | |
| 52 | 56 | baseline ':/!some/path' |
| 53 | 57 | baseline ':!/^/:some/path' |
| 54 | 58 | |
| @@ -143,3 +147,12 @@ baseline ':(top' · 88 unmodified lines | ||
| 143 | 147 | |
| 144 | 148 | # glob_and_literal_keywords_present |
| 145 | 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 | 1 | pub use gix_testtools::Result; |
| 2 | 2 | |
| 3 | −mod parse { | |
| 4 | − use std::collections::HashMap; | |
| 5 | − | |
| 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 | −} | |
| 3 | +mod normalize; | |
| 4 | +mod parse; | |
| 5 | +mod search; | |
| @@ -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 | 164 | gix-worktree = { version = "^0.24.0", path = "../gix-worktree" } |
| 165 | 165 | gix-hashtable = { version = "^0.2.4", path = "../gix-hashtable" } |
| 166 | 166 | gix-commitgraph = { version = "^0.18.2", path = "../gix-commitgraph" } |
| 167 | +gix-pathspec = { version = "^0.1.0", path = "../gix-pathspec" } | |
| 167 | 168 | |
| 168 | 169 | gix-worktree-stream = { version = "^0.3.0", path = "../gix-worktree-stream", optional = true } |
| 169 | 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 | 99 | pub use gix_object as objs; |
| 100 | 100 | pub use gix_object::bstr; |
| 101 | 101 | pub use gix_odb as odb; |
| 102 | +pub use gix_pathspec as pathspec; | |
| 102 | 103 | pub use gix_prompt as prompt; |
| 103 | 104 | #[cfg(feature = "gix-protocol")] |
| 104 | 105 | pub use gix_protocol as protocol; |
| @@ -45,27 +45,30 @@ impl crate::Repository { · 44 unmodified lines | ||
| 45 | 45 | /// Note that there may be `None` if there is no work tree, even though the `PathBuf` will be empty |
| 46 | 46 | /// if the CWD is at the root of the work tree. |
| 47 | 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>> { | |
| 49 | − self.work_tree.as_ref().map(|root| { | |
| 50 | − std::env::current_dir().and_then(|cwd| { | |
| 51 | − gix_path::realpath_opts(root, &cwd, MAX_SYMLINKS) | |
| 52 | − .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) | |
| 53 | − .and_then(|root| { | |
| 54 | − cwd.strip_prefix(&root) | |
| 55 | − .map_err(|_| { | |
| 56 | − std::io::Error::new( | |
| 57 | − std::io::ErrorKind::Other, | |
| 58 | − format!( | |
| 59 | − "CWD '{}' isn't within the work tree '{}'", | |
| 60 | − cwd.display(), | |
| 61 | − root.display() | |
| 62 | − ), | |
| 63 | − ) | |
| 64 | − }) | |
| 65 | − .map(ToOwned::to_owned) | |
| 66 | − }) | |
| 48 | + pub fn prefix(&self) -> std::io::Result<Option<PathBuf>> { | |
| 49 | + self.work_tree | |
| 50 | + .as_ref() | |
| 51 | + .map(|root| { | |
| 52 | + std::env::current_dir().and_then(|cwd| { | |
| 53 | + gix_path::realpath_opts(root, &cwd, MAX_SYMLINKS) | |
| 54 | + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) | |
| 55 | + .and_then(|root| { | |
| 56 | + cwd.strip_prefix(&root) | |
| 57 | + .map_err(|_| { | |
| 58 | + std::io::Error::new( | |
| 59 | + std::io::ErrorKind::Other, | |
| 60 | + format!( | |
| 61 | + "CWD '{}' isn't within the work tree '{}'", | |
| 62 | + cwd.display(), | |
| 63 | + root.display() | |
| 64 | + ), | |
| 65 | + ) | |
| 66 | + }) | |
| 67 | + .map(ToOwned::to_owned) | |
| 68 | + }) | |
| 69 | + }) | |
| 67 | 70 | }) |
| 68 | − }) | |
| 71 | + .transpose() | |
| 69 | 72 | } |
| 70 | 73 | |
| 71 | 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 | 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 | 1022 | "attributes-query", |
| 1023 | 1023 | trace, |
| 1024 | 1024 | verbose, |
| @@ -1029,15 +1029,17 @@ pub fn main() -> Result<()> { · 4 unmodified lines | ||
| 1029 | 1029 | use gix::bstr::ByteSlice; |
| 1030 | 1030 | core::repository::attributes::query( |
| 1031 | 1031 | repository(Mode::Strict)?, |
| 1032 | − if pathspecs.is_empty() { | |
| 1033 | − Box::new( | |
| 1034 | − stdin_or_bail()? | |
| 1035 | − .byte_lines() | |
| 1036 | − .filter_map(Result::ok) | |
| 1037 | − .filter_map(|line| gix::path::Spec::from_bytes(line.as_bstr())), | |
| 1038 | − ) as Box<dyn Iterator<Item = gix::path::Spec>> | |
| 1032 | + if pathspec.is_empty() { | |
| 1033 | + Box::new(stdin_or_bail()?.byte_lines().filter_map(Result::ok).filter_map(|line| { | |
| 1034 | + gix::pathspec::parse( | |
| 1035 | + line.as_bstr(), | |
| 1036 | + // TODO(pathspec): use `repo` actual global defaults when available (see following as well) | |
| 1037 | + Default::default(), | |
| 1038 | + ) | |
| 1039 | + .ok() | |
| 1040 | + })) as Box<dyn Iterator<Item = gix::pathspec::Pattern>> | |
| 1039 | 1041 | } else { |
| 1040 | − Box::new(pathspecs.into_iter()) | |
| 1042 | + Box::new(pathspec.into_iter()) | |
| 1041 | 1043 | }, |
| 1042 | 1044 | out, |
| 1043 | 1045 | err, |
| @@ -1053,15 +1055,11 @@ pub fn main() -> Result<()> { · 9 unmodified lines | ||
| 1053 | 1055 | progress_keep_open, |
| 1054 | 1056 | None, |
| 1055 | 1057 | move |progress, out, err| { |
| 1056 | − use gix::bstr::ByteSlice; | |
| 1057 | 1058 | core::repository::attributes::validate_baseline( |
| 1058 | 1059 | repository(Mode::StrictWithGitInstallConfig)?, |
| 1059 | − stdin_or_bail().ok().map(|stdin| { | |
| 1060 | − stdin | |
| 1061 | − .byte_lines() | |
| 1062 | − .filter_map(Result::ok) | |
| 1063 | − .filter_map(|line| gix::path::Spec::from_bytes(line.as_bstr())) | |
| 1064 | − }), | |
| 1060 | + stdin_or_bail() | |
| 1061 | + .ok() | |
| 1062 | + .map(|stdin| stdin.byte_lines().filter_map(Result::ok).map(gix::bstr::BString::from)), | |
| 1065 | 1063 | progress, |
| 1066 | 1064 | out, |
| 1067 | 1065 | err, |
| @@ -1078,7 +1076,7 @@ pub fn main() -> Result<()> { · 10 unmodified lines | ||
| 1078 | 1076 | exclude::Subcommands::Query { |
| 1079 | 1077 | statistics, |
| 1080 | 1078 | patterns, |
| 1081 | − pathspecs, | |
| 1079 | + pathspec, | |
| 1082 | 1080 | show_ignore_patterns, |
| 1083 | 1081 | } => prepare_and_run( |
| 1084 | 1082 | "exclude-query", |
| @@ -1088,18 +1086,17 @@ pub fn main() -> Result<()> { · 3 unmodified lines | ||
| 1088 | 1086 | progress_keep_open, |
| 1089 | 1087 | None, |
| 1090 | 1088 | move |_progress, out, err| { |
| 1091 | − use gix::bstr::ByteSlice; | |
| 1092 | 1089 | core::repository::exclude::query( |
| 1093 | 1090 | repository(Mode::Strict)?, |
| 1094 | − if pathspecs.is_empty() { | |
| 1091 | + if pathspec.is_empty() { | |
| 1095 | 1092 | Box::new( |
| 1096 | 1093 | stdin_or_bail()? |
| 1097 | 1094 | .byte_lines() |
| 1098 | 1095 | .filter_map(Result::ok) |
| 1099 | − .filter_map(|line| gix::path::Spec::from_bytes(line.as_bstr())), | |
| 1100 | − ) as Box<dyn Iterator<Item = gix::path::Spec>> | |
| 1096 | + .filter_map(|line| gix::pathspec::parse(&line, Default::default()).ok()), | |
| 1097 | + ) as Box<dyn Iterator<Item = gix::pathspec::Pattern>> | |
| 1101 | 1098 | } else { |
| 1102 | − Box::new(pathspecs.into_iter()) | |
| 1099 | + Box::new(pathspec.into_iter()) | |
| 1103 | 1100 | }, |
| 1104 | 1101 | out, |
| 1105 | 1102 | err, |
| @@ -1115,9 +1112,11 @@ pub fn main() -> Result<()> { · 9 unmodified lines | ||
| 1115 | 1112 | }, |
| 1116 | 1113 | Subcommands::Index(cmd) => match cmd { |
| 1117 | 1114 | index::Subcommands::Entries { |
| 1115 | + format: entry_format, | |
| 1118 | 1116 | no_attributes, |
| 1119 | 1117 | attributes_from_index, |
| 1120 | 1118 | statistics, |
| 1119 | + pathspec, | |
| 1121 | 1120 | } => prepare_and_run( |
| 1122 | 1121 | "index-entries", |
| 1123 | 1122 | trace, |
| @@ -1128,10 +1127,15 @@ pub fn main() -> Result<()> { · 4 unmodified lines | ||
| 1128 | 1127 | move |_progress, out, err| { |
| 1129 | 1128 | core::repository::index::entries( |
| 1130 | 1129 | repository(Mode::LenientWithGitInstallConfig)?, |
| 1130 | + pathspec, | |
| 1131 | 1131 | out, |
| 1132 | 1132 | err, |
| 1133 | 1133 | core::repository::index::entries::Options { |
| 1134 | 1134 | format, |
| 1135 | + simple: match entry_format { | |
| 1136 | + index::entries::Format::Simple => true, | |
| 1137 | + index::entries::Format::Rich => false, | |
| 1138 | + }, | |
| 1135 | 1139 | attributes: if no_attributes { |
| 1136 | 1140 | None |
| 1137 | 1141 | } else { |
| @@ -610,7 +610,7 @@ pub mod attributes { · 609 unmodified lines | ||
| 610 | 610 | statistics: bool, |
| 611 | 611 | /// The git path specifications to list attributes for, or unset to read from stdin one per line. |
| 612 | 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 | 639 | patterns: Vec<OsString>, |
| 640 | 640 | /// The git path specifications to check for exclusion, or unset to read from stdin one per line. |
| 641 | 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 | 647 | pub mod index { |
| 648 | + use crate::shared::AsPathSpec; | |
| 648 | 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 | 662 | #[derive(Debug, clap::Subcommand)] |
| 651 | 663 | pub enum Subcommands { |
| 652 | 664 | /// Print all entries to standard output |
| 653 | 665 | Entries { |
| 666 | + /// How to output index entries. | |
| 667 | + #[clap(long, short = 'f', default_value = "simple", value_enum)] | |
| 668 | + format: entries::Format, | |
| 654 | 669 | /// Do not visualize excluded entries or attributes per path. |
| 655 | 670 | #[clap(long)] |
| 656 | 671 | no_attributes: bool, |
| @@ -662,6 +677,9 @@ pub mod index { · 5 unmodified lines | ||
| 662 | 677 | /// Print various statistics to stderr |
| 663 | 678 | #[clap(long, short = 's')] |
| 664 | 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 | 684 | /// Create an index from a tree-ish. |
| 667 | 685 | #[clap(visible_alias = "read-tree")] |
| @@ -125,7 +125,7 @@ pub mod tools { · 124 unmodified lines | ||
| 125 | 125 | TracePath { |
| 126 | 126 | /// The path to trace through history. |
| 127 | 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 | 349 | pub struct AsPathSpec; |
| 350 | 350 | |
| 351 | 351 | impl TypedValueParser for AsPathSpec { |
| 352 | − type Value = gix::path::Spec; | |
| 352 | + type Value = gix::pathspec::Pattern; | |
| 353 | 353 | |
| 354 | 354 | fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> { |
| 355 | 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 | 365 | .parse_ref(cmd, arg, value) |
| 358 | 366 | } |
| 359 | 367 | } |