diff --git a/Cargo.lock b/Cargo.lock index 8555e34e82b..e11cda49eea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1075,8 +1075,8 @@ dependencies = [ name = "git-attributes" version = "0.1.0" dependencies = [ - "bitflags", "bstr", + "git-glob", "git-quote", "git-testtools", "quick-error", @@ -1179,6 +1179,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "git-glob" +version = "0.2.0" +dependencies = [ + "bitflags", + "bstr", + "git-testtools", + "serde", +] + [[package]] name = "git-hash" version = "0.9.3" @@ -1379,7 +1389,7 @@ dependencies = [ [[package]] name = "git-repository" -version = "0.16.0" +version = "0.17.0" dependencies = [ "anyhow", "byte-unit", @@ -1389,6 +1399,7 @@ dependencies = [ "git-config", "git-diff", "git-features", + "git-glob", "git-hash", "git-index", "git-lock", @@ -1457,6 +1468,10 @@ dependencies = [ "tempfile", ] +[[package]] +name = "git-tix" +version = "0.0.0" + [[package]] name = "git-transport" version = "0.16.0" @@ -1556,7 +1571,7 @@ dependencies = [ [[package]] name = "gitoxide" -version = "0.12.0" +version = "0.13.0" dependencies = [ "anyhow", "atty", @@ -1574,7 +1589,7 @@ dependencies = [ [[package]] name = "gitoxide-core" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "async-io", diff --git a/Cargo.toml b/Cargo.toml index 473a9df77c8..f8bcf00ec93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/Byron/gitoxide" authors = ["Sebastian Thiel "] edition = "2018" license = "MIT OR Apache-2.0" -version = "0.12.0" +version = "0.13.0" default-run = "gix" include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"] resolver = "2" @@ -82,9 +82,9 @@ cache-efficiency-debug = ["git-features/cache-efficiency-debug"] [dependencies] anyhow = "1.0.42" -gitoxide-core = { version = "^0.14.0", path = "gitoxide-core" } +gitoxide-core = { version = "^0.15.0", path = "gitoxide-core" } git-features = { version = "^0.20.0", path = "git-features" } -git-repository = { version = "^0.16.0", path = "git-repository", default-features = false } +git-repository = { version = "^0.17.0", path = "git-repository", default-features = false } git-transport-for-configuration-only = { package = "git-transport", optional = true, version = "^0.16.0", path = "git-transport" } @@ -135,6 +135,7 @@ members = [ "git-chunk", "git-quote", "git-object", + "git-glob", "git-diff", "git-traverse", "git-index", @@ -156,6 +157,7 @@ members = [ "git-repository", "gitoxide-core", "git-tui", + "git-tix", "experiments/object-access", "experiments/diffing", diff --git a/Makefile b/Makefile index 08877c8d273..11a4416f7ca 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,7 @@ check: ## Build all code in suitable configurations cd git-index && cargo check --features serde1 cd git-revision && cargo check --features serde1 cd git-attributes && cargo check --features serde1 + cd git-glob && cargo check --features serde1 cd git-mailmap && cargo check --features serde1 cd git-worktree && cargo check --features serde1 cd git-actor && cargo check --features serde1 diff --git a/README.md b/README.md index d0bd828fcff..603bc1cf3b6 100644 --- a/README.md +++ b/README.md @@ -90,15 +90,16 @@ Follow linked crate name for detailed status. Please note that all crates follow Crates that seem feature complete and need to see some more use before they can be released as 1.0. * [git-mailmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-mailmap) +* [git-chunk](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-chunk) ### Initial Development * **usable** * [git-actor](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-actor) * [git-hash](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-hash) - * [git-chunk](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-chunk) * [git-object](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-object) * [git-validate](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-validate) * [git-url](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-url) + * [git-glob](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-glob) * [git-packetline](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-packetline) * [git-transport](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-transport) * [git-protocol](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-protocol) @@ -124,6 +125,7 @@ Crates that seem feature complete and need to see some more use before they can * [git-pathspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-pathspec) * [git-subomdule](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-submodule) * [git-tui](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-tui) + * [git-tix](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-tix) * [git-bundle](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-bundle) ### Stress Testing diff --git a/cargo-smart-release/Cargo.toml b/cargo-smart-release/Cargo.toml index 767d936ce2c..55b1c394a20 100644 --- a/cargo-smart-release/Cargo.toml +++ b/cargo-smart-release/Cargo.toml @@ -26,7 +26,7 @@ test = false cache-efficiency-debug = ["git-repository/cache-efficiency-debug"] [dependencies] -git-repository = { version = "^0.16.0", path = "../git-repository", features = ["unstable"] } +git-repository = { version = "^0.17.0", path = "../git-repository", features = ["unstable"] } anyhow = "1.0.42" clap = { version = "3.0.0", features = ["derive", "cargo"] } env_logger = { version = "0.9.0", default-features = false, features = ["humantime", "termcolor", "atty"] } diff --git a/crate-status.md b/crate-status.md index f50727df0fa..1ffb2607892 100644 --- a/crate-status.md +++ b/crate-status.md @@ -1,5 +1,4 @@ ### git-actor - * [x] read and write a signature that uniquely identifies an actor within a git repository ### git-hash @@ -9,8 +8,8 @@ * [ ] Some examples ### git-chunk -* [ ] decode the chunk file table of contents and provide convenient API -* [ ] write the table of contents +* [x] decode the chunk file table of contents and provide convenient API +* [x] write the table of contents ### git-object * *decode (zero-copy)* borrowed objects @@ -28,7 +27,6 @@ * [ ] Some examples ### git-pack - * **packs** * [x] traverse pack index * [x] 'object' abstraction @@ -73,7 +71,6 @@ * [ ] Some examples ### git-odb - * **loose object store** * [x] traverse * [x] read @@ -210,25 +207,21 @@ Check out the [performance discussion][git-traverse-performance] as well. * [ ] Some examples ### git-attributes - * [x] parse git-ignore files (aka git-attributes without the attributes or negation) * [x] parse git-attributes files * [ ] create an attributes stack, ideally one that includes 'ignored' status from .gitignore files. * [ ] support for built-in `binary` macro for `-text -diff -merge` ### git-quote - * **ansi-c** * [x] quote * [ ] unquote ### git-mailmap - * [x] parsing * [x] lookup and mapping of author names ### git-pathspec - * [ ] parse * [ ] check for match @@ -238,6 +231,10 @@ A mechanism to associate metadata with any object, and keep revisions of it usin * [ ] CRUD for git notes +### git-glob +* [x] parse pattern +* [x] a type for pattern matching of paths and non-paths, optionally case-insensitively. + ### git-worktree * handle the working tree/checkout - [x] checkout an index of files, executables and symlinks just as fast as git @@ -263,7 +260,6 @@ A mechanism to associate metadata with any object, and keep revisions of it usin * parse specifications into revisions (like `git rev-parse`) ### git-submodule - * CRUD for submodules * try to handle with all the nifty interactions and be a little more comfortable than what git offers, lay a foundation for smarter git submodules. @@ -488,6 +484,10 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. ### git-tui * _a terminal user interface seeking to replace and improve on `tig`_ * Can display complex history in novel ways to make them graspable. Maybe [this post] can be an inspiration. + +### git-tix + +A re-implementation of a minimal `tig` like UI that aims to be fast and to the point. [tagname-validation]: https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt#L23:L23 [this post]: http://blog.danieljanus.pl/2021/07/01/commit-groups/ diff --git a/experiments/diffing/Cargo.toml b/experiments/diffing/Cargo.toml index 27eb799f385..0b88cf36527 100644 --- a/experiments/diffing/Cargo.toml +++ b/experiments/diffing/Cargo.toml @@ -9,7 +9,7 @@ publish = false [dependencies] anyhow = "1" -git-repository = { version = "^0.16.0", path = "../../git-repository", features = ["unstable"] } +git-repository = { version = "^0.17.0", path = "../../git-repository", features = ["unstable"] } git-features-for-config = { package = "git-features", version = "^0.20.0", path = "../../git-features", features = ["cache-efficiency-debug"] } git2 = "0.14" rayon = "1.5.0" diff --git a/experiments/object-access/Cargo.toml b/experiments/object-access/Cargo.toml index 12cda474689..6a05cb6168a 100644 --- a/experiments/object-access/Cargo.toml +++ b/experiments/object-access/Cargo.toml @@ -11,7 +11,7 @@ publish = false [dependencies] anyhow = "1" -git-repository = { path = "../../git-repository", version = "^0.16.0", features = ["unstable"] } +git-repository = { path = "../../git-repository", version = "^0.17.0", features = ["unstable"] } git2 = "0.14" rayon = "1.5.0" parking_lot = { version = "0.12.0", default-features = false } diff --git a/experiments/traversal/Cargo.toml b/experiments/traversal/Cargo.toml index d48c4d4e2b9..183904087a9 100644 --- a/experiments/traversal/Cargo.toml +++ b/experiments/traversal/Cargo.toml @@ -9,7 +9,7 @@ publish = false [dependencies] anyhow = "1" -git-repository = { version = "^0.16.0", path = "../../git-repository", features = ["unstable"] } +git-repository = { version = "^0.17.0", path = "../../git-repository", features = ["unstable"] } git2 = "0.14" rayon = "1.5.0" dashmap = "5.1.0" diff --git a/git-actor/src/signature/mod.rs b/git-actor/src/signature/mod.rs index 5295c4dca6c..b20083a9b25 100644 --- a/git-actor/src/signature/mod.rs +++ b/git-actor/src/signature/mod.rs @@ -1,7 +1,8 @@ mod _ref { - use crate::{signature::decode, Signature, SignatureRef}; use bstr::ByteSlice; + use crate::{signature::decode, Signature, SignatureRef}; + impl<'a> SignatureRef<'a> { /// Deserialize a signature from the given `data`. pub fn from_bytes(data: &'a [u8]) -> Result, nom::Err> diff --git a/git-attributes/Cargo.toml b/git-attributes/Cargo.toml index fce0ef71a6b..524ce2c444f 100644 --- a/git-attributes/Cargo.toml +++ b/git-attributes/Cargo.toml @@ -12,15 +12,15 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1"] +serde1 = ["serde", "bstr/serde1", "git-glob/serde1"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] git-quote = { version = "^0.2.0", path = "../git-quote" } +git-glob = { version = "^0.2.0", path = "../git-glob" } bstr = { version = "0.2.13", default-features = false, features = ["std"]} -bitflags = "1.3.2" unicode-bom = "1.1.4" quick-error = "2.0.0" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} diff --git a/git-attributes/src/ignore.rs b/git-attributes/src/ignore.rs deleted file mode 100644 index 77a48989d78..00000000000 --- a/git-attributes/src/ignore.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub mod pattern { - use bitflags::bitflags; - - bitflags! { - #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] - pub struct Mode: u32 { - /// The pattern does not contain a sub-directory and - it doesn't contain slashes after removing the trailing one. - const NO_SUB_DIR = 1 << 0; - /// A pattern that is '*literal', meaning that it ends with what's given here - const ENDS_WITH = 1 << 1; - /// The pattern must match a directory, and not a file. - const MUST_BE_DIR = 1 << 2; - const NEGATIVE = 1 << 3; - } - } -} diff --git a/git-attributes/src/lib.rs b/git-attributes/src/lib.rs index 0d08f1e1963..41866e02f08 100644 --- a/git-attributes/src/lib.rs +++ b/git-attributes/src/lib.rs @@ -18,8 +18,6 @@ pub enum State<'a> { Unspecified, } -pub mod ignore; - pub mod parse; pub fn parse(buf: &[u8]) -> parse::Lines<'_> { diff --git a/git-attributes/src/parse/attribute.rs b/git-attributes/src/parse/attribute.rs index db09daf536f..8d044e933a1 100644 --- a/git-attributes/src/parse/attribute.rs +++ b/git-attributes/src/parse/attribute.rs @@ -6,8 +6,9 @@ use bstr::{BStr, BString, ByteSlice}; #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub enum Kind { /// A pattern to match paths against - Pattern(BString, crate::ignore::pattern::Mode), + Pattern(git_glob::Pattern), /// The name of the macro to define, always a valid attribute name + // TODO: turn it into its own type for maximum safety Macro(BString), } @@ -162,14 +163,14 @@ fn parse_line(line: &BStr, line_number: usize) -> Option, _ => unreachable!("BUG: check_attr() must only return attribute errors"), }), None => { - let (pattern, flags) = super::ignore::parse_line(line.as_ref())?; - if flags.contains(crate::ignore::pattern::Mode::NEGATIVE) { + let pattern = git_glob::Pattern::from_bytes(line.as_ref())?; + if pattern.mode.contains(git_glob::pattern::Mode::NEGATIVE) { Err(Error::PatternNegation { line: line.into_owned(), line_number, }) } else { - Ok(Kind::Pattern(pattern, flags)) + Ok(Kind::Pattern(pattern)) } } }; diff --git a/git-attributes/src/parse/ignore.rs b/git-attributes/src/parse/ignore.rs index 8a3e92df9d1..71c29904ba2 100644 --- a/git-attributes/src/parse/ignore.rs +++ b/git-attributes/src/parse/ignore.rs @@ -1,6 +1,4 @@ -use bstr::{BString, ByteSlice}; - -use crate::ignore; +use bstr::ByteSlice; pub struct Lines<'a> { lines: bstr::Lines<'a>, @@ -18,7 +16,7 @@ impl<'a> Lines<'a> { } impl<'a> Iterator for Lines<'a> { - type Item = (BString, ignore::pattern::Mode, usize); + type Item = (git_glob::Pattern, usize); fn next(&mut self) -> Option { for line in self.lines.by_ref() { @@ -26,77 +24,11 @@ impl<'a> Iterator for Lines<'a> { if line.first() == Some(&b'#') { continue; } - match parse_line(line) { + match git_glob::Pattern::from_bytes(line) { None => continue, - Some((line, flags)) => return Some((line, flags, self.line_no)), + Some(pattern) => return Some((pattern, self.line_no)), } } None } } - -#[inline] -pub(crate) fn parse_line(mut line: &[u8]) -> Option<(BString, ignore::pattern::Mode)> { - let mut mode = ignore::pattern::Mode::empty(); - if line.is_empty() { - return None; - }; - if line.first() == Some(&b'!') { - mode |= ignore::pattern::Mode::NEGATIVE; - line = &line[1..]; - } else if line.first() == Some(&b'\\') { - let second = line.get(1); - if second == Some(&b'!') || second == Some(&b'#') { - line = &line[1..]; - } - } - if line.iter().all(|b| b.is_ascii_whitespace()) { - return None; - } - let mut line = truncate_non_escaped_trailing_spaces(line); - if line.last() == Some(&b'/') { - mode |= ignore::pattern::Mode::MUST_BE_DIR; - line.pop(); - } - if !line.contains(&b'/') { - mode |= ignore::pattern::Mode::NO_SUB_DIR; - } - if line.first() == Some(&b'*') && line[1..].find_byteset(br"*?[\").is_none() { - mode |= ignore::pattern::Mode::ENDS_WITH; - } - Some((line, mode)) -} - -/// We always copy just because that's ultimately needed anyway, not because we always have to. -fn truncate_non_escaped_trailing_spaces(buf: &[u8]) -> BString { - match buf.rfind_not_byteset(br"\ ") { - Some(pos) if pos + 1 == buf.len() => buf.into(), // does not end in (escaped) whitespace - None => buf.into(), - Some(start_of_non_space) => { - // This seems a bit strange but attempts to recreate the git implementation while - // actually removing the escape characters before spaces. We leave other backslashes - // for escapes to be handled by `glob/globset`. - let mut res: BString = buf[..start_of_non_space + 1].into(); - - let mut trailing_bytes = buf[start_of_non_space + 1..].iter(); - let mut bare_spaces = 0; - while let Some(b) = trailing_bytes.next() { - match b { - b' ' => { - bare_spaces += 1; - } - b'\\' => { - res.extend(std::iter::repeat(b' ').take(bare_spaces)); - bare_spaces = 0; - // Skip what follows, like git does, but keep spaces if possible. - if trailing_bytes.next() == Some(&b' ') { - res.push(b' '); - } - } - _ => unreachable!("BUG: this must be either backslash or space"), - } - } - res - } - } -} diff --git a/git-attributes/tests/parse/attribute.rs b/git-attributes/tests/parse/attribute.rs index 09394c18743..abc77196239 100644 --- a/git-attributes/tests/parse/attribute.rs +++ b/git-attributes/tests/parse/attribute.rs @@ -1,13 +1,17 @@ use bstr::{BStr, ByteSlice}; -use git_attributes::{ignore::pattern::Mode, parse, State}; +use git_attributes::{parse, State}; +use git_glob::pattern::Mode; use git_testtools::fixture_bytes; #[test] fn byte_order_marks_are_no_patterns() { - assert_eq!(line("\u{feff}hello"), (pattern(r"hello", Mode::NO_SUB_DIR), vec![], 1)); + assert_eq!( + line("\u{feff}hello"), + (pattern(r"hello", Mode::NO_SUB_DIR, None), vec![], 1) + ); assert_eq!( line("\u{feff}\"hello\""), - (pattern(r"hello", Mode::NO_SUB_DIR), vec![], 1) + (pattern(r"hello", Mode::NO_SUB_DIR, None), vec![], 1) ); } @@ -17,15 +21,19 @@ fn line_numbers_are_counted_correctly() { assert_eq!( try_lines(&String::from_utf8(input).unwrap()).unwrap(), vec![ - (pattern(r"*.[oa]", Mode::NO_SUB_DIR), vec![set("c")], 2), + (pattern(r"*.[oa]", Mode::NO_SUB_DIR, Some(0)), vec![set("c")], 2), ( - pattern(r"*.html", Mode::NO_SUB_DIR | Mode::ENDS_WITH), + pattern(r"*.html", Mode::NO_SUB_DIR | Mode::ENDS_WITH, Some(0)), vec![set("a"), value("b", "c")], 5 ), - (pattern(r"!foo.html", Mode::NO_SUB_DIR), vec![set("x")], 8), - (pattern(r"#a/path", Mode::empty()), vec![unset("a")], 10), - (pattern(r"/*", Mode::empty()), vec![unspecified("b")], 11), + (pattern(r"!foo.html", Mode::NO_SUB_DIR, None), vec![set("x")], 8), + (pattern(r"#a/path", Mode::empty(), None), vec![unset("a")], 10), + ( + pattern(r"*", Mode::ABSOLUTE | Mode::NO_SUB_DIR | Mode::ENDS_WITH, Some(0)), + vec![unspecified("b")], + 11 + ), ] ); } @@ -35,9 +43,9 @@ fn line_endings_can_be_windows_or_unix() { assert_eq!( try_lines("unix\nwindows\r\nlast").unwrap(), vec![ - (pattern(r"unix", Mode::NO_SUB_DIR), vec![], 1), - (pattern(r"windows", Mode::NO_SUB_DIR), vec![], 2), - (pattern(r"last", Mode::NO_SUB_DIR), vec![], 3) + (pattern(r"unix", Mode::NO_SUB_DIR, None), vec![], 1), + (pattern(r"windows", Mode::NO_SUB_DIR, None), vec![], 2), + (pattern(r"last", Mode::NO_SUB_DIR, None), vec![], 3) ] ); } @@ -55,15 +63,15 @@ fn comment_lines_are_ignored_as_well_as_empty_ones() { #[test] fn leading_whitespace_is_ignored() { - assert_eq!(line(" \r\tp"), (pattern(r"p", Mode::NO_SUB_DIR), vec![], 1)); - assert_eq!(line(" \r\t\"p\""), (pattern(r"p", Mode::NO_SUB_DIR), vec![], 1)); + assert_eq!(line(" \r\tp"), (pattern(r"p", Mode::NO_SUB_DIR, None), vec![], 1)); + assert_eq!(line(" \r\t\"p\""), (pattern(r"p", Mode::NO_SUB_DIR, None), vec![], 1)); } #[test] fn quotes_separate_attributes_even_without_whitespace() { assert_eq!( line(r#""path"a b"#), - (pattern(r"path", Mode::NO_SUB_DIR), vec![set("a"), set("b")], 1) + (pattern(r"path", Mode::NO_SUB_DIR, None), vec![set("a"), set("b")], 1) ); } @@ -71,15 +79,21 @@ fn quotes_separate_attributes_even_without_whitespace() { fn comment_can_be_escaped_like_gitignore_or_quoted() { assert_eq!( line(r"\#hello"), - (pattern(r"#hello", Mode::NO_SUB_DIR), vec![], 1), + (pattern(r"#hello", Mode::NO_SUB_DIR, None), vec![], 1), "undocumented, but definitely works" ); - assert_eq!(line("\"# hello\""), (pattern(r"# hello", Mode::NO_SUB_DIR), vec![], 1)); + assert_eq!( + line("\"# hello\""), + (pattern(r"# hello", Mode::NO_SUB_DIR, None), vec![], 1) + ); } #[test] fn exclamation_marks_must_be_escaped_or_error_unlike_gitignore() { - assert_eq!(line(r"\!hello"), (pattern(r"!hello", Mode::NO_SUB_DIR), vec![], 1)); + assert_eq!( + line(r"\!hello"), + (pattern(r"!hello", Mode::NO_SUB_DIR, None), vec![], 1) + ); assert!(matches!( try_line(r"!hello"), Err(parse::Error::PatternNegation { line_number: 1, .. }) @@ -93,7 +107,7 @@ fn exclamation_marks_must_be_escaped_or_error_unlike_gitignore() { ); assert_eq!( line(r#""\\!hello""#), - (pattern(r"!hello", Mode::NO_SUB_DIR), vec![], 1), + (pattern(r"!hello", Mode::NO_SUB_DIR, None), vec![], 1), "…and must be double-escaped, once to get through quote, then to get through parse ignore line" ); } @@ -155,32 +169,32 @@ fn attribute_names_must_not_begin_with_dash_and_must_be_ascii_only() { fn attributes_are_parsed_behind_various_whitespace_characters() { assert_eq!( line(r#"p a b"#), - (pattern("p", Mode::NO_SUB_DIR), vec![set("a"), set("b")], 1), + (pattern("p", Mode::NO_SUB_DIR, None), vec![set("a"), set("b")], 1), "behind space" ); assert_eq!( line(r#""p" a b"#), - (pattern("p", Mode::NO_SUB_DIR), vec![set("a"), set("b")], 1), + (pattern("p", Mode::NO_SUB_DIR, None), vec![set("a"), set("b")], 1), "behind space" ); assert_eq!( line("p\ta\tb"), - (pattern("p", Mode::NO_SUB_DIR), vec![set("a"), set("b")], 1), + (pattern("p", Mode::NO_SUB_DIR, None), vec![set("a"), set("b")], 1), "behind tab" ); assert_eq!( line("\"p\"\ta\tb"), - (pattern("p", Mode::NO_SUB_DIR), vec![set("a"), set("b")], 1), + (pattern("p", Mode::NO_SUB_DIR, None), vec![set("a"), set("b")], 1), "behind tab" ); assert_eq!( line("p \t a \t b"), - (pattern("p", Mode::NO_SUB_DIR), vec![set("a"), set("b")], 1), + (pattern("p", Mode::NO_SUB_DIR, None), vec![set("a"), set("b")], 1), "behind a mix of space and tab" ); assert_eq!( line("\"p\" \t a \t b"), - (pattern("p", Mode::NO_SUB_DIR), vec![set("a"), set("b")], 1), + (pattern("p", Mode::NO_SUB_DIR, None), vec![set("a"), set("b")], 1), "behind a mix of space and tab" ); } @@ -190,7 +204,7 @@ fn attributes_come_in_different_flavors_due_to_prefixes() { assert_eq!( line(r#"p set -unset !unspecified -set"#), ( - pattern("p", Mode::NO_SUB_DIR), + pattern("p", Mode::NO_SUB_DIR, None), vec![set("set"), unset("unset"), unspecified("unspecified"), unset("set")], 1 ), @@ -203,7 +217,7 @@ fn attributes_can_have_values() { assert_eq!( line(r#"p a=one b=2 c=你好 "#), ( - pattern("p", Mode::NO_SUB_DIR), + pattern("p", Mode::NO_SUB_DIR, None), vec![value("a", "one"), value("b", "2"), value("c", "你好")], 1 ), @@ -216,7 +230,7 @@ fn attributes_see_state_adjustments_over_value_assignments() { assert_eq!( line(r#"p set -unset=a !unspecified=b"#), ( - pattern("p", Mode::NO_SUB_DIR), + pattern("p", Mode::NO_SUB_DIR, None), vec![set("set"), unset("unset"), unspecified("unspecified")], 1 ) @@ -225,10 +239,13 @@ fn attributes_see_state_adjustments_over_value_assignments() { #[test] fn trailing_whitespace_in_attributes_is_ignored() { - assert_eq!(line("p a \r\t"), (pattern("p", Mode::NO_SUB_DIR), vec![set("a")], 1),); + assert_eq!( + line("p a \r\t"), + (pattern("p", Mode::NO_SUB_DIR, None), vec![set("a")], 1), + ); assert_eq!( line("\"p\" a \r\t"), - (pattern("p", Mode::NO_SUB_DIR), vec![set("a")], 1), + (pattern("p", Mode::NO_SUB_DIR, None), vec![set("a")], 1), ); } @@ -250,8 +267,13 @@ fn value<'a, 'b>(attr: &'a str, value: &'b str) -> (&'a BStr, State<'b>) { (attr.as_bytes().as_bstr(), State::Value(value.as_bytes().as_bstr())) } -fn pattern(name: &str, flags: git_attributes::ignore::pattern::Mode) -> parse::Kind { - parse::Kind::Pattern(name.into(), flags) +fn pattern(name: &str, flags: git_glob::pattern::Mode, first_wildcard_pos: Option) -> parse::Kind { + parse::Kind::Pattern(git_glob::Pattern { + text: name.into(), + mode: flags, + first_wildcard_pos, + base_path: None, + }) } fn macro_(name: &str) -> parse::Kind { diff --git a/git-attributes/tests/parse/ignore.rs b/git-attributes/tests/parse/ignore.rs index e39a194e493..f3c94e059ac 100644 --- a/git-attributes/tests/parse/ignore.rs +++ b/git-attributes/tests/parse/ignore.rs @@ -1,10 +1,11 @@ -use git_attributes::ignore::pattern::Mode; +use bstr::BString; +use git_glob::{pattern::Mode, Pattern}; use git_testtools::fixture_bytes; #[test] fn byte_order_marks_are_no_patterns() { assert_eq!( - git_attributes::parse::ignore("\u{feff}hello".as_bytes()).next(), + flatten(git_attributes::parse::ignore("\u{feff}hello".as_bytes()).next()), Some((r"hello".into(), Mode::NO_SUB_DIR, 1)) ); } @@ -12,17 +13,17 @@ fn byte_order_marks_are_no_patterns() { #[test] fn line_numbers_are_counted_correctly() { let input = fixture_bytes("ignore/various.txt"); - let actual: Vec<_> = git_attributes::parse::ignore(&input).collect(); + let actual: Vec<_> = git_attributes::parse::ignore(&input).map(flat_map).collect(); assert_eq!( actual, vec![ ("*.[oa]".into(), Mode::NO_SUB_DIR, 2), ("*.html".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH, 5), ("foo.html".into(), Mode::NO_SUB_DIR | Mode::NEGATIVE, 8), - ("/*".into(), Mode::empty(), 11), - ("/foo".into(), Mode::NEGATIVE, 12), - ("/foo/*".into(), Mode::empty(), 13), - ("/foo/bar".into(), Mode::NEGATIVE, 14) + ("*".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH | Mode::ABSOLUTE, 11), + ("foo".into(), Mode::NEGATIVE | Mode::NO_SUB_DIR | Mode::ABSOLUTE, 12), + ("foo/*".into(), Mode::ABSOLUTE, 13), + ("foo/bar".into(), Mode::ABSOLUTE | Mode::NEGATIVE, 14) ] ); } @@ -30,7 +31,9 @@ fn line_numbers_are_counted_correctly() { #[test] fn line_endings_can_be_windows_or_unix() { assert_eq!( - git_attributes::parse::ignore(b"unix\nwindows\r\nlast").collect::>(), + git_attributes::parse::ignore(b"unix\nwindows\r\nlast") + .map(flat_map) + .collect::>(), vec![ (r"unix".into(), Mode::NO_SUB_DIR, 1), (r"windows".into(), Mode::NO_SUB_DIR, 2), @@ -39,32 +42,6 @@ fn line_endings_can_be_windows_or_unix() { ); } -#[test] -fn mark_ends_with_pattern_specifically() { - assert_eq!( - git_attributes::parse::ignore(br"*literal").next(), - Some((r"*literal".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH, 1)) - ); - assert_eq!( - git_attributes::parse::ignore(br"**literal").next(), - Some((r"**literal".into(), Mode::NO_SUB_DIR, 1)), - "double-asterisk won't allow for fast comparisons" - ); - assert_eq!( - git_attributes::parse::ignore(br"*litera[l]").next(), - Some((r"*litera[l]".into(), Mode::NO_SUB_DIR, 1)) - ); - assert_eq!( - git_attributes::parse::ignore(br"*litera?").next(), - Some((r"*litera?".into(), Mode::NO_SUB_DIR, 1)) - ); - assert_eq!( - git_attributes::parse::ignore(br"*litera\?").next(), - Some((r"*litera\?".into(), Mode::NO_SUB_DIR, 1)), - "for now we don't handle escapes properly like git seems to do" - ); -} - #[test] fn comments_are_ignored_as_well_as_empty_ones() { assert!(git_attributes::parse::ignore(b"# hello world").next().is_none()); @@ -74,108 +51,15 @@ fn comments_are_ignored_as_well_as_empty_ones() { #[test] fn backslashes_before_hashes_are_no_comments() { assert_eq!( - git_attributes::parse::ignore(br"\#hello").next(), + flatten(git_attributes::parse::ignore(br"\#hello").next()), Some((r"#hello".into(), Mode::NO_SUB_DIR, 1)) ); } -#[test] -fn backslashes_are_part_of_the_pattern_if_not_in_specific_positions() { - assert_eq!( - git_attributes::parse::ignore(br"\hello\world").next(), - Some((r"\hello\world".into(), Mode::NO_SUB_DIR, 1)) - ); -} - -#[test] -fn leading_exclamation_mark_negates_pattern() { - assert_eq!( - git_attributes::parse::ignore(b"!hello").next(), - Some(("hello".into(), Mode::NEGATIVE | Mode::NO_SUB_DIR, 1)) - ); -} - -#[test] -fn leading_exclamation_marks_can_be_escaped_with_backslash() { - assert_eq!( - git_attributes::parse::ignore(br"\!hello").next(), - Some(("!hello".into(), Mode::NO_SUB_DIR, 1)) - ); -} - -#[test] -fn absence_of_sub_directories_are_marked() { - assert_eq!( - git_attributes::parse::ignore(br"a/b").next(), - Some(("a/b".into(), Mode::empty(), 1)) - ); - assert_eq!( - git_attributes::parse::ignore(br"ab").next(), - Some(("ab".into(), Mode::NO_SUB_DIR, 1)) - ); -} - -#[test] -fn trailing_slashes_are_marked_and_removed() { - assert_eq!( - git_attributes::parse::ignore(b"dir/").next(), - Some(("dir".into(), Mode::MUST_BE_DIR | Mode::NO_SUB_DIR, 1)) - ); - assert_eq!( - git_attributes::parse::ignore(b"dir///").next(), - Some(("dir//".into(), Mode::MUST_BE_DIR, 1)), - "but only the last slash is removed" - ); -} - -#[test] -fn trailing_spaces_are_ignored() { - assert_eq!( - git_attributes::parse::ignore(br"a ").next(), - Some(("a".into(), Mode::NO_SUB_DIR, 1)) - ); - assert_eq!( - git_attributes::parse::ignore(b"a\t\t ").next(), - Some(("a\t\t".into(), Mode::NO_SUB_DIR, 1)), - "trailing tabs are not ignored" - ); +fn flatten(input: Option<(Pattern, usize)>) -> Option<(BString, git_glob::pattern::Mode, usize)> { + input.map(flat_map) } -#[test] -fn trailing_spaces_can_be_escaped_to_be_literal() { - assert_eq!( - git_attributes::parse::ignore(br"a \ ").next(), - Some(("a ".into(), Mode::NO_SUB_DIR, 1)), - "a single escape in front of the last desired space is enough" - ); - assert_eq!( - git_attributes::parse::ignore(br"a b c ").next(), - Some(("a b c".into(), Mode::NO_SUB_DIR, 1)), - "spaces in the middle are fine" - ); - assert_eq!( - git_attributes::parse::ignore(br"a\ \ \ ").next(), - Some(("a ".into(), Mode::NO_SUB_DIR, 1)), - "one can also escape every single one" - ); - assert_eq!( - git_attributes::parse::ignore(br"a \ ").next(), - Some(("a ".into(), Mode::NO_SUB_DIR, 1)), - "or just the one in the middle, losing the last actual space" - ); - assert_eq!( - git_attributes::parse::ignore(br"a \").next(), - Some(("a ".into(), Mode::NO_SUB_DIR, 1)), - "escaping nothing also works as a whitespace protection" - ); - assert_eq!( - git_attributes::parse::ignore(br"a \\\ ").next(), - Some((r"a ".into(), Mode::NO_SUB_DIR, 1)), - "strange things like these work too" - ); - assert_eq!( - git_attributes::parse::ignore(br"a \\ ").next(), - Some((r"a ".into(), Mode::NO_SUB_DIR, 1)), - "strange things like these work as well" - ); +fn flat_map(input: (Pattern, usize)) -> (BString, git_glob::pattern::Mode, usize) { + (input.0.text, input.0.mode, input.1) } diff --git a/git-config/src/values.rs b/git-config/src/values.rs index e51b38dca38..80620d23785 100644 --- a/git-config/src/values.rs +++ b/git-config/src/values.rs @@ -1,8 +1,8 @@ //! Rust containers for valid `git-config` types. -use bstr::BStr; use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr}; +use bstr::BStr; use quick_error::quick_error; #[cfg(feature = "serde")] use serde::{Serialize, Serializer}; @@ -196,7 +196,7 @@ impl<'a> From> for String<'a> { pub mod path { use std::borrow::Cow; - #[cfg(not(target_os = "windows"))] + #[cfg(not(any(target_os = "android", target_os = "windows")))] use pwd::Passwd; use quick_error::ResultExt; @@ -275,7 +275,7 @@ pub mod path { } } - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "android"))] fn interpolate_user(self) -> Result, interpolate::Error> { Err(interpolate::Error::UserInterpolationUnsupported) } diff --git a/git-config/tests/git_config/mod.rs b/git-config/tests/git_config/mod.rs index ce581d6de4c..904df0f6296 100644 --- a/git-config/tests/git_config/mod.rs +++ b/git-config/tests/git_config/mod.rs @@ -1,8 +1,9 @@ #[cfg(test)] mod mutable_value { - use git_config::file::GitConfig; use std::convert::TryFrom; + use git_config::file::GitConfig; + fn init_config() -> GitConfig<'static> { GitConfig::try_from( r#"[core] @@ -137,9 +138,10 @@ b #[cfg(test)] mod mutable_multi_value { - use git_config::file::GitConfig; use std::{borrow::Cow, convert::TryFrom}; + use git_config::file::GitConfig; + fn init_config() -> GitConfig<'static> { GitConfig::try_from( r#"[core] @@ -277,13 +279,12 @@ a"#, #[cfg(test)] mod from_paths_tests { - use std::borrow::Cow; - use std::path::Path; - use std::{fs, io}; + use std::{borrow::Cow, fs, io, path::Path}; - use git_config::file::from_paths::Error; - use git_config::file::{from_paths, GitConfig}; - use git_config::parser::ParserOrIoError; + use git_config::{ + file::{from_paths, from_paths::Error, GitConfig}, + parser::ParserOrIoError, + }; use tempfile::tempdir; /// Escapes backslash when writing a path as string so that it is a valid windows path @@ -749,12 +750,9 @@ mod from_paths_tests { #[cfg(test)] mod from_env_tests { - use std::borrow::Cow; - use std::{env, fs}; + use std::{borrow::Cow, env, fs}; - use git_config::file::from_paths::Options; - use git_config::file::GitConfig; - use git_config::file::{from_env, from_paths}; + use git_config::file::{from_env, from_paths, from_paths::Options, GitConfig}; use serial_test::serial; use tempfile::tempdir; @@ -892,10 +890,12 @@ mod from_env_tests { #[cfg(test)] mod get_raw_value { - use git_config::file::{GitConfig, GitConfigError}; - use git_config::parser::SectionHeaderName; - use std::borrow::Cow; - use std::convert::TryFrom; + use std::{borrow::Cow, convert::TryFrom}; + + use git_config::{ + file::{GitConfig, GitConfigError}, + parser::SectionHeaderName, + }; #[test] fn single_section() { @@ -956,11 +956,12 @@ mod get_raw_value { #[cfg(test)] mod get_value { - use git_config::file::GitConfig; - use git_config::values::{Boolean, Bytes, TrueVariant}; - use std::borrow::Cow; - use std::convert::TryFrom; - use std::error::Error; + use std::{borrow::Cow, convert::TryFrom, error::Error}; + + use git_config::{ + file::GitConfig, + values::{Boolean, Bytes, TrueVariant}, + }; #[test] fn single_section() -> Result<(), Box> { @@ -1005,10 +1006,12 @@ mod get_value { #[cfg(test)] mod get_raw_multi_value { - use git_config::file::{GitConfig, GitConfigError}; - use git_config::parser::SectionHeaderName; - use std::borrow::Cow; - use std::convert::TryFrom; + use std::{borrow::Cow, convert::TryFrom}; + + use git_config::{ + file::{GitConfig, GitConfigError}, + parser::SectionHeaderName, + }; #[test] fn single_value_is_identical_to_single_value_query() { @@ -1089,9 +1092,10 @@ mod get_raw_multi_value { #[cfg(test)] mod display { - use git_config::file::GitConfig; use std::convert::TryFrom; + use git_config::file::GitConfig; + #[test] fn can_reconstruct_empty_config() { let config = r#" diff --git a/git-glob/CHANGELOG.md b/git-glob/CHANGELOG.md new file mode 100644 index 00000000000..bbf8338db99 --- /dev/null +++ b/git-glob/CHANGELOG.md @@ -0,0 +1,116 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.2.0 (2022-04-13) + +### Changed (BREAKING) + + - `parse()` returns a `Pattern`. + This is much more ergonomic as this is the only things we are ever + interested in for matching. If necessary, from there one can also + use the parts individually or alter them. + +### Commit Statistics + + + + - 50 commits contributed to the release over the course of 6 calendar days. + - 6 days passed between releases. + - 1 commit where understood as [conventional](https://www.conventionalcommits.org). + - 1 unique issue was worked on: [#301](https://github.com/Byron/gitoxide/issues/301) + +### Thanks Clippy + + + +[Clippy](https://github.com/rust-lang/rust-clippy) helped 3 times to make code idiomatic. + +### Commit Details + + + +
view details + + * **[#301](https://github.com/Byron/gitoxide/issues/301)** + - `parse()` returns a `Pattern`. ([`6ce3611`](https://github.com/Byron/gitoxide/commit/6ce3611891d4b60c86055bf749a1b4060ee2c3e1)) + - docs for git-glob ([`8f4969f`](https://github.com/Byron/gitoxide/commit/8f4969fe7c2e3f3bb38275d5e4ccb08d0bde02bb)) + - all wildmatch tests succeed ([`d3a7349`](https://github.com/Byron/gitoxide/commit/d3a7349b707911670f17a92a0f82681544ebc769)) + - add all character classes sans some of the more obscure ones ([`538d41d`](https://github.com/Byron/gitoxide/commit/538d41d51d7cdc472b2a712823a5a69810f75015)) + - frame for character classes ([`6b8d0d2`](https://github.com/Byron/gitoxide/commit/6b8d0d20b449f6adffd403d0555596041a6c1903)) + - fix all remaining bracket tests… ([`3afe2d2`](https://github.com/Byron/gitoxide/commit/3afe2d2b862c9a22b90cbfbf75da6c84ca91ebf4)) + - more bracket-range tests succeed ([`c64f71c`](https://github.com/Byron/gitoxide/commit/c64f71c38ff404e9c9f150e3e6d3e02ca11e9235)) + - make bracket matching work better ([`97aa9ed`](https://github.com/Byron/gitoxide/commit/97aa9ed22ccb927147a1e456ee6e3510ecc9f90a)) + - refactor ([`fa0440f`](https://github.com/Byron/gitoxide/commit/fa0440fb3c80f8052e08526cf260e929722ccf02)) + - first steps towards bracket matching ([`54fe029`](https://github.com/Byron/gitoxide/commit/54fe0294e36e6ae9a025ef8661d5e21fd488dc87)) + - adjust wildmatch corpus expectations as it won't match our preprocessor ([`48990af`](https://github.com/Byron/gitoxide/commit/48990af81110a411ad07e199916005a8885db920)) + - fix another issue around double-star ([`d15c2fb`](https://github.com/Byron/gitoxide/commit/d15c2fb0119edc7635efc174a703101e100c0b4c)) + - fix another special case ([`09095df`](https://github.com/Byron/gitoxide/commit/09095dfb123f419a3df715d48e60e1f8ec62d060)) + - fix double-star matches ([`43371b6`](https://github.com/Byron/gitoxide/commit/43371b6fa0d6e62d9cde0399f1c9dd3e76b95d99)) + - fix single-level double-star ([`e5a7995`](https://github.com/Byron/gitoxide/commit/e5a79951dc32d336ae5b6c4230b3058ed80456d6)) + - fix backslash handling; improve star handling ([`7907cb4`](https://github.com/Byron/gitoxide/commit/7907cb4e12b56bdbea6abdc59f1022a508a83c87)) + - new wildcard tests to help fix star matching ([`d21c654`](https://github.com/Byron/gitoxide/commit/d21c6541959b0fe34a3882ffcb9e657d6c685734)) + - All our simple wildmatches are working, a good start ([`321c4d2`](https://github.com/Byron/gitoxide/commit/321c4d2011617f2b13e29109cafe4566e53bfde3)) + - maybe even working double-star handling ([`48c57ff`](https://github.com/Byron/gitoxide/commit/48c57ff3299928fd427bfae3e4eeadf5a9ca8109)) + - slowly move towards star/double-star ([`4efd215`](https://github.com/Byron/gitoxide/commit/4efd21560c754062f09870d253b6a2809cb0efb1)) + - question mark support ([`e83c8df`](https://github.com/Byron/gitoxide/commit/e83c8df03e801e00571f5934331e004af9774c7f)) + - very basic beginnings of wildmatch ([`334c624`](https://github.com/Byron/gitoxide/commit/334c62459dbb6763a46647a64129f89e27b5781b)) + - fix logic in wildmatch tests; validate feasibility of all test cases ([`1336bc9`](https://github.com/Byron/gitoxide/commit/1336bc938cc43e3a2f9e47af64f2c9933c9fc961)) + - test corpus for wildcard matches ([`bd8f95f`](https://github.com/Byron/gitoxide/commit/bd8f95f757e45b3cf8523d3e11503f4571461abf)) + - frame for wildmatch function and its tests ([`04ca834`](https://github.com/Byron/gitoxide/commit/04ca8349e326f7b7505a9ea49a401565259f21dc)) + - more tests for early exit in case no-wildcard prefix doesn't match ([`1ff348c`](https://github.com/Byron/gitoxide/commit/1ff348c4f09839569dcd8bb93699e7004fa59d4a)) + - more non-basename shortcuts, and only wildcard matches left ([`45c6259`](https://github.com/Byron/gitoxide/commit/45c62597b50c3c4bac34e20cd2040b08833584cc)) + - make much clearer how base-path works and put in safe-guards ([`5bf503a`](https://github.com/Byron/gitoxide/commit/5bf503af86ce0dd4d0a79c9b1a451cf89b494a6e)) + - test that bases are ignored for basenames ([`1b26848`](https://github.com/Byron/gitoxide/commit/1b2684892419f234e6006b0f3820341f162dc28b)) + - refactor ([`056b368`](https://github.com/Byron/gitoxide/commit/056b3683eb2d4d4c478ae2655e6ef067d4d0d1e7)) + - a way to set a globs base path ([`3d58db8`](https://github.com/Byron/gitoxide/commit/3d58db8a9abfb91600216b8fc6f4109f5289d776)) + - get to the point where globs probably should have a base ([`2632988`](https://github.com/Byron/gitoxide/commit/263298876d1b10b12011c2a221b67126d6d8202d)) + - refactor ([`f2f3f53`](https://github.com/Byron/gitoxide/commit/f2f3f53574b4c0b5ba85780b134825f9128fa64f)) + - prepare for handling absolute patterns ([`df9778b`](https://github.com/Byron/gitoxide/commit/df9778b924610f6a82d93cdf12cfddda60e61789)) + - Keep track of absolute patterns, those that have to start with it ([`3956480`](https://github.com/Byron/gitoxide/commit/3956480e6fb5f4766a67ebf2860cae2f48125594)) + - basename parsing with simple pattern skips ([`d18ef14`](https://github.com/Byron/gitoxide/commit/d18ef14e7cbf9c6d316086d6c88b5676c4b7516c)) + - git-baseline now acts like a massive regression test ([`fe3d0a7`](https://github.com/Byron/gitoxide/commit/fe3d0a778210a46d46a7db15cc8d213706e45fee)) + - adjust signatures to know enough to implement git-like matching ([`b947ff9`](https://github.com/Byron/gitoxide/commit/b947ff9d2c5ae8810547066096c91c745d1466fe)) + - refactor; roughly sort regex by simplicity ([`a7c3a63`](https://github.com/Byron/gitoxide/commit/a7c3a630cd5661f26220b494f01e50c9f2dbd2e2)) + - Also parse the position of the first wildcard ([`4178a63`](https://github.com/Byron/gitoxide/commit/4178a6356ad11013ae08b6233de2bfb366bf4278)) + - prepare for upcoming wildcard-length field in glob pattern ([`a11f5d4`](https://github.com/Byron/gitoxide/commit/a11f5d441a22b844caefd31b9cb7783dd6b048ad)) + - refactor ([`f285ca0`](https://github.com/Byron/gitoxide/commit/f285ca03094655590d7014770ffb6f6a77d02289)) + - basic infrastructure for running git-baseline against our implementation ([`027869d`](https://github.com/Byron/gitoxide/commit/027869d57bd7fcb7234e814d1a22197cb64c05cf)) + - baseline tests for matches and no-matches ([`621c2ca`](https://github.com/Byron/gitoxide/commit/621c2cac7eed822cc8226c7b9aa8becf3db6872c)) + - bring in all ~140 tests for git pattern matching, git-ignore styile ([`f9ab830`](https://github.com/Byron/gitoxide/commit/f9ab830df2920387c1cffec048be3a4089f4aa40)) + - refactor ([`dbe7305`](https://github.com/Byron/gitoxide/commit/dbe7305d371c7dad02d8888492b60b882b418a46)) + - refactor ([`8a54341`](https://github.com/Byron/gitoxide/commit/8a543410e10326ce506b8a7ba65e662641835849)) + * **Uncategorized** + - thanks clippy ([`b1a6100`](https://github.com/Byron/gitoxide/commit/b1a610029e1b40600f90194ce986155238f58101)) + - thanks clippy ([`1393403`](https://github.com/Byron/gitoxide/commit/1393403b826cf4a2fbaf6ef58d505c5c62fd5e0a)) + - thanks clippy ([`683233e`](https://github.com/Byron/gitoxide/commit/683233e86dab36cc438bed0f8b0338eb767f57a0)) +
+ +## 0.1.0 (2022-04-07) + +Initial release with pattern parsing functionality. + +### Commit Statistics + + + + - 3 commits contributed to the release. + - 0 commits where understood as [conventional](https://www.conventionalcommits.org). + - 1 unique issue was worked on: [#301](https://github.com/Byron/gitoxide/issues/301) + +### Commit Details + + + +
view details + + * **[#301](https://github.com/Byron/gitoxide/issues/301)** + - prepare changelog prior to release ([`2794bb2`](https://github.com/Byron/gitoxide/commit/2794bb2f6bd80cccba508fa9f251609499167646)) + - Add git-glob crate with pattern matching parsing from git-attributes::ignore ([`b3efc94`](https://github.com/Byron/gitoxide/commit/b3efc94134a32018db1d6a2d7f8cc397c4371999)) + * **Uncategorized** + - Release git-glob v0.1.0 ([`0f66c5d`](https://github.com/Byron/gitoxide/commit/0f66c5d56bd3f0febff881065911638f22e71158)) +
+ diff --git a/git-glob/Cargo.toml b/git-glob/Cargo.toml new file mode 100644 index 00000000000..29dc5cad48a --- /dev/null +++ b/git-glob/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "git-glob" +version = "0.2.0" +repository = "https://github.com/Byron/gitoxide" +license = "MIT/Apache-2.0" +description = "A WIP crate of the gitoxide project dealing with pattern matching" +authors = ["Sebastian Thiel "] +edition = "2018" + +[lib] +doctest = false + +[features] +## Data structures implement `serde::Serialize` and `serde::Deserialize`. +serde1 = ["serde", "bstr/serde1"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bstr = { version = "0.2.13", default-features = false, features = ["std"]} +bitflags = "1.3.2" +serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} + +[dev-dependencies] +git-testtools = { path = "../tests/tools"} diff --git a/git-glob/src/lib.rs b/git-glob/src/lib.rs new file mode 100644 index 00000000000..7202baef281 --- /dev/null +++ b/git-glob/src/lib.rs @@ -0,0 +1,36 @@ +#![forbid(unsafe_code)] +#![deny(rust_2018_idioms, missing_docs)] +//! Provide glob [`Patterns`][Pattern] for matching against paths or anything else. + +use bstr::BString; + +/// A glob pattern at a particular base path. +/// +/// This closely models how patterns appear in a directory hierarchy of include or attribute files. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub struct Pattern { + /// the actual pattern bytes + pub text: BString, + /// Additional information to help accelerate pattern matching. + pub mode: pattern::Mode, + /// The position in `text` with the first wildcard character, or `None` if there is no wildcard at all. + pub first_wildcard_pos: Option, + /// The relative base at which this pattern resides, with trailing slash, using slashes as path separator. + /// If `None`, the pattern is considered to be at the root of the repository. + pub base_path: Option, +} + +/// +pub mod pattern; + +/// +pub mod wildmatch; +pub use wildmatch::function::wildmatch; + +mod parse; + +/// Create a [`Pattern`] by parsing `text` or return `None` if `text` is empty. +pub fn parse(text: &[u8]) -> Option { + Pattern::from_bytes(text) +} diff --git a/git-glob/src/parse.rs b/git-glob/src/parse.rs new file mode 100644 index 00000000000..545686ed6c3 --- /dev/null +++ b/git-glob/src/parse.rs @@ -0,0 +1,84 @@ +use bstr::{BString, ByteSlice}; + +use crate::{pattern, pattern::Mode}; + +#[inline] +/// A sloppy parser that performs only the most basic checks, providing additional information +/// using `pattern::Mode` flags. +/// +/// Returns `(pattern, mode, no_wildcard_len)` +pub fn pattern(mut pat: &[u8]) -> Option<(BString, pattern::Mode, Option)> { + let mut mode = Mode::empty(); + if pat.is_empty() { + return None; + }; + if pat.first() == Some(&b'!') { + mode |= Mode::NEGATIVE; + pat = &pat[1..]; + } else if pat.first() == Some(&b'\\') { + let second = pat.get(1); + if second == Some(&b'!') || second == Some(&b'#') { + pat = &pat[1..]; + } + } + if pat.iter().all(|b| b.is_ascii_whitespace()) { + return None; + } + if pat.first() == Some(&b'/') { + mode |= Mode::ABSOLUTE; + pat = &pat[1..]; + } + let mut line = truncate_non_escaped_trailing_spaces(pat); + if line.last() == Some(&b'/') { + mode |= Mode::MUST_BE_DIR; + line.pop(); + } + if !line.contains(&b'/') { + mode |= Mode::NO_SUB_DIR; + } + let pos_of_first_wildcard = first_wildcard_pos(&line); + if line.first() == Some(&b'*') && first_wildcard_pos(&line[1..]).is_none() { + mode |= Mode::ENDS_WITH; + } + Some((line, mode, pos_of_first_wildcard)) +} + +fn first_wildcard_pos(pat: &[u8]) -> Option { + pat.find_byteset(GLOB_CHARACTERS) +} + +pub(crate) const GLOB_CHARACTERS: &[u8] = br"*?[\"; + +/// We always copy just because that's ultimately needed anyway, not because we always have to. +fn truncate_non_escaped_trailing_spaces(buf: &[u8]) -> BString { + match buf.rfind_not_byteset(br"\ ") { + Some(pos) if pos + 1 == buf.len() => buf.into(), // does not end in (escaped) whitespace + None => buf.into(), + Some(start_of_non_space) => { + // This seems a bit strange but attempts to recreate the git implementation while + // actually removing the escape characters before spaces. We leave other backslashes + // for escapes to be handled by `glob/globset`. + let mut res: BString = buf[..start_of_non_space + 1].into(); + + let mut trailing_bytes = buf[start_of_non_space + 1..].iter(); + let mut bare_spaces = 0; + while let Some(b) = trailing_bytes.next() { + match b { + b' ' => { + bare_spaces += 1; + } + b'\\' => { + res.extend(std::iter::repeat(b' ').take(bare_spaces)); + bare_spaces = 0; + // Skip what follows, like git does, but keep spaces if possible. + if trailing_bytes.next() == Some(&b' ') { + res.push(b' '); + } + } + _ => unreachable!("BUG: this must be either backslash or space"), + } + } + res + } + } +} diff --git a/git-glob/src/pattern.rs b/git-glob/src/pattern.rs new file mode 100644 index 00000000000..5cc844ab328 --- /dev/null +++ b/git-glob/src/pattern.rs @@ -0,0 +1,173 @@ +use bitflags::bitflags; +use bstr::{BStr, BString, ByteSlice}; + +use crate::{pattern, wildmatch, Pattern}; + +bitflags! { + /// Information about a [`Pattern`]. + /// + /// Its main purpose is to accelerate pattern matching, or to negate the match result or to + /// keep special rules only applicable when matching paths. + /// + /// The mode is typically created when parsing the pattern by inspecting it and isn't typically handled by the user. + #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] + pub struct Mode: u32 { + /// The pattern does not contain a sub-directory and - it doesn't contain slashes after removing the trailing one. + const NO_SUB_DIR = 1 << 0; + /// A pattern that is '*literal', meaning that it ends with what's given here + const ENDS_WITH = 1 << 1; + /// The pattern must match a directory, and not a file. + const MUST_BE_DIR = 1 << 2; + /// The pattern matches, but should be negated. Note that this mode has to be checked and applied by the caller. + const NEGATIVE = 1 << 3; + /// The pattern starts with a slash and thus matches only from the beginning. + const ABSOLUTE = 1 << 4; + } +} + +/// Describes whether to match a path case sensitively or not. +/// +/// Used in [Pattern::matches_repo_relative_path()]. +#[derive(Debug, PartialOrd, PartialEq, Copy, Clone, Hash, Ord, Eq)] +pub enum Case { + /// The case affects the match + Sensitive, + /// Ignore the case of ascii characters. + Fold, +} + +impl Pattern { + /// Parse the given `text` as pattern, or return `None` if `text` was empty. + pub fn from_bytes(text: &[u8]) -> Option { + crate::parse::pattern(text).map(|(text, mode, first_wildcard_pos)| Pattern { + text, + mode, + first_wildcard_pos, + base_path: None, + }) + } + + /// Return true if a match is negated. + pub fn is_negative(&self) -> bool { + self.mode.contains(Mode::NEGATIVE) + } + + /// Set the base path of the pattern. + /// Must be a slash-separated relative path with a trailing slash. + /// + /// Use this upon creation of the pattern when the source file is known. + pub fn with_base(mut self, path: impl Into) -> Self { + let path = path.into(); + debug_assert!(path.ends_with(b"/"), "base must end with a trailing slash"); + debug_assert!(!path.starts_with(b"/"), "base must be relative"); + self.base_path = Some(path); + self + } + + /// Match the given `path` which takes slashes (and only slashes) literally, and is relative to the repository root. + /// Note that `path` is assumed to be relative to the repository, and that our [`base_path`][Self::base_path] + /// is assumed to contain `path`. + /// + /// We may take various shortcuts which is when `basename_start_pos` and `is_dir` come into play. + /// `basename_start_pos` is the index at which the `path`'s basename starts. + /// + /// Lastly, `case` folding can be configured as well. + /// + /// Note that this method uses shortcuts to accelerate simple patterns. + pub fn matches_repo_relative_path<'a>( + &self, + path: impl Into<&'a BStr>, + basename_start_pos: Option, + is_dir: bool, + case: Case, + ) -> bool { + if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) { + return false; + } + + let flags = wildmatch::Mode::NO_MATCH_SLASH_LITERAL + | match case { + Case::Fold => wildmatch::Mode::IGNORE_CASE, + Case::Sensitive => wildmatch::Mode::empty(), + }; + let path = path.into(); + debug_assert_eq!( + basename_start_pos, + path.rfind_byte(b'/').map(|p| p + 1), + "BUG: invalid cached basename_start_pos provided" + ); + debug_assert!( + self.base_path + .as_ref() + .map(|base| path.starts_with(base)) + .unwrap_or(true), + "repo-relative paths must be pre-filtered to match our base." + ); + + if self.mode.contains(pattern::Mode::NO_SUB_DIR) { + let basename = if self.mode.contains(pattern::Mode::ABSOLUTE) { + self.base_path + .as_ref() + .and_then(|base| path.strip_prefix(base.as_slice()).map(|b| b.as_bstr())) + .unwrap_or(path) + } else { + &path[basename_start_pos.unwrap_or_default()..] + }; + self.matches(basename, flags) + } else { + let path = match self.base_path.as_ref() { + Some(base) => match path.strip_prefix(base.as_slice()) { + Some(path) => path.as_bstr(), + None => return false, + }, + None => path, + }; + self.matches(path, flags) + } + } + + /// See if `value` matches this pattern in the given `mode`. + /// + /// `mode` can identify `value` as path which won't match the slash character, and can match + /// strings with cases ignored as well. Note that the case folding performed here is ASCII only. + /// + /// Note that this method uses some shortcuts to accelerate simple patterns. + pub fn matches<'a>(&self, value: impl Into<&'a BStr>, mode: wildmatch::Mode) -> bool { + let value = value.into(); + match self.first_wildcard_pos { + // "*literal" case, overrides starts-with + Some(pos) if self.mode.contains(pattern::Mode::ENDS_WITH) && !value.contains(&b'/') => { + let text = &self.text[pos + 1..]; + if mode.contains(wildmatch::Mode::IGNORE_CASE) { + value + .len() + .checked_sub(text.len()) + .map(|start| text.eq_ignore_ascii_case(&value[start..])) + .unwrap_or(false) + } else { + value.ends_with(text.as_ref()) + } + } + Some(pos) => { + if mode.contains(wildmatch::Mode::IGNORE_CASE) { + if !value + .get(..pos) + .map_or(false, |value| value.eq_ignore_ascii_case(&self.text[..pos])) + { + return false; + } + } else if !value.starts_with(&self.text[..pos]) { + return false; + } + crate::wildmatch(self.text.as_bstr(), value, mode) + } + None => { + if mode.contains(wildmatch::Mode::IGNORE_CASE) { + self.text.eq_ignore_ascii_case(value) + } else { + self.text == value + } + } + } + } +} diff --git a/git-glob/src/wildmatch.rs b/git-glob/src/wildmatch.rs new file mode 100644 index 00000000000..23a7ce3f536 --- /dev/null +++ b/git-glob/src/wildmatch.rs @@ -0,0 +1,347 @@ +use bitflags::bitflags; +bitflags! { + /// The match mode employed in [`Pattern::matches()`][crate::Pattern::matches()]. + #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] + pub struct Mode: u8 { + /// Let globs like `*` and `?` not match the slash `/` literal, which is useful when matching paths. + const NO_MATCH_SLASH_LITERAL = 1 << 0; + /// Match case insensitively for ascii characters only. + const IGNORE_CASE = 1 << 1; + } +} + +pub(crate) mod function { + use bstr::{BStr, ByteSlice}; + + use crate::wildmatch::Mode; + + #[derive(Eq, PartialEq)] + enum Result { + Match, + NoMatch, + AbortAll, + AbortToStarStar, + } + + const STAR: u8 = b'*'; + const BACKSLASH: u8 = b'\\'; + const SLASH: u8 = b'/'; + const BRACKET_OPEN: u8 = b'['; + const BRACKET_CLOSE: u8 = b']'; + const COLON: u8 = b':'; + + const NEGATE_CLASS: u8 = b'!'; + + fn match_recursive(pattern: &BStr, text: &BStr, mode: Mode) -> Result { + use self::Result::*; + let possibly_lowercase = |c: &u8| { + if mode.contains(Mode::IGNORE_CASE) { + c.to_ascii_lowercase() + } else { + *c + } + }; + let mut p = pattern.iter().map(possibly_lowercase).enumerate().peekable(); + let mut t = text.iter().map(possibly_lowercase).enumerate(); + + while let Some((mut p_idx, mut p_ch)) = p.next() { + let (mut t_idx, mut t_ch) = match t.next() { + Some(c) => c, + None if p_ch != STAR => return AbortAll, + None => (text.len(), 0), + }; + + if p_ch == BACKSLASH { + match p.next() { + Some((_p_idx, p_ch)) => { + if p_ch != t_ch { + return NoMatch; + } else { + continue; + } + } + None => return NoMatch, + }; + } + match p_ch { + b'?' => { + if mode.contains(Mode::NO_MATCH_SLASH_LITERAL) && t_ch == SLASH { + return NoMatch; + } else { + continue; + } + } + STAR => { + let mut match_slash = mode + .contains(Mode::NO_MATCH_SLASH_LITERAL) + .then(|| false) + .unwrap_or(true); + match p.next() { + Some((next_p_idx, next_p_ch)) => { + let next; + if next_p_ch == STAR { + let leading_slash_idx = p_idx.checked_sub(1); + while p.next_if(|(_, c)| *c == STAR).is_some() {} + next = p.next(); + if !mode.contains(Mode::NO_MATCH_SLASH_LITERAL) { + match_slash = true; + } else if leading_slash_idx.map_or(true, |idx| pattern[idx] == SLASH) + && next.map_or(true, |(_, c)| { + c == SLASH || (c == BACKSLASH && p.peek().map(|t| t.1) == Some(SLASH)) + }) + { + if next.map_or(NoMatch, |(idx, _)| { + match_recursive(pattern[idx + 1..].as_bstr(), text[t_idx..].as_bstr(), mode) + }) == Match + { + return Match; + } + match_slash = true; + } else { + match_slash = false; + } + } else { + next = Some((next_p_idx, next_p_ch)); + } + + match next { + None => { + return if !match_slash && text[t_idx..].contains(&SLASH) { + NoMatch + } else { + Match + }; + } + Some((next_p_idx, next_p_ch)) => { + (p_idx, p_ch) = (next_p_idx, next_p_ch); + if !match_slash && p_ch == SLASH { + match text[t_idx..].find_byte(SLASH) { + Some(distance_to_slash) => { + for _ in t.by_ref().take(distance_to_slash) {} + continue; + } + None => return NoMatch, + } + } + } + } + } + None => { + return if !match_slash && text[t_idx..].contains(&SLASH) { + NoMatch + } else { + Match + } + } + } + + return loop { + if !crate::parse::GLOB_CHARACTERS.contains(&p_ch) { + loop { + if (!match_slash && t_ch == SLASH) || t_ch == p_ch { + break; + } + (t_idx, t_ch) = match t.next() { + Some(t) => (t.0, t.1), + None => break, + }; + } + if t_ch != p_ch { + return NoMatch; + } + } + let res = match_recursive(pattern[p_idx..].as_bstr(), text[t_idx..].as_bstr(), mode); + if res != NoMatch { + if !match_slash || res != AbortToStarStar { + return res; + } + } else if !match_slash && t_ch == SLASH { + return AbortToStarStar; + } + (t_idx, t_ch) = match t.next() { + Some(t) => (t.0, t.1), + None => break AbortAll, + }; + }; + } + BRACKET_OPEN => { + (p_idx, p_ch) = match p.next() { + Some(t) => t, + None => return AbortAll, + }; + + if p_ch == b'^' { + p_ch = NEGATE_CLASS; + } + let negated = p_ch == NEGATE_CLASS; + let mut next = if negated { p.next() } else { Some((p_idx, p_ch)) }; + let mut prev_p_ch = 0; + let mut matched = false; + loop { + match next { + None => return AbortAll, + Some((p_idx, mut p_ch)) => match p_ch { + BACKSLASH => match p.next() { + Some((_, p_ch)) => { + if p_ch == t_ch { + matched = true + } else { + prev_p_ch = p_ch; + } + } + None => return AbortAll, + }, + b'-' if prev_p_ch != 0 + && p.peek().is_some() + && p.peek().map(|t| t.1) != Some(BRACKET_CLOSE) => + { + p_ch = p.next().expect("peeked").1; + if p_ch == BACKSLASH { + p_ch = match p.next() { + Some(t) => t.1, + None => return AbortAll, + }; + } + if t_ch <= p_ch && t_ch >= prev_p_ch { + matched = true; + } else if mode.contains(Mode::IGNORE_CASE) && t_ch.is_ascii_lowercase() { + let t_ch_upper = t_ch.to_ascii_uppercase(); + if (t_ch_upper <= p_ch.to_ascii_uppercase() + && t_ch_upper >= prev_p_ch.to_ascii_uppercase()) + || (t_ch_upper <= prev_p_ch.to_ascii_uppercase() + && t_ch_upper >= p_ch.to_ascii_uppercase()) + { + matched = true; + } + } + prev_p_ch = 0; + } + BRACKET_OPEN if matches!(p.peek(), Some((_, COLON))) => { + p.next(); + while p.peek().map_or(false, |t| t.1 != BRACKET_CLOSE) { + p.next(); + } + let closing_bracket_idx = match p.next() { + Some((idx, _)) => idx, + None => return AbortAll, + }; + const BRACKET__COLON__BRACKET: usize = 3; + if closing_bracket_idx - p_idx < BRACKET__COLON__BRACKET + || pattern[closing_bracket_idx - 1] != COLON + { + if t_ch == BRACKET_OPEN { + matched = true + } + p = pattern[p_idx + 1..] + .iter() + .map(possibly_lowercase) + .enumerate() + .peekable(); + } else { + let class = &pattern.as_ref()[p_idx + 2..closing_bracket_idx - 1]; + match class { + b"alnum" => { + if t_ch.is_ascii_alphanumeric() { + matched = true; + } + } + b"alpha" => { + if t_ch.is_ascii_alphabetic() { + matched = true; + } + } + b"blank" => { + if t_ch.is_ascii_whitespace() { + matched = true; + } + } + b"cntrl" => { + if t_ch.is_ascii_control() { + matched = true; + } + } + b"digit" => { + if t_ch.is_ascii_digit() { + matched = true; + } + } + + b"graph" => { + if t_ch.is_ascii_graphic() { + matched = true; + } + } + b"lower" => { + if t_ch.is_ascii_lowercase() { + matched = true; + } + } + b"print" => { + if (0x20u8..=0x7e).contains(&t_ch) { + matched = true; + } + } + b"punct" => { + if t_ch.is_ascii_punctuation() { + matched = true; + } + } + b"space" => { + if t_ch == b' ' { + matched = true; + } + } + b"upper" => { + if t_ch.is_ascii_uppercase() + || mode.contains(Mode::IGNORE_CASE) && t_ch.is_ascii_lowercase() + { + matched = true; + } + } + b"xdigit" => { + if t_ch.is_ascii_hexdigit() { + matched = true; + } + } + _ => return AbortAll, + }; + prev_p_ch = 0; + } + } + _ => { + prev_p_ch = p_ch; + if p_ch == t_ch { + matched = true; + } + } + }, + }; + next = p.next(); + if let Some((_, BRACKET_CLOSE)) = next { + break; + } + } + if matched == negated || mode.contains(Mode::NO_MATCH_SLASH_LITERAL) && t_ch == SLASH { + return NoMatch; + } + continue; + } + non_glob_ch => { + if non_glob_ch != t_ch { + return NoMatch; + } else { + continue; + } + } + } + } + t.next().map(|_| NoMatch).unwrap_or(Match) + } + + /// Employ pattern matching to see if `value` matches `pattern`. + /// + /// `mode` can be used to adjust the way the matching is performed. + pub fn wildmatch(pattern: &BStr, value: &BStr, mode: Mode) -> bool { + match_recursive(pattern, value, mode) == Result::Match + } +} diff --git a/git-glob/tests/fixtures/make_baseline.sh b/git-glob/tests/fixtures/make_baseline.sh new file mode 100644 index 00000000000..e0f37d106c6 --- /dev/null +++ b/git-glob/tests/fixtures/make_baseline.sh @@ -0,0 +1,157 @@ +#!/bin/bash +set -eu -o pipefail + + +git init -q +git config commit.gpgsign false +git config core.autocrlf false +git config core.ignorecase false + +while read -r pattern value; do + echo "$pattern" "$value" + echo "$pattern" > .gitignore + echo "$value" | git check-ignore -vn --stdin 2>&1 || : +done <git-baseline.nmatch +*/\ XXX/\ +*/\\ XXX/\ +/*foo bar/foo +/*foo bar/bazfoo +foo*bar foo/baz/bar +/*foo.txt hello/foo.txt +bar/foo baz/bar/foo +*hello.txt hello.txt-and-then-some +*hello.txt goodbye.txt +*some/path/to/hello.txt some/path/to/hello.txt-and-then-some +*some/path/to/hello.txt some/other/path/to/hello.txt +*some/path/to/hello.txt a/bigger/some/path/to/hello.txt +abc?def abc/def +a*b*c abcd +abc*abc*abc abcabcabcabcabcabcabca +a[0-9]b a_b +a[!0-9]b a0b +a[!0-9]b a9b +[!-] - +a[^0-9]b a0b +a[^0-9]b a9b +[^-] - +{a,b} a +{a,b} b +{[}],foo} } +{foo} foo +{*.foo,*.bar,*.wat} test.foo +{*.foo,*.bar,*.wat} test.bar +{*.foo,*.bar,*.wat} test.wat +abc*def abc/def +aBcDeFg abcdefg +aBcDeFg ABCDEFG +aBcDeFg AbCdEfG +some/**/needle.txt some/other/notthis.txt +some/**/**/needle.txt some/other/notthis.txt +/**/test one/notthis +/**/test notthis +**/.* ab.c +**/.* abc/ab.c +.*/** a.bc +.*/** abc/a.bc +./foo foo +**/foo foofoo +**/foo/bar foofoo/bar +/*.c mozilla-sha1/sha1.c +**/m4/ltoptions.m4 csharp/src/packages/repositories.config +some/*/needle.txt some/needle.txt +some/*/needle.txt some/one/two/needle.txt +some/*/needle.txt some/one/two/three/needle.txt +.*/** .abc +foo/** foo +{**/src/**,foo} abc/src/bar +{**/src/**,foo} foo +abc[/]def abc/def +EOF + +while read -r pattern value; do + echo "$pattern" "$value" + echo "$pattern" > .gitignore + echo "$value" | git check-ignore -vn --stdin 2>&1 || : +done <git-baseline.match +\a a +\\\[a-z] \a +\\\? \a +\\\* \\ +/*foo.txt barfoo.txt +*foo.txt bar/foo.txt +*.c mozilla-sha1/sha1.c +*.rs .rs +*hello.txt hello.txt +*hello.txt gareth_says_hello.txt +*hello.txt some/path/to/hello.txt +/*foo.txt foo.txt +*hello.txt some\path\to\hello.txt +*hello.txt an/absolute/path/to/hello.txt +*some/path/to/hello.txt some/path/to/hello.txt +a foo/a +a a +a*b a_b +a*b*c abc +a*b*c a_b_c +a*b*c a___b___c +abc*abc*abc abcabcabcabcabcabcabc +a*a*a*a*a*a*a*a*a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +a*b[xyz]c*d abxcdbxcddd +☃ ☃ +** abcde +** .asdf +** x/.asdf +a[0-9]b a0b +a[0-9]b a9b +a[!0-9]b a_b +[a-z123] 1 +[1a-z23] 1 +[123a-z] 1 +[abc-] - +[-abc] - +[-a-c] b +[a-c-] b +[-] - +a[^0-9]b a_b +_[[]_[]]_[?]_[*]_!_ _[_]_?_*_!_ +a,b a,b +\[ [ +\? ? +\* * +aBcDeFg aBcDeFg +some/**/needle.txt some/needle.txt +some/**/needle.txt some/one/needle.txt +some/**/needle.txt some/one/two/needle.txt +some/**/needle.txt some/other/needle.txt +some/**/**/needle.txt some/needle.txt +some/**/**/needle.txt some/one/needle.txt +some/**/**/needle.txt some/one/two/needle.txt +some/**/**/needle.txt some/other/needle.txt +**/test one/two/test +**/test one/test +**/test test +/**/test one/two/test +/**/test one/test +/**/test test +**/.* .abc +**/.* abc/.abc +**/foo/bar foo/bar +.*/** .abc/abc +test/** test/ +test/** test/one +test/** test/one/two +some/*/needle.txt some/one/needle.txt +abc/def abc/def +EOF + +git config core.ignorecase true +while read -r pattern value; do + echo "$pattern" "$value" + echo "$pattern" > .gitignore + echo "$value" | git check-ignore -vn --stdin 2>&1 || : +done <git-baseline.match-icase +aBcDeFg aBcDeFg +aBcDeFg abcdefg +aBcDeFg ABCDEFG +aBcDeFg AbCdEfG +EOF diff --git a/git-glob/tests/glob.rs b/git-glob/tests/glob.rs new file mode 100644 index 00000000000..c7452ed5f32 --- /dev/null +++ b/git-glob/tests/glob.rs @@ -0,0 +1,3 @@ +mod matching; +mod parse; +mod wildmatch; diff --git a/git-glob/tests/matching/mod.rs b/git-glob/tests/matching/mod.rs new file mode 100644 index 00000000000..a4d9f868502 --- /dev/null +++ b/git-glob/tests/matching/mod.rs @@ -0,0 +1,304 @@ +use std::collections::BTreeSet; + +use bstr::{BStr, ByteSlice}; +use git_glob::{pattern, pattern::Case}; + +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] +pub struct GitMatch<'a> { + pattern: &'a BStr, + value: &'a BStr, + /// True if git could match `value` with `pattern` + is_match: bool, +} + +pub struct Baseline<'a> { + inner: bstr::Lines<'a>, +} + +impl<'a> Iterator for Baseline<'a> { + type Item = GitMatch<'a>; + + fn next(&mut self) -> Option { + let mut tokens = self.inner.next()?.splitn(2, |b| *b == b' '); + let pattern = tokens.next().expect("pattern").as_bstr(); + let value = tokens.next().expect("value").as_bstr().trim_start().as_bstr(); + + let git_match = self.inner.next()?; + let is_match = !git_match.starts_with(b"::\t"); + Some(GitMatch { + pattern, + value, + is_match, + }) + } +} + +impl<'a> Baseline<'a> { + fn new(input: &'a [u8]) -> Self { + Baseline { + inner: input.as_bstr().lines(), + } + } +} + +#[test] +fn compare_baseline_with_ours() { + let dir = git_testtools::scripted_fixture_repo_read_only("make_baseline.sh").unwrap(); + let (mut total_matches, mut total_correct, mut panics) = (0, 0, 0); + let mut mismatches = Vec::new(); + for (input_file, expected_matches, case) in &[ + ("git-baseline.match", true, pattern::Case::Sensitive), + ("git-baseline.nmatch", false, pattern::Case::Sensitive), + ("git-baseline.match-icase", true, pattern::Case::Fold), + ] { + let input = std::fs::read(dir.join(*input_file)).unwrap(); + let mut seen = BTreeSet::default(); + + for m @ GitMatch { + pattern, + value, + is_match, + } in Baseline::new(&input) + { + total_matches += 1; + assert!(seen.insert(m), "duplicate match entry: {:?}", m); + assert_eq!( + is_match, *expected_matches, + "baseline for matches must be {} - check baseline and git version: {:?}", + expected_matches, m + ); + match std::panic::catch_unwind(|| { + let pattern = pat(pattern); + pattern.matches_repo_relative_path( + value, + basename_start_pos(value), + false, // TODO: does it make sense to pretend it is a dir and see what happens? + *case, + ) + }) { + Ok(actual_match) => { + if actual_match == is_match { + total_correct += 1; + } else { + mismatches.push((pattern.to_owned(), value.to_owned(), is_match, expected_matches)); + } + } + Err(_) => { + panics += 1; + continue; + } + }; + } + } + + dbg!(mismatches); + assert_eq!( + total_correct, + total_matches - panics, + "We perfectly agree with git here" + ); + assert_eq!(panics, 0); +} + +#[test] +fn non_dirs_for_must_be_dir_patterns_are_ignored() { + let pattern = pat("hello/"); + + assert!(pattern.mode.contains(pattern::Mode::MUST_BE_DIR)); + assert_eq!( + pattern.text, "hello", + "a dir pattern doesn't actually end with the trailing slash" + ); + let path = "hello"; + assert!( + !pattern.matches_repo_relative_path(path, None, false /* is-dir */, Case::Sensitive), + "non-dirs never match a dir pattern" + ); + assert!( + pattern.matches_repo_relative_path(path, None, true /* is-dir */, Case::Sensitive), + "dirs can match a dir pattern with the normal rules" + ); +} + +#[test] +fn basename_matches_from_end() { + let pat = &pat("foo"); + assert!(match_file(pat, "FoO", Case::Fold)); + assert!(!match_file(pat, "FoOo", Case::Fold)); + assert!(!match_file(pat, "Foo", Case::Sensitive)); + assert!(match_file(pat, "foo", Case::Sensitive)); + assert!(!match_file(pat, "Foo", Case::Sensitive)); + assert!(!match_file(pat, "barfoo", Case::Sensitive)); +} + +#[test] +#[should_panic] +fn base_path_must_match_or_panic_occours_in_debug_mode() { + let pat = pat("foo").with_base("base/"); + match_file(&pat, "other/FoO", Case::Fold); +} + +#[test] +fn absolute_basename_matches_only_from_beginning() { + let mut pattern = pat("/foo"); + let pat = &pattern; + assert!(match_file(pat, "FoO", Case::Fold)); + assert!(!match_file(pat, "bar/Foo", Case::Fold)); + assert!(match_file(pat, "foo", Case::Sensitive)); + assert!(!match_file(pat, "Foo", Case::Sensitive)); + assert!(!match_file(pat, "bar/foo", Case::Sensitive)); + + pattern = pattern.with_base("base/"); + let pat = &pattern; + assert!(match_file(pat, "base/FoO", Case::Fold)); + assert!(!match_file(pat, "base/bar/Foo", Case::Fold)); + assert!(match_file(pat, "base/foo", Case::Sensitive)); + assert!(!match_file(pat, "base/Foo", Case::Sensitive)); + assert!(!match_file(pat, "base/bar/foo", Case::Sensitive)); +} + +#[test] +fn absolute_path_matches_only_from_beginning() { + let mut pattern = pat("/bar/foo"); + let pat = &pattern; + assert!(!match_file(pat, "FoO", Case::Fold)); + assert!(match_file(pat, "bar/Foo", Case::Fold)); + assert!(!match_file(pat, "foo", Case::Sensitive)); + assert!(match_file(pat, "bar/foo", Case::Sensitive)); + assert!(!match_file(pat, "bar/Foo", Case::Sensitive)); + + pattern = pattern.with_base("base/"); + let pat = &pattern; + assert!(!match_file(pat, "base/FoO", Case::Fold)); + assert!(match_file(pat, "base/bar/Foo", Case::Fold)); + assert!(!match_file(pat, "base/foo", Case::Sensitive)); + assert!(match_file(pat, "base/bar/foo", Case::Sensitive)); + assert!(!match_file(pat, "base/bar/Foo", Case::Sensitive)); +} + +#[test] +fn absolute_path_with_recursive_glob_detects_mismatches_quickly() { + let mut pattern = pat("/bar/foo/**"); + let pat = &pattern; + assert!(!match_file(pat, "FoO", Case::Fold)); + assert!(!match_file(pat, "bar/Fooo", Case::Fold)); + assert!(!match_file(pat, "baz/bar/Foo", Case::Fold)); + + pattern = pattern.with_base("base/"); + let pat = &pattern; + assert!(!match_file(pat, "base/FoO", Case::Fold)); + assert!(!match_file(pat, "base/bar/Fooo", Case::Fold)); + assert!(!match_file(pat, "base/baz/bar/foo", Case::Sensitive)); +} + +#[test] +fn absolute_path_with_recursive_glob_can_do_case_insensitive_prefix_search() { + let mut pattern = pat("/bar/foo/**"); + let pat = &pattern; + assert!(!match_file(pat, "bar/Foo/match", Case::Sensitive)); + assert!(match_file(pat, "bar/Foo/match", Case::Fold)); + + pattern = pattern.with_base("base/"); + let pat = &pattern; + assert!(!match_file(pat, "base/bar/Foo/match", Case::Sensitive)); + assert!(match_file(pat, "base/bar/Foo/match", Case::Fold)); +} + +#[test] +fn relative_path_does_not_match_from_end() { + let pattern = &pat("bar/foo"); + assert!(!match_file(pattern, "FoO", Case::Fold)); + assert!(match_file(pattern, "bar/Foo", Case::Fold)); + assert!(!match_file(pattern, "baz/bar/Foo", Case::Fold)); + assert!(!match_file(pattern, "foo", Case::Sensitive)); + assert!(match_file(pattern, "bar/foo", Case::Sensitive)); + assert!(!match_file(pattern, "baz/bar/foo", Case::Sensitive)); + assert!(!match_file(pattern, "Baz/bar/Foo", Case::Sensitive)); +} + +#[test] +fn basename_glob_and_literal_is_ends_with() { + let pattern = &pat("*foo"); + assert!(match_file(pattern, "FoO", Case::Fold)); + assert!(match_file(pattern, "BarFoO", Case::Fold)); + assert!(!match_file(pattern, "BarFoOo", Case::Fold)); + assert!(!match_file(pattern, "Foo", Case::Sensitive)); + assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); + assert!(match_file(pattern, "barfoo", Case::Sensitive)); + assert!(!match_file(pattern, "barfooo", Case::Sensitive)); + + assert!(match_file(pattern, "bar/foo", Case::Sensitive)); + assert!(match_file(pattern, "bar/bazfoo", Case::Sensitive)); +} + +#[test] +fn special_cases_from_corpus() { + let pattern = &pat("foo*bar"); + assert!( + !match_file(pattern, "foo/baz/bar", Case::Sensitive), + "asterisk does not match path separators" + ); + let pattern = &pat("*some/path/to/hello.txt"); + assert!( + !match_file(pattern, "a/bigger/some/path/to/hello.txt", Case::Sensitive), + "asterisk doesn't match path separators" + ); + + let pattern = &pat("/*foo.txt"); + assert!(match_file(pattern, "hello-foo.txt", Case::Sensitive)); + assert!( + !match_file(pattern, "hello/foo.txt", Case::Sensitive), + "absolute single asterisk doesn't match paths" + ); +} + +#[test] +fn absolute_basename_glob_and_literal_is_ends_with_in_basenames() { + let pattern = &pat("/*foo"); + + assert!(match_file(pattern, "FoO", Case::Fold)); + assert!(match_file(pattern, "BarFoO", Case::Fold)); + assert!(!match_file(pattern, "BarFoOo", Case::Fold)); + assert!(!match_file(pattern, "Foo", Case::Sensitive)); + assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); + assert!(match_file(pattern, "barfoo", Case::Sensitive)); + assert!(!match_file(pattern, "barfooo", Case::Sensitive)); +} + +#[test] +fn absolute_basename_glob_and_literal_is_glob_in_paths() { + let pattern = &pat("/*foo"); + + assert!(!match_file(pattern, "bar/foo", Case::Sensitive), "* does not match /"); + assert!(!match_file(pattern, "bar/bazfoo", Case::Sensitive)); +} + +#[test] +fn negated_patterns_are_handled_by_caller() { + let pattern = &pat("!foo"); + assert!( + match_file(pattern, "foo", Case::Sensitive), + "negative patterns match like any other" + ); + assert!( + pattern.is_negative(), + "the caller checks for the negative flag and acts accordingly" + ); +} + +fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern { + git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works") +} + +fn match_file<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, case: Case) -> bool { + match_path(pattern, path, false, case) +} + +fn match_path<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, is_dir: bool, case: Case) -> bool { + let path = path.into(); + pattern.matches_repo_relative_path(path, basename_start_pos(path), is_dir, case) +} + +fn basename_start_pos(value: &BStr) -> Option { + value.rfind_byte(b'/').map(|pos| pos + 1) +} diff --git a/git-glob/tests/parse/mod.rs b/git-glob/tests/parse/mod.rs new file mode 100644 index 00000000000..15b5abaa419 --- /dev/null +++ b/git-glob/tests/parse/mod.rs @@ -0,0 +1,157 @@ +use git_glob::pattern::Mode; +use git_glob::Pattern; + +#[test] +fn mark_ends_with_pattern_specifically() { + assert_eq!( + git_glob::parse(br"*literal"), + pat(r"*literal", Mode::NO_SUB_DIR | Mode::ENDS_WITH, Some(0)) + ); + assert_eq!( + git_glob::parse(br"**literal"), + pat(r"**literal", Mode::NO_SUB_DIR, Some(0)), + "double-asterisk won't allow for fast comparisons" + ); + assert_eq!( + git_glob::parse(br"*litera[l]"), + pat(r"*litera[l]", Mode::NO_SUB_DIR, Some(0)) + ); + assert_eq!( + git_glob::parse(br"*litera?"), + pat(r"*litera?", Mode::NO_SUB_DIR, Some(0)) + ); + assert_eq!( + git_glob::parse(br"*litera\?"), + pat(r"*litera\?", Mode::NO_SUB_DIR, Some(0)), + "for now we don't handle escapes properly like git seems to do" + ); +} + +fn pat(pattern: &str, mode: Mode, first_glob_char_pos: Option) -> Option { + Some(Pattern { + text: pattern.into(), + mode, + first_wildcard_pos: first_glob_char_pos, + base_path: None, + }) +} + +#[test] +fn whitespace_only_is_ignored() { + assert!(git_glob::parse(b"\n\r\n\t\t \n").is_none()); +} + +#[test] +fn hash_symbols_are_not_special() { + assert_eq!( + git_glob::parse(b"# hello world"), + pat("# hello world", Mode::NO_SUB_DIR, None) + ); +} + +#[test] +fn backslashes_before_hashes_are_considered_an_escape_sequence() { + assert_eq!(git_glob::parse(br"\#hello"), pat(r"#hello", Mode::NO_SUB_DIR, None)); +} + +#[test] +fn backslashes_are_part_of_the_pattern_if_not_in_specific_positions() { + assert_eq!( + git_glob::parse(br"\hello\world"), + pat(r"\hello\world", Mode::NO_SUB_DIR, Some(0)) + ); +} + +#[test] +fn leading_exclamation_mark_negates_pattern() { + assert_eq!( + git_glob::parse(b"!hello"), + pat("hello", Mode::NEGATIVE | Mode::NO_SUB_DIR, None) + ); +} + +#[test] +fn leading_exclamation_marks_can_be_escaped_with_backslash() { + assert_eq!(git_glob::parse(br"\!hello"), pat("!hello", Mode::NO_SUB_DIR, None)); +} + +#[test] +fn leading_slashes_mark_patterns_as_absolute() { + assert_eq!( + git_glob::parse(br"/absolute"), + pat("absolute", Mode::NO_SUB_DIR | Mode::ABSOLUTE, None) + ); + + assert_eq!( + git_glob::parse(br"/absolute/path"), + pat("absolute/path", Mode::ABSOLUTE, None) + ); +} + +#[test] +fn absence_of_sub_directories_are_marked() { + assert_eq!(git_glob::parse(br"a/b"), pat("a/b", Mode::empty(), None)); + assert_eq!(git_glob::parse(br"ab"), pat("ab", Mode::NO_SUB_DIR, None)); +} + +#[test] +fn trailing_slashes_are_marked_and_removed() { + assert_eq!( + git_glob::parse(b"dir/"), + pat("dir", Mode::MUST_BE_DIR | Mode::NO_SUB_DIR, None) + ); + assert_eq!( + git_glob::parse(b"dir///"), + pat("dir//", Mode::MUST_BE_DIR, None), + "but only the last slash is removed" + ); +} + +#[test] +fn trailing_spaces_are_ignored() { + assert_eq!(git_glob::parse(br"a "), pat("a", Mode::NO_SUB_DIR, None)); + assert_eq!( + git_glob::parse(b"a\t\t "), + pat("a\t\t", Mode::NO_SUB_DIR, None), + "trailing tabs are not ignored" + ); +} + +#[test] +fn trailing_spaces_can_be_escaped_to_be_literal() { + assert_eq!( + git_glob::parse(br"a \ "), + pat("a ", Mode::NO_SUB_DIR, None), + "a single escape in front of the last desired space is enough" + ); + assert_eq!( + git_glob::parse(br"a b c "), + pat("a b c", Mode::NO_SUB_DIR, None), + "spaces in the middle are fine" + ); + assert_eq!( + git_glob::parse(br"a\ \ \ "), + pat("a ", Mode::NO_SUB_DIR, None), + "one can also escape every single one" + ); + assert_eq!( + git_glob::parse(br"a \ "), + pat("a ", Mode::NO_SUB_DIR, None), + "or just the one in the middle, losing the last actual space" + ); + assert_eq!( + git_glob::parse(br"a \"), + pat("a ", Mode::NO_SUB_DIR, None), + "escaping nothing also works as a whitespace protection" + ); + assert_eq!( + git_glob::parse(br"a \\\ "), + pat(r"a ", Mode::NO_SUB_DIR, None), + "strange things like these work too" + ); + assert_eq!( + git_glob::parse(br"a \\ "), + pat(r"a ", Mode::NO_SUB_DIR, None), + "strange things like these work as well" + ); +} diff --git a/git-glob/tests/wildmatch/mod.rs b/git-glob/tests/wildmatch/mod.rs new file mode 100644 index 00000000000..433bf902998 --- /dev/null +++ b/git-glob/tests/wildmatch/mod.rs @@ -0,0 +1,368 @@ +use git_glob::pattern::Case; +use git_glob::{wildmatch, Pattern}; +use std::fmt::{Debug, Display, Formatter}; +use std::panic::catch_unwind; + +#[test] +fn corpus() { + // based on git/t/t3070.sh + let tests = [ + (1u8,1u8,1u8,1u8, "foo", "foo"), + (0,0,0,0, "foo", "bar"), + (1,1,1,1, "foo", "???"), + (0,0,0,0, "foo", "??"), + (1,1,1,1, "foo", "*"), + (1,1,1,1, "foo", "f*"), + (0,0,0,0, "foo", "*f"), + (1,1,1,1, "foo", "*foo*"), + (1,1,1,1, "foobar", "*ob*a*r*"), + (1,1,1,1, "aaaaaaabababab", "*ab"), + (1,1,1,1, "foo*", r"foo\*"), + (0,0,0,0, "foobar", r"foo\*bar"), + (1,1,1,1, r"f\oo", r"f\\oo"), + (1,1,1,1, "ball", "*[al]?"), + (0,0,0,0, "ten", "[ten]"), + (1,1,1,1, "ten", "**[!te]"), + (0,0,0,0, "ten", "**[!ten]"), + (1,1,1,1, "ten", "t[a-g]n"), + (0,0,0,0, "ten", "t[!a-g]n"), + (1,1,1,1, "ton", "t[!a-g]n"), + (1,1,1,1, "ton", "t[^a-g]n"), + (1,1,1,1, "a]b", "a[]]b"), + (1,1,1,1, "a-b", "a[]-]b"), + (1,1,1,1, "a]b", "a[]-]b"), + (0,0,0,0, "aab", "a[]-]b"), + (1,1,1,1, "aab", "a[]a-]b"), + (1,1,1,1, "]", "]"), + // Extended slash-matching features + (0,0,1,1, "foo/baz/bar", "foo*bar"), + (0,0,1,1, "foo/baz/bar", "foo**bar"), + (1,1,1,1, "foobazbar", "foo**bar"), + (1,1,1,1, "foo/baz/bar", "foo/**/bar"), + (1,1,0,0, "foo/baz/bar", "foo/**/**/bar"), + (1,1,1,1, "foo/b/a/z/bar", "foo/**/bar"), + (1,1,1,1, "foo/b/a/z/bar", "foo/**/**/bar"), + (1,1,0,0, "foo/bar", "foo/**/bar"), + (1,1,0,0, "foo/bar", "foo/**/**/bar"), + (0,0,1,1, "foo/bar", "foo?bar"), + (0,0,1,1, "foo/bar", "foo[/]bar"), + (0,0,1,1, "foo/bar", "foo[^a-z]bar"), + (0,0,1,1, "foo/bar", "f[^eiu][^eiu][^eiu][^eiu][^eiu]r"), + (1,1,1,1, "foo-bar", "f[^eiu][^eiu][^eiu][^eiu][^eiu]r"), + (1,1,0,0, "foo", "**/foo"), + (1,1,1,1, "XXX/foo", "**/foo"), + (1,1,1,1, "bar/baz/foo", "**/foo"), + (0,0,1,1, "bar/baz/foo", "*/foo"), + (0,0,1,1, "foo/bar/baz", "**/bar*"), + (1,1,1,1, "deep/foo/bar/baz", "**/bar/*"), + (0,0,1,1, "deep/foo/bar/baz/", "**/bar/*"), + (1,1,1,1, "deep/foo/bar/baz/", "**/bar/**"), + (0,0,0,0, "deep/foo/bar", "**/bar/*"), + (1,1,1,1, "deep/foo/bar/", "**/bar/**"), + (0,0,1,1, "foo/bar/baz", "**/bar**"), + (1,1,1,1, "foo/bar/baz/x", "*/bar/**"), + (0,0,1,1, "deep/foo/bar/baz/x", "*/bar/**"), + (1,1,1,1, "deep/foo/bar/baz/x", "**/bar/*/*"), + + // Various additional tests + (0,0,0,0, "acrt", "a[c-c]st"), + (1,1,1,1, "acrt", "a[c-c]rt"), + (0,0,0,0, "]", "[!]-]"), + (1,1,1,1, "a", "[!]-]"), + (0,0,0,0, "", r"\"), + (0,0,1,1, r"XXX/\", r"*/\"), + (0,0,1,1, r"XXX/\", r"*/\\"), + (1,1,1,1, "foo", "foo"), + (1,1,1,1, "@foo", "@foo"), + (0,0,0,0, "foo", "@foo"), + (1,1,1,1, "[ab]", r"\[ab]"), + (1,1,1,1, "[ab]", "[[]ab]"), + (1,1,1,1, "[ab]", "[[:]ab]"), + (0,0,0,0, "[ab]", "[[::]ab]"), + (1,1,1,1, "[ab]", "[[:digit]ab]"), + (1,1,1,1, "[ab]", r"[\[:]ab]"), + (1,1,1,1, "?a?b", r"\??\?b"), + (1,1,1,1, "abc", r"\a\b\c"), + (1,1,1,1, "foo/bar/baz/to", "**/t[o]"), + + // Character class tests + (1,1,1,1, "a1B", "[[:alpha:]][[:digit:]][[:upper:]]"), + (0,1,0,1, "a", "[[:digit:][:upper:][:space:]]"), + (1,1,1,1, "A", "[[:digit:][:upper:][:space:]]"), + (1,1,1,1, "1", "[[:digit:][:upper:][:space:]]"), + (0,0,0,0, "1", "[[:digit:][:upper:][:spaci:]]"), + (1,1,1,1, " ", "[[:digit:][:upper:][:space:]]"), + (0,0,0,0, ".", "[[:digit:][:upper:][:space:]]"), + (1,1,1,1, ".", "[[:digit:][:punct:][:space:]]"), + (1,1,1,1, "5", "[[:xdigit:]]"), + (1,1,1,1, "f", "[[:xdigit:]]"), + (1,1,1,1, "D", "[[:xdigit:]]"), + (1,1,1,1, "_", "[[:alnum:][:alpha:][:blank:][:cntrl:][:digit:][:graph:][:lower:][:print:][:punct:][:space:][:upper:][:xdigit:]]"), + (1,1,1,1, ".", "[^[:alnum:][:alpha:][:blank:][:cntrl:][:digit:][:lower:][:space:][:upper:][:xdigit:]]"), + (1,1,1,1, "5", "[a-c[:digit:]x-z]"), + (1,1,1,1, "b", "[a-c[:digit:]x-z]"), + (1,1,1,1, "y", "[a-c[:digit:]x-z]"), + (0,0,0,0, "q", "[a-c[:digit:]x-z]"), + + // Additional tests, including some malformed wild(patterns + (1,1,1,1, "]", r"[\\-^]"), + (0,0,0,0, "[", r"[\\-^]"), + (1,1,1,1, "-", r"[\-_]"), + (1,1,1,1, "]", r"[\]]"), + (0,0,0,0, r"\]", r"[\]]"), + (0,0,0,0, r"\", r"[\]]"), + (0,0,0,0, "ab", "a[]b"), + (0,0,0,0, "ab", "[!"), + (0,0,0,0, "ab", "[-"), + (1,1,1,1, "-", "[-]"), + (0,0,0,0, "-", "[a-"), + (0,0,0,0, "-", "[!a-"), + (1,1,1,1, "-", "[--A]"), + (1,1,1,1, "5", "[--A]"), + (1,1,1,1, " ", "[ --]"), + (1,1,1,1, "$", "[ --]"), + (1,1,1,1, "-", "[ --]"), + (0,0,0,0, "0", "[ --]"), + (1,1,1,1, "-", "[---]"), + (1,1,1,1, "-", "[------]"), + (0,0,0,0, "j", "[a-e-n]"), + (1,1,1,1, "-", "[a-e-n]"), + (1,1,1,1, "a", "[!------]"), + (0,0,0,0, "[", "[]-a]"), + (1,1,1,1, "^", "[]-a]"), + (0,0,0,0, "^", "[!]-a]"), + (1,1,1,1, "[", "[!]-a]"), + (1,1,1,1, "^", "[a^bc]"), + (1,1,1,1, "-b]", "[a-]b]"), + (0,0,0,0, r"\", r"[\]"), + (1,1,1,1, r"\", r"[\\]"), + (0,0,0,0, r"\", r"[!\\]"), + (1,1,1,1, "G", r"[A-\\]"), + (0,0,0,0, "aaabbb", "b*a"), + (0,0,0,0, "aabcaa", "*ba*"), + (1,1,1,1, ",", "[,]"), + (1,1,1,1, ",", r"[\\,]"), + (1,1,1,1, r"\", r"[\\,]"), + (1,1,1,1, "-", "[,-.]"), + (0,0,0,0, "+", "[,-.]"), + (0,0,0,0, "-.]", "[,-.]"), + (1,1,1,1, "2", r"[\1-\3]"), + (1,1,1,1, "3", r"[\1-\3]"), + (0,0,0,0, "4", r"[\1-\3]"), + (1,1,1,1, r"\", r"[[-\]]"), + (1,1,1,1, "[", r"[[-\]]"), + (1,1,1,1, "]", r"[[-\]]"), + (0,0,0,0, "-", r"[[-\]]"), + + // Test recursion + (1,1,1,1, "-adobe-courier-bold-o-normal--12-120-75-75-m-70-iso8859-1", "-*-*-*-*-*-*-12-*-*-*-m-*-*-*"), + (0,0,0,0, "-adobe-courier-bold-o-normal--12-120-75-75-X-70-iso8859-1", "-*-*-*-*-*-*-12-*-*-*-m-*-*-*"), + (0,0,0,0, "-adobe-courier-bold-o-normal--12-120-75-75-/-70-iso8859-1", "-*-*-*-*-*-*-12-*-*-*-m-*-*-*"), + (1,1,1,1, "XXX/adobe/courier/bold/o/normal//12/120/75/75/m/70/iso8859/1", "XXX/*/*/*/*/*/*/12/*/*/*/m/*/*/*"), + (0,0,0,0, "XXX/adobe/courier/bold/o/normal//12/120/75/75/X/70/iso8859/1", "XXX/*/*/*/*/*/*/12/*/*/*/m/*/*/*"), + (1,1,1,1, "abcd/abcdefg/abcdefghijk/abcdefghijklmnop.txt", "**/*a*b*g*n*t"), + (0,0,0,0, "abcd/abcdefg/abcdefghijk/abcdefghijklmnop.txtz", "**/*a*b*g*n*t"), + (0,0,0,0, "foo", "*/*/*"), + (0,0,0,0, "foo/bar", "*/*/*"), + (1,1,1,1, "foo/bba/arr", "*/*/*"), + (0,0,1,1, "foo/bb/aa/rr", "*/*/*"), + (1,1,1,1, "foo/bb/aa/rr", "**/**/**"), + (1,1,1,1, "abcXdefXghi", "*X*i"), + (0,0,1,1, "ab/cXd/efXg/hi", "*X*i"), + (1,1,1,1, "ab/cXd/efXg/hi", "*/*X*/*/*i"), + (1,1,1,1, "ab/cXd/efXg/hi", "**/*X*/**/*i"), + + // Extra path(tests + (0,0,0,0, "foo", "fo"), + (1,1,1,1,"foo/bar", "foo/bar"), + (1,1,1,1, "foo/bar", "foo/*"), + (0,0,1,1, "foo/bba/arr", "foo/*"), + (1,1,1,1, "foo/bba/arr", "foo/**"), + (0,0,1,1, "foo/bba/arr", "foo*"), + (0,0,1,1, "foo/bba/arr", "foo/*arr"), + (0,0,1,1, "foo/bba/arr", "foo/**arr"), + (0,0,0,0, "foo/bba/arr", "foo/*z"), + (0,0,0,0, "foo/bba/arr", "foo/**z"), + (0,0,1,1, "foo/bar", "foo?bar"), + (0,0,1,1, "foo/bar", "foo[/]bar"), + (0,0,1,1, "foo/bar", "foo[^a-z]bar"), + (0,0,1,1, "ab/cXd/efXg/hi", "*Xg*i"), + + // Extra case-sensitivity tests + (0,1,0,1, "a", "[A-Z]"), + (1,1,1,1, "A", "[A-Z]"), + (0,1,0,1, "A", "[a-z]"), + (1,1,1,1, "a", "[a-z]"), + (0,1,0,1, "a", "[[:upper:]]"), + (1,1,1,1, "A", "[[:upper:]]"), + (0,1,0,1, "A", "[[:lower:]]"), + (1,1,1,1, "a", "[[:lower:]]"), + (0,1,0,1, "A", "[B-Za]"), + (1,1,1,1, "a", "[B-Za]"), + (0,1,0,1, "A", "[B-a]"), + (1,1,1,1, "a", "[B-a]"), + (0,1,0,1, "z", "[Z-y]"), + (1,1,1,1, "Z", "[Z-y]"), + ]; + + let mut failures = Vec::new(); + let mut at_least_one_panic = 0; + for (path_match, path_imatch, glob_match, glob_imatch, text, pattern_text) in tests { + let (pattern, actual) = multi_match(pattern_text, text); + let expected = expect_multi(path_match, path_imatch, glob_match, glob_imatch); + + if actual.all_panicked() { + at_least_one_panic += 1; + } else if actual != expected { + failures.push((pattern, pattern_text, text, actual, expected)); + } else { + at_least_one_panic += if actual.any_panicked() { 1 } else { 0 }; + } + } + + assert_eq!(failures.len(), 0); + assert_eq!(at_least_one_panic, 0, "not a single panic in any invocation"); + + // TODO: reproduce these + // (0 0 0 0 \ + // 1 1 1 1 '\' '\' + // (0 0 0 0 \ + // E E E E 'foo' '' + // (0 0 0 0 \ + // 1 1 1 1 'a[]b' 'a[]b' + // (0 0 0 0 \ + // 1 1 1 1 'ab[' 'ab[' + // (0 0 1 1 \ + // 1 1 1 1 foo/bba/arr 'foo**' +} + +#[test] +fn brackets() { + let (_pattern, actual) = multi_match(r"[B-a]", "A"); + assert!(!actual.any_panicked()); + assert_eq!(actual, expect_multi(0, 1, 0, 1)); +} + +fn multi_match(pattern_text: &str, text: &str) -> (Pattern, MultiMatch) { + let pattern = git_glob::Pattern::from_bytes(pattern_text.as_bytes()).expect("valid (enough) pattern"); + let actual_path_match: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Sensitive)).into(); + let actual_path_imatch: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Fold)).into(); + let actual_glob_match: MatchResult = catch_unwind(|| pattern.matches(text, wildmatch::Mode::empty())).into(); + let actual_glob_imatch: MatchResult = catch_unwind(|| pattern.matches(text, wildmatch::Mode::IGNORE_CASE)).into(); + let actual = MultiMatch { + path_match: actual_path_match, + path_imatch: actual_path_imatch, + glob_match: actual_glob_match, + glob_imatch: actual_glob_imatch, + }; + (pattern, actual) +} + +fn expect_multi(path_match: u8, path_imatch: u8, glob_match: u8, glob_imatch: u8) -> MultiMatch { + (path_match, path_imatch, glob_match, glob_imatch).into() +} + +#[derive(Eq, PartialEq)] +struct MultiMatch { + path_match: MatchResult, + path_imatch: MatchResult, + glob_match: MatchResult, + glob_imatch: MatchResult, +} + +impl MultiMatch { + fn all_panicked(&self) -> bool { + use MatchResult::Panic; + matches!(self.path_match, Panic) + && matches!(self.path_imatch, Panic) + && matches!(self.glob_match, Panic) + && matches!(self.glob_imatch, Panic) + } + fn any_panicked(&self) -> bool { + use MatchResult::Panic; + matches!(self.path_match, Panic) + || matches!(self.path_imatch, Panic) + || matches!(self.glob_match, Panic) + || matches!(self.glob_imatch, Panic) + } +} + +impl From<(u8, u8, u8, u8)> for MultiMatch { + fn from(t: (u8, u8, u8, u8)) -> Self { + MultiMatch { + path_match: t.0.into(), + path_imatch: t.1.into(), + glob_match: t.2.into(), + glob_imatch: t.3.into(), + } + } +} + +impl Debug for MultiMatch { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "({} {} {} {})", + self.path_match, self.path_imatch, self.glob_match, self.glob_imatch + ) + } +} + +enum MatchResult { + Match, + NoMatch, + Panic, +} + +impl PartialEq for MatchResult { + fn eq(&self, other: &Self) -> bool { + use MatchResult::*; + match (self, other) { + (Panic, _) | (_, Panic) => true, + (Match, NoMatch) | (NoMatch, Match) => false, + (Match, Match) | (NoMatch, NoMatch) => true, + } + } +} + +impl std::cmp::Eq for MatchResult {} + +impl From> for MatchResult { + fn from(v: std::thread::Result) -> Self { + use MatchResult::*; + match v { + Ok(v) if v => Match, + Ok(_) => NoMatch, + Err(_) => Panic, + } + } +} + +impl From for MatchResult { + fn from(v: u8) -> Self { + use MatchResult::*; + match v { + 1 => Match, + 0 => NoMatch, + _ => unreachable!("BUG: only use 0 or 1 for expected values"), + } + } +} + +impl Display for MatchResult { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + use MatchResult::*; + f.write_str(match self { + Match => "✔️", + NoMatch => "⨯", + Panic => "E", + }) + } +} + +fn match_file_path(pattern: &git_glob::Pattern, path: &str, case: Case) -> bool { + pattern.matches_repo_relative_path(path, basename_of(path), false /* is_dir */, case) +} +fn basename_of(path: &str) -> Option { + path.rfind('/').map(|pos| pos + 1) +} diff --git a/git-odb/tests/odb/store/compound.rs b/git-odb/tests/odb/store/compound.rs index e8bad044cfe..f5a5711f7b5 100644 --- a/git-odb/tests/odb/store/compound.rs +++ b/git-odb/tests/odb/store/compound.rs @@ -9,9 +9,10 @@ fn db() -> git_odb::Handle { } mod locate { - use crate::{hex_to_id, odb::store::compound::db}; use git_odb::Find; + use crate::{hex_to_id, odb::store::compound::db}; + fn can_locate(db: &git_odb::Handle, hex_id: &str) { let mut buf = vec![]; assert!(db diff --git a/git-odb/tests/odb/store/dynamic.rs b/git-odb/tests/odb/store/dynamic.rs index 1e760be9e8c..6d6b6df9a27 100644 --- a/git-odb/tests/odb/store/dynamic.rs +++ b/git-odb/tests/odb/store/dynamic.rs @@ -1,6 +1,6 @@ -use git_hash::ObjectId; use std::process::Command; +use git_hash::ObjectId; use git_odb::{store, Find, FindExt, Write}; use git_testtools::{fixture_path, hex_to_id}; diff --git a/git-odb/tests/odb/store/linked.rs b/git-odb/tests/odb/store/linked.rs index bc9d680483c..4171ad12d33 100644 --- a/git-odb/tests/odb/store/linked.rs +++ b/git-odb/tests/odb/store/linked.rs @@ -57,10 +57,11 @@ mod locate { } mod init { - use crate::odb::{alternate::alternate, store::linked::db}; use git_hash::ObjectId; use git_odb::Find; + use crate::odb::{alternate::alternate, store::linked::db}; + #[test] fn multiple_linked_repositories_via_alternates() -> crate::Result { let tmp = git_testtools::tempfile::TempDir::new()?; diff --git a/git-ref/tests/fullname/mod.rs b/git-ref/tests/fullname/mod.rs index fa09ebc6c6d..01fb8129a1e 100644 --- a/git-ref/tests/fullname/mod.rs +++ b/git-ref/tests/fullname/mod.rs @@ -1,6 +1,7 @@ -use git_ref::Category; use std::convert::TryInto; +use git_ref::Category; + #[test] fn file_name() { let name: git_ref::FullName = "refs/heads/main".try_into().unwrap(); diff --git a/git-repository/Cargo.toml b/git-repository/Cargo.toml index 18183327434..f6bf517ed6f 100644 --- a/git-repository/Cargo.toml +++ b/git-repository/Cargo.toml @@ -3,7 +3,7 @@ name = "git-repository" repository = "https://github.com/Byron/gitoxide" description = "Abstractions for git repositories" license = "MIT/Apache-2.0" -version = "0.16.0" +version = "0.17.0" authors = ["Sebastian Thiel "] edition = "2018" include = ["src/**/*", "CHANGELOG.md"] @@ -49,7 +49,7 @@ max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git- local-time-support = ["git-actor/local-time-support"] ## Re-export stability tier 2 crates for convenience and make `Repository` struct fields with types from these crates publicly accessible. ## Doing so is less stable than the stability tier 1 that `git-repository` is a member of. -unstable = ["git-index", "git-worktree", "git-mailmap"] +unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob"] ## Print debugging information about usage of object database caches, useful for tuning cache sizes. cache-efficiency-debug = ["git-features/cache-efficiency-debug"] @@ -78,6 +78,7 @@ git-mailmap = { version = "^0.1.0", path = "../git-mailmap", optional = true } git-features = { version = "^0.20.0", path = "../git-features", features = ["progress"] } # unstable only +git-glob = { version = "^0.2.0", path = "../git-glob", optional = true } git-index = { version = "^0.2.0", path = "../git-index", optional = true } git-worktree = { version = "^0.1.0", path = "../git-worktree", optional = true } diff --git a/git-repository/src/commit.rs b/git-repository/src/commit.rs index c1f18165f35..d3175990552 100644 --- a/git-repository/src/commit.rs +++ b/git-repository/src/commit.rs @@ -17,12 +17,12 @@ pub enum Error { /// pub mod describe { - use crate::bstr::BStr; - use crate::ext::ObjectIdExt; - use crate::Repository; + use std::borrow::Cow; + use git_hash::ObjectId; use git_odb::Find; - use std::borrow::Cow; + + use crate::{bstr::BStr, ext::ObjectIdExt, Repository}; /// The result of [try_resolve()][Platform::try_resolve()]. pub struct Resolution<'repo> { diff --git a/git-repository/src/config.rs b/git-repository/src/config.rs index ec829d5019d..19de3b86538 100644 --- a/git-repository/src/config.rs +++ b/git-repository/src/config.rs @@ -26,16 +26,20 @@ pub(crate) struct Cache { pub object_hash: git_hash::Kind, /// If true, multi-pack indices, whether present or not, may be used by the object database. pub use_multi_pack_index: bool, + // TODO: make core.precomposeUnicode available as well. } mod cache { + use std::{borrow::Cow, convert::TryFrom}; + + use git_config::{ + file::GitConfig, + values, + values::{Boolean, Integer}, + }; + use super::{Cache, Error}; use crate::bstr::ByteSlice; - use git_config::file::GitConfig; - use git_config::values; - use git_config::values::{Boolean, Integer}; - use std::borrow::Cow; - use std::convert::TryFrom; impl Cache { pub fn new(git_dir: &std::path::Path) -> Result { diff --git a/git-repository/src/lib.rs b/git-repository/src/lib.rs index bba8fe4adc3..0bb2f9c6797 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -88,6 +88,7 @@ //! * [`actor`] //! * [`bstr`][bstr] //! * [`index`] +//! * [`glob`] //! * [`worktree`] //! * [`mailmap`] //! * [`objs`] @@ -126,6 +127,8 @@ pub use git_diff as diff; use git_features::threading::OwnShared; #[cfg(feature = "unstable")] pub use git_features::{parallel, progress, progress::Progress, threading}; +#[cfg(all(feature = "unstable", feature = "git-glob"))] +pub use git_glob as glob; pub use git_hash as hash; #[doc(inline)] #[cfg(all(feature = "unstable", feature = "git-index"))] diff --git a/git-repository/src/object/commit.rs b/git-repository/src/object/commit.rs index 434cc6e1399..b4b56138fa7 100644 --- a/git-repository/src/object/commit.rs +++ b/git-repository/src/object/commit.rs @@ -18,10 +18,10 @@ mod error { } } -use crate::id::Ancestors; - pub use error::Error; +use crate::id::Ancestors; + impl<'repo> Commit<'repo> { /// Create an owned instance of this object, copying our data in the process. pub fn detached(&self) -> DetachedObject { diff --git a/git-repository/src/object/tag.rs b/git-repository/src/object/tag.rs index 419e88d2ce2..f7532aa1f27 100644 --- a/git-repository/src/object/tag.rs +++ b/git-repository/src/object/tag.rs @@ -1,5 +1,4 @@ -use crate::ext::ObjectIdExt; -use crate::Tag; +use crate::{ext::ObjectIdExt, Tag}; impl<'repo> Tag<'repo> { /// Decode this tag and return the id of its target. diff --git a/git-repository/src/repository/location.rs b/git-repository/src/repository/location.rs index bc391b914ed..f3998dbbf25 100644 --- a/git-repository/src/repository/location.rs +++ b/git-repository/src/repository/location.rs @@ -9,7 +9,7 @@ impl crate::Repository { self.work_tree.as_deref() } - // TODO: tests + // TODO: tests, respect precomposeUnicode /// The directory of the binary path of the current process. pub fn install_dir(&self) -> std::io::Result { std::env::current_exe().and_then(|exe| { diff --git a/git-revision/src/describe.rs b/git-revision/src/describe.rs index eda3c66c778..6c355cc23c0 100644 --- a/git-revision/src/describe.rs +++ b/git-revision/src/describe.rs @@ -143,21 +143,19 @@ where } pub(crate) mod function { - use super::Error; - use hash_hasher::HashBuildHasher; - use std::cmp::Ordering; - use std::collections::HashMap; use std::{ borrow::Cow, - collections::{hash_map, VecDeque}, + cmp::Ordering, + collections::{hash_map, HashMap, VecDeque}, iter::FromIterator, }; - use crate::describe::{Flags, Options, MAX_CANDIDATES}; use git_hash::oid; use git_object::{bstr::BStr, CommitRefIter}; + use hash_hasher::HashBuildHasher; - use super::Outcome; + use super::{Error, Outcome}; + use crate::describe::{Flags, Options, MAX_CANDIDATES}; /// Given a `commit` id, traverse the commit graph and collect candidate names from the `name_by_oid` mapping to produce /// an `Outcome`, which converted [`into_format()`][Outcome::into_format()] will produce a typical `git describe` string. diff --git a/git-tix/CHANGELOG.md b/git-tix/CHANGELOG.md new file mode 100644 index 00000000000..1241f04e634 --- /dev/null +++ b/git-tix/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.0.0 (2022-04-14) + +Initial release. + +### Commit Statistics + + + + - 2 commits contributed to the release. + - 0 commits where understood as [conventional](https://www.conventionalcommits.org). + - 1 unique issue was worked on: [#325](https://github.com/Byron/gitoxide/issues/325) + +### Commit Details + + + +
view details + + * **[#325](https://github.com/Byron/gitoxide/issues/325)** + - update changelog ([`37a57f1`](https://github.com/Byron/gitoxide/commit/37a57f17c1b73e733a498fa5648187380fcb30c0)) + - empty crate for 'tix' tool ([`5427cf7`](https://github.com/Byron/gitoxide/commit/5427cf7f286eb0429c3164e697e96130daf2de39)) +
+ diff --git a/git-tix/Cargo.toml b/git-tix/Cargo.toml new file mode 100644 index 00000000000..ae86d5d988b --- /dev/null +++ b/git-tix/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "git-tix" +version = "0.0.0" +repository = "https://github.com/Byron/gitoxide" +license = "MIT/Apache-2.0" +description = "A tool like `tig`, but minimal, fast and efficient" +authors = ["Sebastian Thiel "] +edition = "2018" + +[lib] +doctest = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/git-tix/src/lib.rs b/git-tix/src/lib.rs new file mode 100644 index 00000000000..d7a83e4f525 --- /dev/null +++ b/git-tix/src/lib.rs @@ -0,0 +1 @@ +#![forbid(unsafe_code, rust_2018_idioms)] diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index b17f498903d..82404a4c42a 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -2,7 +2,7 @@ name = "gitoxide-core" description = "The library implementating all capabilities of the gitoxide CLI" repository = "https://github.com/Byron/gitoxide" -version = "0.14.0" +version = "0.15.0" authors = ["Sebastian Thiel "] license = "MIT/Apache-2.0" edition = "2018" @@ -38,7 +38,7 @@ local-time-support = ["git-repository/local-time-support"] [dependencies] # deselect everything else (like "performance") as this should be controllable by the parent application. -git-repository = { version = "^0.16.0", path = "../git-repository", default-features = false, features = ["local", "unstable"]} # TODO: eventually 'unstable' shouldn't be needed anymore +git-repository = { version = "^0.17.0", path = "../git-repository", default-features = false, features = ["local", "unstable"]} # TODO: eventually 'unstable' shouldn't be needed anymore git-pack-for-configuration-only = { package = "git-pack", version = "^0.18.0", path = "../git-pack", default-features = false, features = ["pack-cache-lru-dynamic", "pack-cache-lru-static"] } git-commitgraph = { version = "^0.7.0", path = "../git-commitgraph" } git-config = { version = "^0.2.1", path = "../git-config" } diff --git a/gitoxide-core/src/repository/commit.rs b/gitoxide-core/src/repository/commit.rs index f8dc75e181d..1e449121361 100644 --- a/gitoxide-core/src/repository/commit.rs +++ b/gitoxide-core/src/repository/commit.rs @@ -1,6 +1,7 @@ +use std::path::PathBuf; + use anyhow::{Context, Result}; use git_repository as git; -use std::path::PathBuf; pub fn describe( repo: impl Into, diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 00afd0a50e4..f8b391a56b4 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -9,7 +9,6 @@ use std::{ use anyhow::Result; use clap::Parser; - use gitoxide_core as core; use gitoxide_core::pack::verify;