Unified · Split
| @@ -6,7 +6,8 @@ · 5 unmodified lines | |||
| 6 | doc = ::document_features::document_features!() | 6 | doc = ::document_features::document_features!() |
| 7 | )] | 7 | )] |
| 8 | #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))] | 8 | #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))] |
| 9 | #![deny(missing_docs, rust_2018_idioms, unsafe_code)] | 9 | #![deny(missing_docs, rust_2018_idioms)] |
| 10 | #![forbid(unsafe_code)] | ||
| 10 | 11 | ||
| 11 | pub use gix_glob as glob; | 12 | pub use gix_glob as glob; |
| 12 | use kstring::{KString, KStringRef}; | 13 | use kstring::{KString, KStringRef}; |
| @@ -1,8 +1,7 @@ | |||
| 1 | use crate::{Name, NameRef}; | ||
| 1 | use bstr::{BStr, BString, ByteSlice}; | 2 | use bstr::{BStr, BString, ByteSlice}; |
| 2 | use kstring::KStringRef; | 3 | use kstring::KStringRef; |
| 3 | 4 | ||
| 4 | use crate::{Name, NameRef}; | ||
| 5 | |||
| 6 | impl<'a> NameRef<'a> { | 5 | impl<'a> NameRef<'a> { |
| 7 | /// Turn this ref into its owned counterpart. | 6 | /// Turn this ref into its owned counterpart. |
| 8 | pub fn to_owned(self) -> Name { | 7 | pub fn to_owned(self) -> Name { |
| @@ -1,10 +1,9 @@ | |||
| 1 | use std::borrow::Cow; | 1 | use std::borrow::Cow; |
| 2 | 2 | ||
| 3 | use crate::{name, AssignmentRef, Name, NameRef, StateRef}; | ||
| 3 | use bstr::{BStr, ByteSlice}; | 4 | use bstr::{BStr, ByteSlice}; |
| 4 | use kstring::KStringRef; | 5 | use kstring::KStringRef; |
| 5 | 6 | ||
| 6 | use crate::{name, AssignmentRef, Name, NameRef, StateRef}; | ||
| 7 | |||
| 8 | /// The kind of attribute that was parsed. | 7 | /// The kind of attribute that was parsed. |
| 9 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | 8 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] |
| 10 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | 9 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| @@ -1,7 +1,6 @@ | |||
| 1 | use std::collections::HashMap; | ||
| 2 | |||
| 3 | use kstring::KString; | 1 | use kstring::KString; |
| 4 | use smallvec::SmallVec; | 2 | use smallvec::SmallVec; |
| 3 | use std::collections::HashMap; | ||
| 5 | 4 | ||
| 6 | use crate::{Assignment, AssignmentRef}; | 5 | use crate::{Assignment, AssignmentRef}; |
| 7 | 6 | ||
| @@ -1,29 +1,24 @@ | |||
| 1 | use bstr::{BStr, ByteSlice}; | ||
| 2 | use kstring::{KString, KStringRef}; | ||
| 3 | |||
| 4 | use crate::{State, StateRef}; | 1 | use crate::{State, StateRef}; |
| 2 | use bstr::{BStr, BString, ByteSlice}; | ||
| 5 | 3 | ||
| 6 | /// A container to encapsulate a tightly packed and typically unallocated byte value that isn't necessarily UTF8 encoded. | 4 | /// A container to encapsulate a tightly packed and typically unallocated byte value that isn't necessarily UTF8 encoded. |
| 7 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | 5 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] |
| 8 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | 6 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| 9 | pub struct Value(KString); | 7 | // TODO: This should be some sort of 'smallbstring' - but can't use `kstring` here due to UTF8 requirement. 5% performance boost possible. |
| 8 | // What's really needed here is a representation that displays as string when serialized which helps with JSON. | ||
| 9 | // Maybe `smallvec` with display and serialization wrapper would do the trick? | ||
| 10 | pub struct Value(BString); | ||
| 10 | 11 | ||
| 11 | /// A reference container to encapsulate a tightly packed and typically unallocated byte value that isn't necessarily UTF8 encoded. | 12 | /// A reference container to encapsulate a tightly packed and typically unallocated byte value that isn't necessarily UTF8 encoded. |
| 12 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] | 13 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] |
| 13 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | 14 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| 14 | pub struct ValueRef<'a>(#[cfg_attr(feature = "serde", serde(borrow))] KStringRef<'a>); | 15 | pub struct ValueRef<'a>(#[cfg_attr(feature = "serde", serde(borrow))] &'a [u8]); |
| 15 | 16 | ||
| 16 | /// Lifecycle | 17 | /// Lifecycle |
| 17 | impl<'a> ValueRef<'a> { | 18 | impl<'a> ValueRef<'a> { |
| 18 | /// Keep `input` as our value. | 19 | /// Keep `input` as our value. |
| 19 | pub fn from_bytes(input: &'a [u8]) -> Self { | 20 | pub fn from_bytes(input: &'a [u8]) -> Self { |
| 20 | Self(KStringRef::from_ref( | 21 | Self(input) |
| 21 | // SAFETY: our API makes accessing that value as `str` impossible, so illformed UTF8 is never exposed as such. | ||
| 22 | #[allow(unsafe_code)] | ||
| 23 | unsafe { | ||
| 24 | std::str::from_utf8_unchecked(input) | ||
| 25 | }, | ||
| 26 | )) | ||
| 27 | } | 22 | } |
| 28 | } | 23 | } |
| 29 | 24 | ||
| @@ -42,7 +37,7 @@ impl ValueRef<'_> { · 12 unmodified lines | |||
| 42 | 37 | ||
| 43 | impl<'a> From<&'a str> for ValueRef<'a> { | 38 | impl<'a> From<&'a str> for ValueRef<'a> { |
| 44 | fn from(v: &'a str) -> Self { | 39 | fn from(v: &'a str) -> Self { |
| 45 | ValueRef(v.into()) | 40 | ValueRef(v.as_bytes()) |
| 46 | } | 41 | } |
| 47 | } | 42 | } |
| 48 | 43 | ||
| @@ -54,7 +49,7 @@ impl<'a> From<ValueRef<'a>> for Value { · 5 unmodified lines | |||
| 54 | 49 | ||
| 55 | impl From<&str> for Value { | 50 | impl From<&str> for Value { |
| 56 | fn from(v: &str) -> Self { | 51 | fn from(v: &str) -> Self { |
| 57 | Value(KString::from_ref(v)) | 52 | Value(v.as_bytes().into()) |
| 58 | } | 53 | } |
| 59 | } | 54 | } |
| 60 | 55 | ||
| @@ -1,4 +1,5 @@ | |||
| 1 | use bstr::BString; | 1 | use bstr::BString; |
| 2 | use gix_attributes::state::ValueRef; | ||
| 2 | use gix_attributes::{parse, StateRef}; | 3 | use gix_attributes::{parse, StateRef}; |
| 3 | use gix_glob::pattern::Mode; | 4 | use gix_glob::pattern::Mode; |
| 4 | use gix_testtools::fixture_bytes; | 5 | use gix_testtools::fixture_bytes; |
| @@ -275,6 +276,19 @@ fn attributes_can_have_values() { · 270 unmodified lines | |||
| 275 | ); | 276 | ); |
| 276 | } | 277 | } |
| 277 | 278 | ||
| 279 | #[test] | ||
| 280 | fn attributes_can_have_illformed_utf8() { | ||
| 281 | assert_eq!( | ||
| 282 | byte_line(b"p a=one b=\xC3\x28\x41 c=d "), | ||
| 283 | ( | ||
| 284 | pattern("p", Mode::NO_SUB_DIR, None), | ||
| 285 | vec![value("a", "one"), byte_value("b", b"\xC3\x28\x41"), value("c", "d")], | ||
| 286 | 1 | ||
| 287 | ), | ||
| 288 | "illformed UTF8 is fully supported" | ||
| 289 | ); | ||
| 290 | } | ||
| 291 | |||
| 278 | #[test] | 292 | #[test] |
| 279 | fn attributes_see_state_adjustments_over_value_assignments() { | 293 | fn attributes_see_state_adjustments_over_value_assignments() { |
| 280 | assert_eq!( | 294 | assert_eq!( |
| @@ -325,6 +339,10 @@ fn value<'b>(attr: &str, value: &'b str) -> (BString, StateRef<'b>) { · 44 unmodified lines | |||
| 325 | (attr.into(), StateRef::Value(value.into())) | 339 | (attr.into(), StateRef::Value(value.into())) |
| 326 | } | 340 | } |
| 327 | 341 | ||
| 342 | fn byte_value<'b>(attr: &str, value: &'b [u8]) -> (BString, StateRef<'b>) { | ||
| 343 | (attr.into(), StateRef::Value(ValueRef::from_bytes(value))) | ||
| 344 | } | ||
| 345 | |||
| 328 | fn pattern(name: &str, flags: gix_glob::pattern::Mode, first_wildcard_pos: Option<usize>) -> parse::Kind { | 346 | fn pattern(name: &str, flags: gix_glob::pattern::Mode, first_wildcard_pos: Option<usize>) -> parse::Kind { |
| 329 | parse::Kind::Pattern(gix_glob::Pattern { | 347 | parse::Kind::Pattern(gix_glob::Pattern { |
| 330 | text: name.into(), | 348 | text: name.into(), |
| @@ -344,6 +362,17 @@ fn line(input: &str) -> ExpandedAttribute { · 13 unmodified lines | |||
| 344 | try_line(input).unwrap() | 362 | try_line(input).unwrap() |
| 345 | } | 363 | } |
| 346 | 364 | ||
| 365 | fn byte_line(input: &[u8]) -> ExpandedAttribute { | ||
| 366 | try_byte_line(input).unwrap() | ||
| 367 | } | ||
| 368 | |||
| 369 | fn try_byte_line(input: &[u8]) -> Result<ExpandedAttribute, parse::Error> { | ||
| 370 | let mut lines = gix_attributes::parse(input); | ||
| 371 | let res = expand(lines.next().unwrap())?; | ||
| 372 | assert!(lines.next().is_none(), "expected only one line"); | ||
| 373 | Ok(res) | ||
| 374 | } | ||
| 375 | |||
| 347 | fn lenient_lines(input: &str) -> Vec<ExpandedAttribute> { | 376 | fn lenient_lines(input: &str) -> Vec<ExpandedAttribute> { |
| 348 | gix_attributes::parse(input.as_bytes()) | 377 | gix_attributes::parse(input.as_bytes()) |
| 349 | .map(expand) | 378 | .map(expand) |
| @@ -270,7 +270,7 @@ fn given_attributes_are_made_available_in_given_order() -> crate::Result { · 269 unmodified lines | |||
| 270 | fn size_of_outcome() { | 270 | fn size_of_outcome() { |
| 271 | assert_eq!( | 271 | assert_eq!( |
| 272 | std::mem::size_of::<Outcome>(), | 272 | std::mem::size_of::<Outcome>(), |
| 273 | 904, | 273 | 840, |
| 274 | "it's quite big, shouldn't change without us noticing" | 274 | "it's quite big, shouldn't change without us noticing" |
| 275 | ) | 275 | ) |
| 276 | } | 276 | } |
| @@ -169,6 +169,7 @@ pub fn path( · 168 unmodified lines | |||
| 169 | .map(|platform| platform.excluded_kind()) | 169 | .map(|platform| platform.excluded_kind()) |
| 170 | }) | 170 | }) |
| 171 | .map_err(Error::ExcludesAccess)? | 171 | .map_err(Error::ExcludesAccess)? |
| 172 | .filter(|_| filename_start_idx > 0) | ||
| 172 | { | 173 | { |
| 173 | out.status = entry::Status::Ignored(excluded); | 174 | out.status = entry::Status::Ignored(excluded); |
| 174 | } | 175 | } |
| @@ -256,6 +257,7 @@ pub fn path( · 81 unmodified lines | |||
| 256 | if let Some(excluded) = ctx | 257 | if let Some(excluded) = ctx |
| 257 | .excludes | 258 | .excludes |
| 258 | .as_mut() | 259 | .as_mut() |
| 260 | .filter(|_| !rela_path.is_empty()) | ||
| 259 | .map_or(Ok(None), |stack| { | 261 | .map_or(Ok(None), |stack| { |
| 260 | stack | 262 | stack |
| 261 | .at_entry(rela_path.as_bstr(), is_dir, ctx.objects) | 263 | .at_entry(rela_path.as_bstr(), is_dir, ctx.objects) |
| @@ -351,3 +351,83 @@ git clone submodule multiple-submodules · 350 unmodified lines | |||
| 351 | git submodule add ../submodule a/b | 351 | git submodule add ../submodule a/b |
| 352 | git commit -m "add modules" | 352 | git commit -m "add modules" |
| 353 | ) | 353 | ) |
| 354 | |||
| 355 | git clone submodule one-ignored-submodule | ||
| 356 | (cd one-ignored-submodule | ||
| 357 | git submodule add ../submodule submodule | ||
| 358 | echo '/submodule/' > .gitignore | ||
| 359 | echo '*' > submodule/.gitignore | ||
| 360 | git commit -m "add seemingly ignored submodule" | ||
| 361 | ) | ||
| 362 | |||
| 363 | git init slash-in-root-and-negated | ||
| 364 | (cd slash-in-root-and-negated | ||
| 365 | cat <<'EOF' >.gitignore | ||
| 366 | / | ||
| 367 | !file | ||
| 368 | !*.md | ||
| 369 | !.github | ||
| 370 | !.github/** | ||
| 371 | EOF | ||
| 372 | touch file readme.md | ||
| 373 | mkdir .github | ||
| 374 | touch .github/workflow.yml | ||
| 375 | git add .github readme.md .gitignore | ||
| 376 | git commit -m "init" | ||
| 377 | ) | ||
| 378 | |||
| 379 | git init star-in-root-and-negated | ||
| 380 | (cd star-in-root-and-negated | ||
| 381 | cat <<'EOF' >.gitignore | ||
| 382 | * | ||
| 383 | !file | ||
| 384 | !.gitignore | ||
| 385 | !*.md | ||
| 386 | !.github | ||
| 387 | !.github/** | ||
| 388 | EOF | ||
| 389 | touch file readme.md | ||
| 390 | mkdir .github | ||
| 391 | touch .github/workflow.yml | ||
| 392 | git add .github readme.md .gitignore | ||
| 393 | git commit -m "init" | ||
| 394 | ) | ||
| 395 | |||
| 396 | git init slash-in-subdir-and-negated | ||
| 397 | (cd slash-in-subdir-and-negated | ||
| 398 | mkdir sub | ||
| 399 | (cd sub | ||
| 400 | cat <<'EOF' >.gitignore | ||
| 401 | / | ||
| 402 | !file | ||
| 403 | !*.md | ||
| 404 | !.github | ||
| 405 | !.github/** | ||
| 406 | EOF | ||
| 407 | touch file readme.md | ||
| 408 | mkdir .github | ||
| 409 | touch .github/workflow.yml | ||
| 410 | git add .github readme.md .gitignore | ||
| 411 | git commit -m "init" | ||
| 412 | ) | ||
| 413 | ) | ||
| 414 | |||
| 415 | git init star-in-subdir-and-negated | ||
| 416 | (cd star-in-subdir-and-negated | ||
| 417 | mkdir sub | ||
| 418 | (cd sub | ||
| 419 | cat <<'EOF' >.gitignore | ||
| 420 | * | ||
| 421 | !file | ||
| 422 | !.gitignore | ||
| 423 | !*.md | ||
| 424 | !.github | ||
| 425 | !.github/** | ||
| 426 | EOF | ||
| 427 | touch file readme.md | ||
| 428 | mkdir .github | ||
| 429 | touch .github/workflow.yml | ||
| 430 | git add .github readme.md .gitignore | ||
| 431 | git commit -m "init" | ||
| 432 | ) | ||
| 433 | ) | ||
| @@ -4276,3 +4276,161 @@ fn type_mismatch_ignore_case_clash_file_is_dir() { · 4275 unmodified lines | |||
| 4276 | If there was no special handling for this, it would have found the file (`d` in the index, icase), which would have been wrong." | 4276 | If there was no special handling for this, it would have found the file (`d` in the index, icase), which would have been wrong." |
| 4277 | ); | 4277 | ); |
| 4278 | } | 4278 | } |
| 4279 | |||
| 4280 | #[test] | ||
| 4281 | fn top_level_slash_with_negations() -> crate::Result { | ||
| 4282 | for repo_name in ["slash-in-root-and-negated", "star-in-root-and-negated"] { | ||
| 4283 | let root = fixture(repo_name); | ||
| 4284 | let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); | ||
| 4285 | assert_eq!( | ||
| 4286 | out, | ||
| 4287 | walk::Outcome { | ||
| 4288 | read_dir_calls: 2, | ||
| 4289 | returned_entries: entries.len(), | ||
| 4290 | seen_entries: 5, | ||
| 4291 | } | ||
| 4292 | ); | ||
| 4293 | assert_eq!( | ||
| 4294 | entries, | ||
| 4295 | &[ | ||
| 4296 | entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always), | ||
| 4297 | entry(".github/workflow.yml", Tracked, File), | ||
| 4298 | entry(".gitignore", Tracked, File), | ||
| 4299 | entry("file", Untracked, File), | ||
| 4300 | entry("readme.md", Tracked, File), | ||
| 4301 | ], | ||
| 4302 | "the top-level is never considered ignored" | ||
| 4303 | ); | ||
| 4304 | |||
| 4305 | let ((out, _root), entries) = collect(&root, None, |keep, ctx| { | ||
| 4306 | walk( | ||
| 4307 | &root, | ||
| 4308 | ctx, | ||
| 4309 | walk::Options { | ||
| 4310 | for_deletion: Some(ForDeletionMode::FindRepositoriesInIgnoredDirectories), | ||
| 4311 | emit_tracked: false, | ||
| 4312 | ..options_emit_all() | ||
| 4313 | }, | ||
| 4314 | keep, | ||
| 4315 | ) | ||
| 4316 | }); | ||
| 4317 | assert_eq!( | ||
| 4318 | out, | ||
| 4319 | walk::Outcome { | ||
| 4320 | read_dir_calls: 2, | ||
| 4321 | returned_entries: entries.len(), | ||
| 4322 | seen_entries: 5, | ||
| 4323 | } | ||
| 4324 | ); | ||
| 4325 | assert_eq!( | ||
| 4326 | entries, | ||
| 4327 | &[ | ||
| 4328 | entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always), | ||
| 4329 | entry("file", Untracked, File) | ||
| 4330 | ], | ||
| 4331 | "And the negated file is correctly detected as untracked" | ||
| 4332 | ); | ||
| 4333 | } | ||
| 4334 | Ok(()) | ||
| 4335 | } | ||
| 4336 | |||
| 4337 | #[test] | ||
| 4338 | fn subdir_slash_with_negations() -> crate::Result { | ||
| 4339 | for repo_name in ["slash-in-subdir-and-negated", "star-in-subdir-and-negated"] { | ||
| 4340 | let root = fixture(repo_name); | ||
| 4341 | let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); | ||
| 4342 | assert_eq!( | ||
| 4343 | out, | ||
| 4344 | walk::Outcome { | ||
| 4345 | read_dir_calls: 3, | ||
| 4346 | returned_entries: entries.len(), | ||
| 4347 | seen_entries: 5, | ||
| 4348 | } | ||
| 4349 | ); | ||
| 4350 | assert_eq!( | ||
| 4351 | entries, | ||
| 4352 | &[ | ||
| 4353 | entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always), | ||
| 4354 | entry("sub/.github/workflow.yml", Tracked, File), | ||
| 4355 | entry("sub/.gitignore", Tracked, File), | ||
| 4356 | entry("sub/file", Untracked, File), | ||
| 4357 | entry("sub/readme.md", Tracked, File), | ||
| 4358 | ], | ||
| 4359 | "subdirectory matches work as expected, also with a `/` which has no bearing." | ||
| 4360 | ); | ||
| 4361 | |||
| 4362 | let ((out, _root), entries) = collect(&root, None, |keep, ctx| { | ||
| 4363 | walk( | ||
| 4364 | &root, | ||
| 4365 | ctx, | ||
| 4366 | walk::Options { | ||
| 4367 | for_deletion: Some(ForDeletionMode::FindRepositoriesInIgnoredDirectories), | ||
| 4368 | emit_tracked: false, | ||
| 4369 | ..options_emit_all() | ||
| 4370 | }, | ||
| 4371 | keep, | ||
| 4372 | ) | ||
| 4373 | }); | ||
| 4374 | assert_eq!( | ||
| 4375 | out, | ||
| 4376 | walk::Outcome { | ||
| 4377 | read_dir_calls: 3, | ||
| 4378 | returned_entries: entries.len(), | ||
| 4379 | seen_entries: 5, | ||
| 4380 | } | ||
| 4381 | ); | ||
| 4382 | assert_eq!( | ||
| 4383 | entries, | ||
| 4384 | &[ | ||
| 4385 | entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always), | ||
| 4386 | entry("sub/file", Untracked, File) | ||
| 4387 | ], | ||
| 4388 | "This is expected, and the `.git` top-level is pruned." | ||
| 4389 | ); | ||
| 4390 | } | ||
| 4391 | Ok(()) | ||
| 4392 | } | ||
| 4393 | |||
| 4394 | #[test] | ||
| 4395 | fn one_ignored_submodule() -> crate::Result { | ||
| 4396 | let root = fixture("one-ignored-submodule"); | ||
| 4397 | let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); | ||
| 4398 | assert_eq!( | ||
| 4399 | out, | ||
| 4400 | walk::Outcome { | ||
| 4401 | read_dir_calls: 1, | ||
| 4402 | returned_entries: entries.len(), | ||
| 4403 | seen_entries: 5, | ||
| 4404 | } | ||
| 4405 | ); | ||
| 4406 | assert_eq!( | ||
| 4407 | entries, | ||
| 4408 | &[ | ||
| 4409 | entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always), | ||
| 4410 | entry(".gitignore", Untracked, File), | ||
| 4411 | entry(".gitmodules", Tracked, File), | ||
| 4412 | entry("empty", Tracked, File), | ||
| 4413 | entry("submodule", Tracked, Repository), | ||
| 4414 | ], | ||
| 4415 | "when traversing the worktree root, this is correct, the submodule doesn't count as ignored" | ||
| 4416 | ); | ||
| 4417 | |||
| 4418 | let troot = root.join("submodule"); | ||
| 4419 | let ((out, _root), entries) = collect(&root, Some(&troot), |keep, ctx| { | ||
| 4420 | walk(&root, ctx, options_emit_all(), keep) | ||
| 4421 | }); | ||
| 4422 | assert_eq!( | ||
| 4423 | out, | ||
| 4424 | walk::Outcome { | ||
| 4425 | read_dir_calls: 0, | ||
| 4426 | returned_entries: entries.len(), | ||
| 4427 | seen_entries: 1 | ||
| 4428 | } | ||
| 4429 | ); | ||
| 4430 | assert_eq!( | ||
| 4431 | entries, | ||
| 4432 | &[entryps("submodule", Tracked, Repository, Verbatim)], | ||
| 4433 | "The submodule is simply tracked, it doesn't count as ignored" | ||
| 4434 | ); | ||
| 4435 | Ok(()) | ||
| 4436 | } | ||
| @@ -15,9 +15,8 @@ a/b/* · 14 unmodified lines | |||
| 15 | z/x | 15 | z/x |
| 16 | EOF | 16 | EOF |
| 17 | 17 | ||
| 18 | mkdir repo; | 18 | git init -q repo; |
| 19 | (cd repo | 19 | (cd repo |
| 20 | git init -q | ||
| 21 | git config core.excludesFile ../user.exclude | 20 | git config core.excludesFile ../user.exclude |
| 22 | 21 | ||
| 23 | cat <<EOF >.git/info/exclude | 22 | cat <<EOF >.git/info/exclude |
| @@ -93,3 +92,70 @@ E/f · 69 unmodified lines | |||
| 93 | E/F | 92 | E/F |
| 94 | EOF | 93 | EOF |
| 95 | ) | 94 | ) |
| 95 | |||
| 96 | git init slash-and-excludes | ||
| 97 | (cd slash-and-excludes | ||
| 98 | cat <<EOF >.gitignore | ||
| 99 | # a lone slash does nothing | ||
| 100 | / | ||
| 101 | # a file that was never ignored to begin | ||
| 102 | !file | ||
| 103 | EOF | ||
| 104 | |||
| 105 | git check-ignore -vn --stdin 2>&1 <<EOF >git-check-ignore.baseline || : | ||
| 106 | file | ||
| 107 | a-file-not-mentioned-in-gitignore | ||
| 108 | EOF | ||
| 109 | ) | ||
| 110 | |||
| 111 | git init slash-and-excludes-in-subdir | ||
| 112 | (cd slash-and-excludes-in-subdir | ||
| 113 | mkdir sub | ||
| 114 | (cd sub | ||
| 115 | cat <<EOF >.gitignore | ||
| 116 | # a lone slash does nothing | ||
| 117 | / | ||
| 118 | # a file that was never ignored to begin | ||
| 119 | !file | ||
| 120 | EOF | ||
| 121 | ) | ||
| 122 | git check-ignore -vn --stdin 2>&1 <<EOF >git-check-ignore.baseline || : | ||
| 123 | sub/file | ||
| 124 | sub/a-file-not-mentioned-in-gitignore | ||
| 125 | a-file-not-mentioned-in-gitignore | ||
| 126 | EOF | ||
| 127 | ) | ||
| 128 | |||
| 129 | git init star-and-excludes | ||
| 130 | (cd star-and-excludes | ||
| 131 | cat <<EOF >.gitignore | ||
| 132 | # everything is excluded by default | ||
| 133 | * | ||
| 134 | # And negations are used as an allow-list | ||
| 135 | !file | ||
| 136 | EOF | ||
| 137 | |||
| 138 | git check-ignore -vn --stdin 2>&1 <<EOF >git-check-ignore.baseline || : | ||
| 139 | file | ||
| 140 | a-file-not-mentioned-in-gitignore | ||
| 141 | EOF | ||
| 142 | ) | ||
| 143 | |||
| 144 | git init star-and-excludes-in-subdir | ||
| 145 | (cd star-and-excludes-in-subdir | ||
| 146 | mkdir sub | ||
| 147 | (cd sub | ||
| 148 | cat <<EOF >.gitignore | ||
| 149 | # everything is excluded by default | ||
| 150 | * | ||
| 151 | # And negations are used as an allow-list | ||
| 152 | !file | ||
| 153 | EOF | ||
| 154 | ) | ||
| 155 | |||
| 156 | git check-ignore -vn --stdin 2>&1 <<EOF >git-check-ignore.baseline || : | ||
| 157 | sub/file | ||
| 158 | sub/a-file-not-mentioned-in-gitignore | ||
| 159 | a-file-not-mentioned-in-gitignore | ||
| 160 | EOF | ||
| 161 | ) | ||
| @@ -1,5 +1,3 @@ | |||
| 1 | use std::io::Read; | ||
| 2 | |||
| 3 | use bstr::{BStr, ByteSlice}; | 1 | use bstr::{BStr, ByteSlice}; |
| 4 | use gix_glob::pattern::Case; | 2 | use gix_glob::pattern::Case; |
| 5 | use gix_ignore::search::Match; | 3 | use gix_ignore::search::Match; |
| @@ -31,69 +29,84 @@ impl<'a> Iterator for Expectations<'a> { · 25 unmodified lines | |||
| 31 | 29 | ||
| 32 | #[test] | 30 | #[test] |
| 33 | fn baseline_from_git_dir() -> crate::Result { | 31 | fn baseline_from_git_dir() -> crate::Result { |
| 34 | let case = if gix_fs::Capabilities::probe("../.git".as_ref()).ignore_case { | 32 | for repo_name in [ |
| 35 | Case::Fold | 33 | "repo", |
| 36 | } else { | 34 | "slash-and-excludes", |
| 37 | Case::Sensitive | 35 | "star-and-excludes-in-subdir", |
| 38 | }; | 36 | "slash-and-excludes-in-subdir", |
| 39 | let dir = gix_testtools::scripted_fixture_read_only("make_global_and_external_and_dir_ignores.sh")?; | 37 | ] { |
| 40 | let repo_dir = dir.join("repo"); | 38 | let case = if gix_fs::Capabilities::probe("../.git".as_ref()).ignore_case { |
| 41 | let git_dir = repo_dir.join(".git"); | 39 | Case::Fold |
| 42 | let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; | 40 | } else { |
| 43 | let mut buf = Vec::new(); | 41 | Case::Sensitive |
| 44 | let mut group = gix_ignore::Search::from_git_dir(&git_dir, Some(dir.join("user.exclude")), &mut buf)?; | 42 | }; |
| 43 | let dir = gix_testtools::scripted_fixture_read_only("make_global_and_external_and_dir_ignores.sh")?; | ||
| 44 | let repo_dir = dir.join(repo_name); | ||
| 45 | let git_dir = repo_dir.join(".git"); | ||
| 46 | let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; | ||
| 47 | let mut buf = Vec::new(); | ||
| 48 | let user_exclude = dir.join("user.exclude"); | ||
| 49 | let mut group = | ||
| 50 | gix_ignore::Search::from_git_dir(&git_dir, user_exclude.is_file().then_some(user_exclude), &mut buf)?; | ||
| 45 | 51 | ||
| 46 | assert!( | 52 | assert!( |
| 47 | !gix_glob::search::add_patterns_file(&mut group.patterns, "not-a-file".into(), false, None, &mut buf)?, | 53 | !gix_glob::search::add_patterns_file(&mut group.patterns, "not-a-file".into(), false, None, &mut buf)?, |
| 48 | "missing files are no problem and cause a negative response" | 54 | "missing files are no problem and cause a negative response" |
| 49 | ); | 55 | ); |
| 50 | assert!( | 56 | let mut ignore_file = repo_dir.join(".gitignore"); |
| 51 | gix_glob::search::add_patterns_file( | 57 | if !ignore_file.is_file() { |
| 52 | &mut group.patterns, | 58 | ignore_file.pop(); |
| 53 | repo_dir.join(".gitignore"), | 59 | ignore_file.push("sub/.gitignore"); |
| 54 | true, | 60 | } |
| 55 | repo_dir.as_path().into(), | 61 | assert!( |
| 56 | &mut buf | 62 | gix_glob::search::add_patterns_file( |
| 57 | )?, | 63 | &mut group.patterns, |
| 58 | "existing files return true" | 64 | ignore_file, |
| 59 | ); | 65 | true, |
| 66 | repo_dir.as_path().into(), | ||
| 67 | &mut buf | ||
| 68 | )?, | ||
| 69 | "existing files return true" | ||
| 70 | ); | ||
| 60 | 71 | ||
| 61 | buf.clear(); | 72 | let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore"); |
| 62 | let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore"); | 73 | if ignore_file.is_file() { |
| 63 | std::fs::File::open(&ignore_file)?.read_to_end(&mut buf)?; | 74 | let buf = std::fs::read(&ignore_file)?; |
| 64 | group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into()); | 75 | group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into()); |
| 76 | } | ||
| 65 | 77 | ||
| 66 | for (path, source_and_line) in (Expectations { | 78 | for (path, source_and_line) in (Expectations { |
| 67 | lines: baseline.lines(), | 79 | lines: baseline.lines(), |
| 68 | }) { | 80 | }) { |
| 69 | let actual = group.pattern_matching_relative_path( | 81 | let actual = group.pattern_matching_relative_path( |
| 70 | path, | 82 | path, |
| 71 | repo_dir | 83 | repo_dir |
| 72 | .join(path.to_str_lossy().as_ref()) | 84 | .join(path.to_str_lossy().as_ref()) |
| 73 | .metadata() | 85 | .metadata() |
| 74 | .ok() | 86 | .ok() |
| 75 | .map(|m| m.is_dir()), | 87 | .map(|m| m.is_dir()), |
| 76 | case, | 88 | case, |
| 77 | ); | 89 | ); |
| 78 | match (actual, source_and_line) { | 90 | match (actual, source_and_line) { |
| 79 | ( | 91 | ( |
| 80 | Some(Match { | 92 | Some(Match { |
| 81 | sequence_number, | 93 | sequence_number, |
| 82 | pattern: _, | 94 | pattern: _, |
| 83 | source, | 95 | source, |
| 84 | kind: gix_ignore::Kind::Expendable, | 96 | kind: gix_ignore::Kind::Expendable, |
| 85 | }), | 97 | }), |
| 86 | Some((expected_source, line, _expected_pattern)), | 98 | Some((expected_source, line, _expected_pattern)), |
| 87 | ) => { | 99 | ) => { |
| 88 | assert_eq!(sequence_number, line, "our counting should match the one used in git"); | 100 | assert_eq!(sequence_number, line, "our counting should match the one used in git"); |
| 89 | assert_eq!( | 101 | assert_eq!( |
| 90 | source.map(|p| p.canonicalize().unwrap()), | 102 | source.map(|p| p.canonicalize().unwrap()), |
| 91 | Some(repo_dir.join(expected_source.to_str_lossy().as_ref()).canonicalize()?) | 103 | Some(repo_dir.join(expected_source.to_str_lossy().as_ref()).canonicalize()?) |
| 92 | ); | 104 | ); |
| 93 | } | 105 | } |
| 94 | (None, None) => {} | 106 | (None, None) => {} |
| 95 | (actual, expected) => { | 107 | (actual, expected) => { |
| 96 | panic!("{case:?}: actual {actual:?} should match {expected:?} with path '{path}'") | 108 | panic!("{repo_name}: {case:?}: actual {actual:?} should match {expected:?} with path '{path}'") |
| 109 | } | ||
| 97 | } | 110 | } |
| 98 | } | 111 | } |
| 99 | } | 112 | } |
| @@ -64,7 +64,7 @@ mod message { · 63 unmodified lines | |||
| 64 | out.extend_from_slice(host.as_bytes()); | 64 | out.extend_from_slice(host.as_bytes()); |
| 65 | if let Some(port) = port { | 65 | if let Some(port) = port { |
| 66 | out.push_byte(b':'); | 66 | out.push_byte(b':'); |
| 67 | out.push_str(&format!("{port}")); | 67 | out.push_str(format!("{port}")); |
| 68 | } | 68 | } |
| 69 | out.push(0); | 69 | out.push(0); |
| 70 | } | 70 | } |
| @@ -193,10 +193,7 @@ impl crate::Repository { · 192 unmodified lines | |||
| 193 | remote_name | 193 | remote_name |
| 194 | .and_then(|name| { | 194 | .and_then(|name| { |
| 195 | config | 195 | config |
| 196 | .string_filter( | 196 | .string_filter(format!("remote.{}.{}", name, Remote::PROXY.name), &mut trusted_only) |
| 197 | &format!("remote.{}.{}", name, Remote::PROXY.name), | ||
| 198 | &mut trusted_only, | ||
| 199 | ) | ||
| 200 | .map(|v| (v, Cow::Owned(format!("remote.{name}.proxy").into()), &Remote::PROXY)) | 197 | .map(|v| (v, Cow::Owned(format!("remote.{name}.proxy").into()), &Remote::PROXY)) |
| 201 | }) | 198 | }) |
| 202 | .or_else(|| { | 199 | .or_else(|| { |
| @@ -254,7 +251,7 @@ impl crate::Repository { · 51 unmodified lines | |||
| 254 | remote_name | 251 | remote_name |
| 255 | .and_then(|name| { | 252 | .and_then(|name| { |
| 256 | config | 253 | config |
| 257 | .string_filter(&format!("remote.{name}.proxyAuthMethod"), &mut trusted_only) | 254 | .string_filter(format!("remote.{name}.proxyAuthMethod"), &mut trusted_only) |
| 258 | .map(|v| { | 255 | .map(|v| { |
| 259 | ( | 256 | ( |
| 260 | v, | 257 | v, |
| @@ -137,7 +137,7 @@ impl crate::Repository { · 136 unmodified lines | |||
| 137 | let mut config_url = |key: &'static config::tree::keys::Url, kind: &'static str| { | 137 | let mut config_url = |key: &'static config::tree::keys::Url, kind: &'static str| { |
| 138 | self.config | 138 | self.config |
| 139 | .resolved | 139 | .resolved |
| 140 | .string_filter(&format!("remote.{}.{}", name_or_url, key.name), &mut filter) | 140 | .string_filter(format!("remote.{}.{}", name_or_url, key.name), &mut filter) |
| 141 | .map(|url| { | 141 | .map(|url| { |
| 142 | key.try_into_url(url).map_err(|err| find::Error::Url { | 142 | key.try_into_url(url).map_err(|err| find::Error::Url { |
| 143 | kind, | 143 | kind, |
| @@ -151,7 +151,7 @@ impl crate::Repository { · 7 unmodified lines | |||
| 151 | let config = &self.config.resolved; | 151 | let config = &self.config.resolved; |
| 152 | 152 | ||
| 153 | let fetch_specs = config | 153 | let fetch_specs = config |
| 154 | .strings_filter(&format!("remote.{}.{}", name_or_url, "fetch"), &mut filter) | 154 | .strings_filter(format!("remote.{}.{}", name_or_url, "fetch"), &mut filter) |
| 155 | .map(|specs| { | 155 | .map(|specs| { |
| 156 | config_spec( | 156 | config_spec( |
| 157 | specs, | 157 | specs, |
| @@ -161,7 +161,7 @@ impl crate::Repository { · 3 unmodified lines | |||
| 161 | ) | 161 | ) |
| 162 | }); | 162 | }); |
| 163 | let push_specs = config | 163 | let push_specs = config |
| 164 | .strings_filter(&format!("remote.{}.{}", name_or_url, "push"), &mut filter) | 164 | .strings_filter(format!("remote.{}.{}", name_or_url, "push"), &mut filter) |
| 165 | .map(|specs| { | 165 | .map(|specs| { |
| 166 | config_spec( | 166 | config_spec( |
| 167 | specs, | 167 | specs, |
| @@ -171,7 +171,7 @@ impl crate::Repository { · 3 unmodified lines | |||
| 171 | ) | 171 | ) |
| 172 | }); | 172 | }); |
| 173 | let fetch_tags = config | 173 | let fetch_tags = config |
| 174 | .string_filter(&format!("remote.{}.{}", name_or_url, "tagOpt"), &mut filter) | 174 | .string_filter(format!("remote.{}.{}", name_or_url, "tagOpt"), &mut filter) |
| 175 | .map(|value| { | 175 | .map(|value| { |
| 176 | config::tree::Remote::TAG_OPT | 176 | config::tree::Remote::TAG_OPT |
| 177 | .try_into_tag_opt(value) | 177 | .try_into_tag_opt(value) |
| @@ -70,6 +70,16 @@ mod ref_map; · 69 unmodified lines | |||
| 70 | mod save; | 70 | mod save; |
| 71 | mod name { | 71 | mod name { |
| 72 | 72 | ||
| 73 | #[test] | ||
| 74 | fn origin_is_valid() { | ||
| 75 | assert!(gix::remote::name::validated("origin").is_ok()); | ||
| 76 | } | ||
| 77 | |||
| 78 | #[test] | ||
| 79 | fn multiple_slashes_are_valid() { | ||
| 80 | assert!(gix::remote::name::validated("origin/another").is_ok()); | ||
| 81 | } | ||
| 82 | |||
| 73 | #[test] | 83 | #[test] |
| 74 | fn empty_is_invalid() { | 84 | fn empty_is_invalid() { |
| 75 | assert!(gix::remote::name::validated("").is_err()); | 85 | assert!(gix::remote::name::validated("").is_err()); |