Unified · Split
| @@ -6,7 +6,8 @@ · 5 unmodified lines | ||
| 6 | 6 | doc = ::document_features::document_features!() |
| 7 | 7 | )] |
| 8 | 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 | 12 | pub use gix_glob as glob; |
| 12 | 13 | use kstring::{KString, KStringRef}; |
| @@ -1,8 +1,7 @@ | ||
| 1 | +use crate::{Name, NameRef}; | |
| 1 | 2 | use bstr::{BStr, BString, ByteSlice}; |
| 2 | 3 | use kstring::KStringRef; |
| 3 | 4 | |
| 4 | −use crate::{Name, NameRef}; | |
| 5 | − | |
| 6 | 5 | impl<'a> NameRef<'a> { |
| 7 | 6 | /// Turn this ref into its owned counterpart. |
| 8 | 7 | pub fn to_owned(self) -> Name { |
| @@ -1,10 +1,9 @@ | ||
| 1 | 1 | use std::borrow::Cow; |
| 2 | 2 | |
| 3 | +use crate::{name, AssignmentRef, Name, NameRef, StateRef}; | |
| 3 | 4 | use bstr::{BStr, ByteSlice}; |
| 4 | 5 | use kstring::KStringRef; |
| 5 | 6 | |
| 6 | −use crate::{name, AssignmentRef, Name, NameRef, StateRef}; | |
| 7 | − | |
| 8 | 7 | /// The kind of attribute that was parsed. |
| 9 | 8 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] |
| 10 | 9 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| @@ -1,7 +1,6 @@ | ||
| 1 | −use std::collections::HashMap; | |
| 2 | − | |
| 3 | 1 | use kstring::KString; |
| 4 | 2 | use smallvec::SmallVec; |
| 3 | +use std::collections::HashMap; | |
| 5 | 4 | |
| 6 | 5 | use crate::{Assignment, AssignmentRef}; |
| 7 | 6 | |
| @@ -1,29 +1,24 @@ | ||
| 1 | −use bstr::{BStr, ByteSlice}; | |
| 2 | −use kstring::{KString, KStringRef}; | |
| 3 | − | |
| 4 | 1 | use crate::{State, StateRef}; |
| 2 | +use bstr::{BStr, BString, ByteSlice}; | |
| 5 | 3 | |
| 6 | 4 | /// A container to encapsulate a tightly packed and typically unallocated byte value that isn't necessarily UTF8 encoded. |
| 7 | 5 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] |
| 8 | 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 | 12 | /// A reference container to encapsulate a tightly packed and typically unallocated byte value that isn't necessarily UTF8 encoded. |
| 12 | 13 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] |
| 13 | 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 | 17 | /// Lifecycle |
| 17 | 18 | impl<'a> ValueRef<'a> { |
| 18 | 19 | /// Keep `input` as our value. |
| 19 | 20 | pub fn from_bytes(input: &'a [u8]) -> Self { |
| 20 | − Self(KStringRef::from_ref( | |
| 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 | − )) | |
| 21 | + Self(input) | |
| 27 | 22 | } |
| 28 | 23 | } |
| 29 | 24 | |
| @@ -42,7 +37,7 @@ impl ValueRef<'_> { · 12 unmodified lines | ||
| 42 | 37 | |
| 43 | 38 | impl<'a> From<&'a str> for ValueRef<'a> { |
| 44 | 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 | 50 | impl From<&str> for Value { |
| 56 | 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 | 1 | use bstr::BString; |
| 2 | +use gix_attributes::state::ValueRef; | |
| 2 | 3 | use gix_attributes::{parse, StateRef}; |
| 3 | 4 | use gix_glob::pattern::Mode; |
| 4 | 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 | 292 | #[test] |
| 279 | 293 | fn attributes_see_state_adjustments_over_value_assignments() { |
| 280 | 294 | assert_eq!( |
| @@ -325,6 +339,10 @@ fn value<'b>(attr: &str, value: &'b str) -> (BString, StateRef<'b>) { · 44 unmodified lines | ||
| 325 | 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 | 346 | fn pattern(name: &str, flags: gix_glob::pattern::Mode, first_wildcard_pos: Option<usize>) -> parse::Kind { |
| 329 | 347 | parse::Kind::Pattern(gix_glob::Pattern { |
| 330 | 348 | text: name.into(), |
| @@ -344,6 +362,17 @@ fn line(input: &str) -> ExpandedAttribute { · 13 unmodified lines | ||
| 344 | 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 | 376 | fn lenient_lines(input: &str) -> Vec<ExpandedAttribute> { |
| 348 | 377 | gix_attributes::parse(input.as_bytes()) |
| 349 | 378 | .map(expand) |
| @@ -270,7 +270,7 @@ fn given_attributes_are_made_available_in_given_order() -> crate::Result { · 269 unmodified lines | ||
| 270 | 270 | fn size_of_outcome() { |
| 271 | 271 | assert_eq!( |
| 272 | 272 | std::mem::size_of::<Outcome>(), |
| 273 | − 904, | |
| 273 | + 840, | |
| 274 | 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 | 169 | .map(|platform| platform.excluded_kind()) |
| 170 | 170 | }) |
| 171 | 171 | .map_err(Error::ExcludesAccess)? |
| 172 | + .filter(|_| filename_start_idx > 0) | |
| 172 | 173 | { |
| 173 | 174 | out.status = entry::Status::Ignored(excluded); |
| 174 | 175 | } |
| @@ -256,6 +257,7 @@ pub fn path( · 81 unmodified lines | ||
| 256 | 257 | if let Some(excluded) = ctx |
| 257 | 258 | .excludes |
| 258 | 259 | .as_mut() |
| 260 | + .filter(|_| !rela_path.is_empty()) | |
| 259 | 261 | .map_or(Ok(None), |stack| { |
| 260 | 262 | stack |
| 261 | 263 | .at_entry(rela_path.as_bstr(), is_dir, ctx.objects) |
| @@ -351,3 +351,83 @@ git clone submodule multiple-submodules · 350 unmodified lines | ||
| 351 | 351 | git submodule add ../submodule a/b |
| 352 | 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 | 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 | 15 | z/x |
| 16 | 16 | EOF |
| 17 | 17 | |
| 18 | −mkdir repo; | |
| 18 | +git init -q repo; | |
| 19 | 19 | (cd repo |
| 20 | − git init -q | |
| 21 | 20 | git config core.excludesFile ../user.exclude |
| 22 | 21 | |
| 23 | 22 | cat <<EOF >.git/info/exclude |
| @@ -93,3 +92,70 @@ E/f · 69 unmodified lines | ||
| 93 | 92 | E/F |
| 94 | 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 | 1 | use bstr::{BStr, ByteSlice}; |
| 4 | 2 | use gix_glob::pattern::Case; |
| 5 | 3 | use gix_ignore::search::Match; |
| @@ -31,69 +29,84 @@ impl<'a> Iterator for Expectations<'a> { · 25 unmodified lines | ||
| 31 | 29 | |
| 32 | 30 | #[test] |
| 33 | 31 | fn baseline_from_git_dir() -> crate::Result { |
| 34 | − let case = if gix_fs::Capabilities::probe("../.git".as_ref()).ignore_case { | |
| 35 | − Case::Fold | |
| 36 | − } else { | |
| 37 | − Case::Sensitive | |
| 38 | − }; | |
| 39 | − let dir = gix_testtools::scripted_fixture_read_only("make_global_and_external_and_dir_ignores.sh")?; | |
| 40 | − let repo_dir = dir.join("repo"); | |
| 41 | − let git_dir = repo_dir.join(".git"); | |
| 42 | − let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; | |
| 43 | − let mut buf = Vec::new(); | |
| 44 | − let mut group = gix_ignore::Search::from_git_dir(&git_dir, Some(dir.join("user.exclude")), &mut buf)?; | |
| 32 | + for repo_name in [ | |
| 33 | + "repo", | |
| 34 | + "slash-and-excludes", | |
| 35 | + "star-and-excludes-in-subdir", | |
| 36 | + "slash-and-excludes-in-subdir", | |
| 37 | + ] { | |
| 38 | + let case = if gix_fs::Capabilities::probe("../.git".as_ref()).ignore_case { | |
| 39 | + Case::Fold | |
| 40 | + } else { | |
| 41 | + Case::Sensitive | |
| 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!( | |
| 47 | − !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" | |
| 49 | − ); | |
| 50 | − assert!( | |
| 51 | − gix_glob::search::add_patterns_file( | |
| 52 | − &mut group.patterns, | |
| 53 | − repo_dir.join(".gitignore"), | |
| 54 | − true, | |
| 55 | − repo_dir.as_path().into(), | |
| 56 | − &mut buf | |
| 57 | − )?, | |
| 58 | − "existing files return true" | |
| 59 | − ); | |
| 52 | + assert!( | |
| 53 | + !gix_glob::search::add_patterns_file(&mut group.patterns, "not-a-file".into(), false, None, &mut buf)?, | |
| 54 | + "missing files are no problem and cause a negative response" | |
| 55 | + ); | |
| 56 | + let mut ignore_file = repo_dir.join(".gitignore"); | |
| 57 | + if !ignore_file.is_file() { | |
| 58 | + ignore_file.pop(); | |
| 59 | + ignore_file.push("sub/.gitignore"); | |
| 60 | + } | |
| 61 | + assert!( | |
| 62 | + gix_glob::search::add_patterns_file( | |
| 63 | + &mut group.patterns, | |
| 64 | + ignore_file, | |
| 65 | + true, | |
| 66 | + repo_dir.as_path().into(), | |
| 67 | + &mut buf | |
| 68 | + )?, | |
| 69 | + "existing files return true" | |
| 70 | + ); | |
| 60 | 71 | |
| 61 | − buf.clear(); | |
| 62 | − let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore"); | |
| 63 | − std::fs::File::open(&ignore_file)?.read_to_end(&mut buf)?; | |
| 64 | − group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into()); | |
| 72 | + let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore"); | |
| 73 | + if ignore_file.is_file() { | |
| 74 | + let buf = std::fs::read(&ignore_file)?; | |
| 75 | + group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into()); | |
| 76 | + } | |
| 65 | 77 | |
| 66 | − for (path, source_and_line) in (Expectations { | |
| 67 | − lines: baseline.lines(), | |
| 68 | − }) { | |
| 69 | − let actual = group.pattern_matching_relative_path( | |
| 70 | − path, | |
| 71 | − repo_dir | |
| 72 | − .join(path.to_str_lossy().as_ref()) | |
| 73 | − .metadata() | |
| 74 | − .ok() | |
| 75 | − .map(|m| m.is_dir()), | |
| 76 | − case, | |
| 77 | − ); | |
| 78 | − match (actual, source_and_line) { | |
| 79 | − ( | |
| 80 | − Some(Match { | |
| 81 | − sequence_number, | |
| 82 | − pattern: _, | |
| 83 | − source, | |
| 84 | − kind: gix_ignore::Kind::Expendable, | |
| 85 | − }), | |
| 86 | − Some((expected_source, line, _expected_pattern)), | |
| 87 | − ) => { | |
| 88 | − assert_eq!(sequence_number, line, "our counting should match the one used in git"); | |
| 89 | − assert_eq!( | |
| 90 | − source.map(|p| p.canonicalize().unwrap()), | |
| 91 | − Some(repo_dir.join(expected_source.to_str_lossy().as_ref()).canonicalize()?) | |
| 92 | − ); | |
| 93 | − } | |
| 94 | − (None, None) => {} | |
| 95 | − (actual, expected) => { | |
| 96 | − panic!("{case:?}: actual {actual:?} should match {expected:?} with path '{path}'") | |
| 78 | + for (path, source_and_line) in (Expectations { | |
| 79 | + lines: baseline.lines(), | |
| 80 | + }) { | |
| 81 | + let actual = group.pattern_matching_relative_path( | |
| 82 | + path, | |
| 83 | + repo_dir | |
| 84 | + .join(path.to_str_lossy().as_ref()) | |
| 85 | + .metadata() | |
| 86 | + .ok() | |
| 87 | + .map(|m| m.is_dir()), | |
| 88 | + case, | |
| 89 | + ); | |
| 90 | + match (actual, source_and_line) { | |
| 91 | + ( | |
| 92 | + Some(Match { | |
| 93 | + sequence_number, | |
| 94 | + pattern: _, | |
| 95 | + source, | |
| 96 | + kind: gix_ignore::Kind::Expendable, | |
| 97 | + }), | |
| 98 | + Some((expected_source, line, _expected_pattern)), | |
| 99 | + ) => { | |
| 100 | + assert_eq!(sequence_number, line, "our counting should match the one used in git"); | |
| 101 | + assert_eq!( | |
| 102 | + source.map(|p| p.canonicalize().unwrap()), | |
| 103 | + Some(repo_dir.join(expected_source.to_str_lossy().as_ref()).canonicalize()?) | |
| 104 | + ); | |
| 105 | + } | |
| 106 | + (None, None) => {} | |
| 107 | + (actual, expected) => { | |
| 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 | 64 | out.extend_from_slice(host.as_bytes()); |
| 65 | 65 | if let Some(port) = port { |
| 66 | 66 | out.push_byte(b':'); |
| 67 | − out.push_str(&format!("{port}")); | |
| 67 | + out.push_str(format!("{port}")); | |
| 68 | 68 | } |
| 69 | 69 | out.push(0); |
| 70 | 70 | } |
| @@ -193,10 +193,7 @@ impl crate::Repository { · 192 unmodified lines | ||
| 193 | 193 | remote_name |
| 194 | 194 | .and_then(|name| { |
| 195 | 195 | config |
| 196 | − .string_filter( | |
| 197 | − &format!("remote.{}.{}", name, Remote::PROXY.name), | |
| 198 | − &mut trusted_only, | |
| 199 | − ) | |
| 196 | + .string_filter(format!("remote.{}.{}", name, Remote::PROXY.name), &mut trusted_only) | |
| 200 | 197 | .map(|v| (v, Cow::Owned(format!("remote.{name}.proxy").into()), &Remote::PROXY)) |
| 201 | 198 | }) |
| 202 | 199 | .or_else(|| { |
| @@ -254,7 +251,7 @@ impl crate::Repository { · 51 unmodified lines | ||
| 254 | 251 | remote_name |
| 255 | 252 | .and_then(|name| { |
| 256 | 253 | config |
| 257 | − .string_filter(&format!("remote.{name}.proxyAuthMethod"), &mut trusted_only) | |
| 254 | + .string_filter(format!("remote.{name}.proxyAuthMethod"), &mut trusted_only) | |
| 258 | 255 | .map(|v| { |
| 259 | 256 | ( |
| 260 | 257 | v, |
| @@ -137,7 +137,7 @@ impl crate::Repository { · 136 unmodified lines | ||
| 137 | 137 | let mut config_url = |key: &'static config::tree::keys::Url, kind: &'static str| { |
| 138 | 138 | self.config |
| 139 | 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 | 141 | .map(|url| { |
| 142 | 142 | key.try_into_url(url).map_err(|err| find::Error::Url { |
| 143 | 143 | kind, |
| @@ -151,7 +151,7 @@ impl crate::Repository { · 7 unmodified lines | ||
| 151 | 151 | let config = &self.config.resolved; |
| 152 | 152 | |
| 153 | 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 | 155 | .map(|specs| { |
| 156 | 156 | config_spec( |
| 157 | 157 | specs, |
| @@ -161,7 +161,7 @@ impl crate::Repository { · 3 unmodified lines | ||
| 161 | 161 | ) |
| 162 | 162 | }); |
| 163 | 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 | 165 | .map(|specs| { |
| 166 | 166 | config_spec( |
| 167 | 167 | specs, |
| @@ -171,7 +171,7 @@ impl crate::Repository { · 3 unmodified lines | ||
| 171 | 171 | ) |
| 172 | 172 | }); |
| 173 | 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 | 175 | .map(|value| { |
| 176 | 176 | config::tree::Remote::TAG_OPT |
| 177 | 177 | .try_into_tag_opt(value) |
| @@ -70,6 +70,16 @@ mod ref_map; · 69 unmodified lines | ||
| 70 | 70 | mod save; |
| 71 | 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 | 83 | #[test] |
| 74 | 84 | fn empty_is_invalid() { |
| 75 | 85 | assert!(gix::remote::name::validated("").is_err()); |