gix-dir-0.11.0/.cargo_vcs_info.json 0000644 00000000145 00000000001 0012456 0 ustar {
"git": {
"sha1": "beb0ea8c4ff94c64b7773772a9d388ccb403f3c1"
},
"path_in_vcs": "gix-dir"
} gix-dir-0.11.0/CHANGELOG.md 0000644 0000000 0000000 00000122563 10461020230 0013070 0 ustar 0000000 0000000 # 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.11.0 (2024-12-22)
### Other
- Clarify and expand descriptions of `NonFile`s
Discussed in:
https://github.com/GitoxideLabs/gitoxide/pull/1730#discussion_r1894381449
At least for now, they remain called `NonFile`s (and sometimes
referred to as "non-files" in text), but more specifically defined.
### Bug Fixes (BREAKING)
- rename `entry::Kind::NonFile` to `entry::Kind::Untrackable`.
- Add `entry::Kind::NonFile`.
Previously, these were misclassified as `File`, which can lead to blocking applications
which get stuck reading pipes.
Now the downstream is forced to deal with the possibility that the item at hand isn't a file,
to do application-specific things.
- assure `fifo` or non-files aren't considered files, but are pruned instead.
That way, algorithms relying on dirwalking can still see them if they want to,
but would have a hard time to use them (accidentally).
Note that this replaces the `From` implementation with `entry::Kind::try_from_file_type()`,
which makes this a breaking change.`
### Commit Statistics
- 13 commits contributed to the release over the course of 28 calendar days.
- 28 days passed between releases.
- 4 commits were understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' were seen in commit messages
### Commit Details
view details
* **Uncategorized**
- Release gix-date v0.9.3, gix-object v0.46.1, gix-command v0.4.0, gix-filter v0.16.0, gix-fs v0.12.1, gix-traverse v0.43.1, gix-worktree-stream v0.18.0, gix-archive v0.18.0, gix-ref v0.49.1, gix-prompt v0.9.0, gix-url v0.28.2, gix-credentials v0.26.0, gix-diff v0.49.0, gix-dir v0.11.0, gix-revision v0.31.1, gix-merge v0.2.0, gix-pack v0.56.0, gix-odb v0.66.0, gix-shallow v0.1.0, gix-packetline v0.18.2, gix-transport v0.44.0, gix-protocol v0.47.0, gix-status v0.16.0, gix-worktree-state v0.16.0, gix v0.69.0, gitoxide-core v0.44.0, gitoxide v0.40.0, safety bump 16 crates ([`c1ba571`](https://github.com/GitoxideLabs/gitoxide/commit/c1ba5719132227410abefeb54e3032b015233e94))
- Update changelogs prior to release ([`7ea8582`](https://github.com/GitoxideLabs/gitoxide/commit/7ea85821c6999e3e6cf50a2a009904e9c38642a4))
- Merge pull request #1734 from EliahKagan/nonfiles ([`ad6b9b6`](https://github.com/GitoxideLabs/gitoxide/commit/ad6b9b66aa3e3561e413d04d00f6dbf832d63353))
- Reword "non-files" in documentation comments ([`154b21f`](https://github.com/GitoxideLabs/gitoxide/commit/154b21f0e9beb0e5b6615f091f5f0716df6a3f7b))
- Rename `entry::Kind::NonFile` to `entry::Kind::Untrackable`. ([`d90412b`](https://github.com/GitoxideLabs/gitoxide/commit/d90412bdf86c70ecb5a6d0a1fefae875b3eb836b))
- Clarify and expand descriptions of `NonFile`s ([`c06a57e`](https://github.com/GitoxideLabs/gitoxide/commit/c06a57e698ee21d6c6d8a35bbb37323ceef1e143))
- Merge pull request #1730 from GitoxideLabs/fix-1729 ([`6822689`](https://github.com/GitoxideLabs/gitoxide/commit/6822689fca04c15e309f9ca41d610bca9cb93e3b))
- Add `entry::Kind::NonFile`. ([`0d51771`](https://github.com/GitoxideLabs/gitoxide/commit/0d517716b81e203a131552c5163894a4fbf2863e))
- Merge pull request #1727 from GitoxideLabs/dirwalk-ignore-non-regulars ([`69ee6a3`](https://github.com/GitoxideLabs/gitoxide/commit/69ee6a32dd221a1aae7b8c3817f90feacf577598))
- Assure `fifo` or non-files aren't considered files, but are pruned instead. ([`f7ffb91`](https://github.com/GitoxideLabs/gitoxide/commit/f7ffb9183f918a9521a8f21d3cc0c70925d4b0f1))
- Add a test to show what FIFO does to the dirwalk. ([`949fe2c`](https://github.com/GitoxideLabs/gitoxide/commit/949fe2c4da0a74a0c45cfa8cf820f3ce7a808cda))
- Refactor test-structure to match the 'new style'. ([`e6199a5`](https://github.com/GitoxideLabs/gitoxide/commit/e6199a5819d2567e9549b16020d69bfc55057dc7))
- Merge pull request #1701 from GitoxideLabs/release ([`e8b3b41`](https://github.com/GitoxideLabs/gitoxide/commit/e8b3b41dd79b8f4567670b1f89dd8867b6134e9e))
## 0.10.0 (2024-11-24)
A maintenance release without user-facing changes.
### Commit Statistics
- 10 commits contributed to the release.
- 0 commits were understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' were seen in commit messages
### Commit Details
view details
* **Uncategorized**
- Release gix-glob v0.17.1, gix-command v0.3.11, gix-filter v0.15.0, gix-chunk v0.4.10, gix-commitgraph v0.25.1, gix-revwalk v0.17.0, gix-traverse v0.43.0, gix-worktree-stream v0.17.0, gix-archive v0.17.0, gix-config-value v0.14.10, gix-lock v15.0.1, gix-ref v0.49.0, gix-sec v0.10.10, gix-config v0.42.0, gix-prompt v0.8.9, gix-url v0.28.1, gix-credentials v0.25.1, gix-ignore v0.12.1, gix-bitmap v0.2.13, gix-index v0.37.0, gix-worktree v0.38.0, gix-diff v0.48.0, gix-discover v0.37.0, gix-pathspec v0.8.1, gix-dir v0.10.0, gix-mailmap v0.25.1, gix-revision v0.31.0, gix-merge v0.1.0, gix-negotiate v0.17.0, gix-pack v0.55.0, gix-odb v0.65.0, gix-packetline v0.18.1, gix-transport v0.43.1, gix-protocol v0.46.1, gix-refspec v0.27.0, gix-status v0.15.0, gix-submodule v0.16.0, gix-worktree-state v0.15.0, gix v0.68.0, gix-fsck v0.8.0, gitoxide-core v0.43.0, gitoxide v0.39.0 ([`4000197`](https://github.com/GitoxideLabs/gitoxide/commit/4000197ecc8cf1a5d79361620e4c114f86476703))
- Release gix-date v0.9.2, gix-actor v0.33.1, gix-hash v0.15.1, gix-features v0.39.1, gix-validate v0.9.2, gix-object v0.46.0, gix-path v0.10.13, gix-quote v0.4.14, gix-attributes v0.23.1, gix-packetline-blocking v0.18.1, gix-filter v0.15.0, gix-chunk v0.4.10, gix-commitgraph v0.25.1, gix-revwalk v0.17.0, gix-traverse v0.43.0, gix-worktree-stream v0.17.0, gix-archive v0.17.0, gix-config-value v0.14.10, gix-lock v15.0.1, gix-ref v0.49.0, gix-config v0.42.0, gix-prompt v0.8.9, gix-url v0.28.1, gix-credentials v0.25.1, gix-bitmap v0.2.13, gix-index v0.37.0, gix-worktree v0.38.0, gix-diff v0.48.0, gix-discover v0.37.0, gix-pathspec v0.8.1, gix-dir v0.10.0, gix-mailmap v0.25.1, gix-revision v0.31.0, gix-merge v0.1.0, gix-negotiate v0.17.0, gix-pack v0.55.0, gix-odb v0.65.0, gix-packetline v0.18.1, gix-transport v0.43.1, gix-protocol v0.46.1, gix-refspec v0.27.0, gix-status v0.15.0, gix-submodule v0.16.0, gix-worktree-state v0.15.0, gix v0.68.0, gix-fsck v0.8.0, gitoxide-core v0.43.0, gitoxide v0.39.0, safety bump 25 crates ([`8ce4912`](https://github.com/GitoxideLabs/gitoxide/commit/8ce49129a75e21346ceedf7d5f87fa3a34b024e1))
- Prepare changelogs prior to release ([`bc9d994`](https://github.com/GitoxideLabs/gitoxide/commit/bc9d9943e8499a76fc47a05b63ac5c684187d1ae))
- Merge pull request #1662 from paolobarbolini/thiserror-v2 ([`7a40648`](https://github.com/GitoxideLabs/gitoxide/commit/7a406481b072728cec089d7c05364f9dbba335a2))
- Upgrade thiserror to v2.0.0 ([`0f0e4fe`](https://github.com/GitoxideLabs/gitoxide/commit/0f0e4fe121932a8a6302cf950b3caa4c8608fb61))
- Merge pull request #1652 from EliahKagan/run-ci/chmod ([`8e99eba`](https://github.com/GitoxideLabs/gitoxide/commit/8e99eba2a284b35b5e9bcb97e47bfbbafc3df5d1))
- Enable gix-dir walk symlink tests even on Windows ([`d03971b`](https://github.com/GitoxideLabs/gitoxide/commit/d03971b27f5b4fffc009329edb05f8ac0baac1d8))
- Merge pull request #1646 from EliahKagan/run-ci/portable-rm ([`35d4d9f`](https://github.com/GitoxideLabs/gitoxide/commit/35d4d9f801fa60875ebcd6102d874522be861e18))
- Avoid `cp -v` in fixtures for portability ([`dffb694`](https://github.com/GitoxideLabs/gitoxide/commit/dffb6945508023eb87c14c79ba59bb23723193ff))
- Merge pull request #1642 from GitoxideLabs/new-release ([`db5c9cf`](https://github.com/GitoxideLabs/gitoxide/commit/db5c9cfce93713b4b3e249cff1f8cc1ef146f470))
## 0.9.0 (2024-10-22)
### Other
- Update gitoxide repository URLs
This updates `Byron/gitoxide` URLs to `GitoxideLabs/gitoxide` in:
- Markdown documentation, except changelogs and other such files
where such changes should not be made.
- Documentation comments (in .rs files).
- Manifest (.toml) files, for the value of the `repository` key.
- The comments appearing at the top of a sample hook that contains
a repository URL as an example.
When making these changes, I also allowed my editor to remove
trailing whitespace in any lines in files already being edited
(since, in this case, there was no disadvantage to allowing this).
The gitoxide repository URL changed when the repository was moved
into the recently created GitHub organization `GitoxideLabs`, as
detailed in #1406. Please note that, although I believe updating
the URLs to their new canonical values is useful, this is not
needed to fix any broken links, since `Byron/gitoxide` URLs
redirect (and hopefully will always redirect) to the coresponding
`GitoxideLabs/gitoxide` URLs.
While this change should not break any URLs, some affected URLs
were already broken. This updates them, but they are still broken.
They will be fixed in a subsequent commit.
This also does not update `Byron/gitoxide` URLs in test fixtures
or test cases, nor in the `Makefile`. (It may make sense to change
some of those too, but it is not really a documentation change.)
### Commit Statistics
- 13 commits contributed to the release.
- 60 days passed between releases.
- 1 commit was understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' were seen in commit messages
### Thanks Clippy
[Clippy](https://github.com/rust-lang/rust-clippy) helped 1 time to make code idiomatic.
### Commit Details
view details
* **Uncategorized**
- Release gix-date v0.9.1, gix-utils v0.1.13, gix-actor v0.33.0, gix-hash v0.15.0, gix-trace v0.1.11, gix-features v0.39.0, gix-hashtable v0.6.0, gix-validate v0.9.1, gix-object v0.45.0, gix-path v0.10.12, gix-glob v0.17.0, gix-quote v0.4.13, gix-attributes v0.23.0, gix-command v0.3.10, gix-packetline-blocking v0.18.0, gix-filter v0.14.0, gix-fs v0.12.0, gix-chunk v0.4.9, gix-commitgraph v0.25.0, gix-revwalk v0.16.0, gix-traverse v0.42.0, gix-worktree-stream v0.16.0, gix-archive v0.16.0, gix-config-value v0.14.9, gix-tempfile v15.0.0, gix-lock v15.0.0, gix-ref v0.48.0, gix-sec v0.10.9, gix-config v0.41.0, gix-prompt v0.8.8, gix-url v0.28.0, gix-credentials v0.25.0, gix-ignore v0.12.0, gix-bitmap v0.2.12, gix-index v0.36.0, gix-worktree v0.37.0, gix-diff v0.47.0, gix-discover v0.36.0, gix-pathspec v0.8.0, gix-dir v0.9.0, gix-mailmap v0.25.0, gix-merge v0.0.0, gix-negotiate v0.16.0, gix-pack v0.54.0, gix-odb v0.64.0, gix-packetline v0.18.0, gix-transport v0.43.0, gix-protocol v0.46.0, gix-revision v0.30.0, gix-refspec v0.26.0, gix-status v0.14.0, gix-submodule v0.15.0, gix-worktree-state v0.14.0, gix v0.67.0, gix-fsck v0.7.0, gitoxide-core v0.42.0, gitoxide v0.38.0, safety bump 41 crates ([`3f7e8ee`](https://github.com/GitoxideLabs/gitoxide/commit/3f7e8ee2c5107aec009eada1a05af7941da9cb4d))
- Merge pull request #1624 from EliahKagan/update-repo-url ([`795962b`](https://github.com/GitoxideLabs/gitoxide/commit/795962b107d86f58b1f7c75006da256d19cc80ad))
- Update gitoxide repository URLs ([`64ff0a7`](https://github.com/GitoxideLabs/gitoxide/commit/64ff0a77062d35add1a2dd422bb61075647d1a36))
- Merge pull request #1612 from Byron/merge ([`37c1e4c`](https://github.com/GitoxideLabs/gitoxide/commit/37c1e4c919382c9d213bd5ca299ed659d63ab45d))
- Thanks clippy ([`af03832`](https://github.com/GitoxideLabs/gitoxide/commit/af0383254422b70d53f27572c415eea2e4154447))
- Merge pull request #1582 from Byron/gix-path-release ([`93e86f1`](https://github.com/GitoxideLabs/gitoxide/commit/93e86f12a8d0ab59ad5d885ce552d0dec9a6fba6))
- Release gix-trace v0.1.10, gix-path v0.10.11 ([`012a754`](https://github.com/GitoxideLabs/gitoxide/commit/012a75455edebc857ff13c97c1e7603ea5ea6cdc))
- Merge pull request #1557 from Byron/merge-base ([`649f588`](https://github.com/GitoxideLabs/gitoxide/commit/649f5882cbebadf1133fa5f310e09b4aab77217e))
- Allow empty-docs ([`beba720`](https://github.com/GitoxideLabs/gitoxide/commit/beba7204a50a84b30e3eb81413d968920599e226))
- Merge branch 'global-lints' ([`37ba461`](https://github.com/GitoxideLabs/gitoxide/commit/37ba4619396974ec9cc41d1e882ac5efaf3816db))
- Workspace Clippy lint management ([`2e0ce50`](https://github.com/GitoxideLabs/gitoxide/commit/2e0ce506968c112b215ca0056bd2742e7235df48))
- Merge pull request #1546 from nyurik/semilocons ([`f992fb7`](https://github.com/GitoxideLabs/gitoxide/commit/f992fb773b443454015bd14658cfaa2f3ac07997))
- Add missing semicolons ([`ec69c88`](https://github.com/GitoxideLabs/gitoxide/commit/ec69c88fc119f3aa1967a7e7f5fca30e3ce97595))
## 0.8.0 (2024-08-22)
A maintenance release without user-facing changes.
### Commit Statistics
- 2 commits contributed to the release.
- 0 commits were understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' were seen in commit messages
### Commit Details
view details
* **Uncategorized**
- Release gix-actor v0.32.0, gix-object v0.44.0, gix-filter v0.13.0, gix-revwalk v0.15.0, gix-traverse v0.41.0, gix-worktree-stream v0.15.0, gix-archive v0.15.0, gix-ref v0.47.0, gix-config v0.40.0, gix-index v0.35.0, gix-worktree v0.36.0, gix-diff v0.46.0, gix-discover v0.35.0, gix-dir v0.8.0, gix-mailmap v0.24.0, gix-negotiate v0.15.0, gix-pack v0.53.0, gix-odb v0.63.0, gix-revision v0.29.0, gix-refspec v0.25.0, gix-status v0.13.0, gix-submodule v0.14.0, gix-worktree-state v0.13.0, gix v0.66.0, gix-fsck v0.6.0, gitoxide-core v0.41.0, gitoxide v0.38.0, safety bump 26 crates ([`b3ff033`](https://github.com/GitoxideLabs/gitoxide/commit/b3ff033b602f303433f0b2e4daa2dba90b619c9e))
- Prepare changelog prior to (yet another) release ([`209b6de`](https://github.com/GitoxideLabs/gitoxide/commit/209b6de0329dbaaf61b929d32d9d54cf13fe241e))
## 0.7.0 (2024-08-22)
### Bug Fixes
- Assure that worktrees in hidden directories are not deleted
- assure an ignored repository clone is for deletion is recognized as repository
### New Features (BREAKING)
- make it possible to consider worktrees to be 'tracked'
That way it's possibel for them to be equivalent to submodules, which
would never be deleted by accident due to their 'tracked' status.
This works by passing repository-relative paths of worktree locations
that are within this repository.
### Commit Statistics
- 11 commits contributed to the release over the course of 28 calendar days.
- 30 days passed between releases.
- 3 commits were understood as [conventional](https://www.conventionalcommits.org).
- 3 unique issues were worked on: [#1464](https://github.com/GitoxideLabs/gitoxide/issues/1464), [#1469](https://github.com/GitoxideLabs/gitoxide/issues/1469), [#1470](https://github.com/GitoxideLabs/gitoxide/issues/1470)
### Commit Details
view details
* **[#1464](https://github.com/GitoxideLabs/gitoxide/issues/1464)**
- Make it possible to consider worktrees to be 'tracked' ([`c9cd2d2`](https://github.com/GitoxideLabs/gitoxide/commit/c9cd2d258a46ba3dc93559abea9a95405e62f930))
- Assure an ignored repository clone is for deletion is recognized as repository ([`6c8850b`](https://github.com/GitoxideLabs/gitoxide/commit/6c8850b44e288548a81db24974779f3811433fc9))
* **[#1469](https://github.com/GitoxideLabs/gitoxide/issues/1469)**
- Add a test to see what happens if worktrees are hidden in ignored directories ([`2bacc45`](https://github.com/GitoxideLabs/gitoxide/commit/2bacc45f794d300656ca770c803a63c5df8b71fc))
* **[#1470](https://github.com/GitoxideLabs/gitoxide/issues/1470)**
- Assure that worktrees in hidden directories are not deleted ([`31e795a`](https://github.com/GitoxideLabs/gitoxide/commit/31e795abf27dea8fd7dd7f59996cdd5d70398601))
* **Uncategorized**
- Release gix-dir v0.7.0, gix-mailmap v0.23.6, gix-negotiate v0.14.0, gix-pack v0.52.0, gix-odb v0.62.0, gix-packetline v0.17.6, gix-transport v0.42.3, gix-protocol v0.45.3, gix-revision v0.28.0, gix-refspec v0.24.0, gix-status v0.12.0, gix-submodule v0.13.0, gix-worktree-state v0.12.0, gix v0.65.0, gix-fsck v0.5.0, gitoxide-core v0.40.0, gitoxide v0.38.0 ([`4fe330e`](https://github.com/GitoxideLabs/gitoxide/commit/4fe330e68d10d51b0a7116a7ef8b9ea3b48a224c))
- Release gix-attributes v0.22.5, gix-filter v0.12.0, gix-fs v0.11.3, gix-revwalk v0.14.0, gix-traverse v0.40.0, gix-worktree-stream v0.14.0, gix-archive v0.14.0, gix-config-value v0.14.8, gix-tempfile v14.0.2, gix-ref v0.46.0, gix-sec v0.10.8, gix-config v0.39.0, gix-prompt v0.8.7, gix-url v0.27.5, gix-credentials v0.24.5, gix-ignore v0.11.4, gix-index v0.34.0, gix-worktree v0.35.0, gix-diff v0.45.0, gix-discover v0.34.0, gix-pathspec v0.7.7, gix-dir v0.7.0, gix-mailmap v0.23.6, gix-negotiate v0.14.0, gix-pack v0.52.0, gix-odb v0.62.0, gix-packetline v0.17.6, gix-transport v0.42.3, gix-protocol v0.45.3, gix-revision v0.28.0, gix-refspec v0.24.0, gix-status v0.12.0, gix-submodule v0.13.0, gix-worktree-state v0.12.0, gix v0.65.0, gix-fsck v0.5.0, gitoxide-core v0.40.0, gitoxide v0.38.0 ([`f2b522d`](https://github.com/GitoxideLabs/gitoxide/commit/f2b522df2ddad07f065f43c2dbad49aa726714dd))
- Release gix-glob v0.16.5, gix-filter v0.12.0, gix-fs v0.11.3, gix-revwalk v0.14.0, gix-traverse v0.40.0, gix-worktree-stream v0.14.0, gix-archive v0.14.0, gix-config-value v0.14.8, gix-tempfile v14.0.2, gix-ref v0.46.0, gix-sec v0.10.8, gix-config v0.39.0, gix-prompt v0.8.7, gix-url v0.27.5, gix-credentials v0.24.5, gix-ignore v0.11.4, gix-index v0.34.0, gix-worktree v0.35.0, gix-diff v0.45.0, gix-discover v0.34.0, gix-pathspec v0.7.7, gix-dir v0.7.0, gix-mailmap v0.23.6, gix-negotiate v0.14.0, gix-pack v0.52.0, gix-odb v0.62.0, gix-packetline v0.17.6, gix-transport v0.42.3, gix-protocol v0.45.3, gix-revision v0.28.0, gix-refspec v0.24.0, gix-status v0.12.0, gix-submodule v0.13.0, gix-worktree-state v0.12.0, gix v0.65.0, gix-fsck v0.5.0, gitoxide-core v0.40.0, gitoxide v0.38.0 ([`a65a17f`](https://github.com/GitoxideLabs/gitoxide/commit/a65a17fc396ef49663b0a75cf7b5502d370db269))
- Release gix-date v0.9.0, gix-actor v0.31.6, gix-validate v0.9.0, gix-object v0.43.0, gix-path v0.10.10, gix-attributes v0.22.4, gix-command v0.3.9, gix-packetline-blocking v0.17.5, gix-filter v0.12.0, gix-fs v0.11.3, gix-revwalk v0.14.0, gix-traverse v0.40.0, gix-worktree-stream v0.14.0, gix-archive v0.14.0, gix-ref v0.46.0, gix-config v0.39.0, gix-prompt v0.8.7, gix-url v0.27.5, gix-credentials v0.24.5, gix-ignore v0.11.4, gix-index v0.34.0, gix-worktree v0.35.0, gix-diff v0.45.0, gix-discover v0.34.0, gix-dir v0.7.0, gix-mailmap v0.23.6, gix-negotiate v0.14.0, gix-pack v0.52.0, gix-odb v0.62.0, gix-packetline v0.17.6, gix-transport v0.42.3, gix-protocol v0.45.3, gix-revision v0.28.0, gix-refspec v0.24.0, gix-status v0.12.0, gix-submodule v0.13.0, gix-worktree-state v0.12.0, gix v0.65.0, gix-fsck v0.5.0, gitoxide-core v0.40.0, gitoxide v0.38.0, safety bump 25 crates ([`d19af16`](https://github.com/GitoxideLabs/gitoxide/commit/d19af16e1d2031d4f0100e76b6cd410a5d252af1))
- Prepare changelogs prior to release ([`0f25841`](https://github.com/GitoxideLabs/gitoxide/commit/0f2584178ae88e425f1c629eb85b69f3b4310d9f))
- Merge branch 'fix-clean' ([`348b9bf`](https://github.com/GitoxideLabs/gitoxide/commit/348b9bf6cbf7a61b8094e5db8354376abb2e8c99))
- Merge branch 'fix-clean' ([`33eacfb`](https://github.com/GitoxideLabs/gitoxide/commit/33eacfbaace2021043e2b5d72dcb9293af6c4020))
## 0.6.0 (2024-07-23)
### Bug Fixes
- assure that worktree-roots are never considered ignored.
If they were, they would more easily be deleted by tooling like `gix clean`.
### Commit Statistics
- 18 commits contributed to the release over the course of 55 calendar days.
- 62 days passed between releases.
- 1 commit was understood as [conventional](https://www.conventionalcommits.org).
- 1 unique issue was worked on: [#1458](https://github.com/GitoxideLabs/gitoxide/issues/1458)
### Commit Details
view details
* **[#1458](https://github.com/GitoxideLabs/gitoxide/issues/1458)**
- Assure that worktree-roots are never considered ignored. ([`1e92d1e`](https://github.com/GitoxideLabs/gitoxide/commit/1e92d1ed3f23e9de84203252d64e701e2c7fb941))
- Add tests that show what happens with allow-lists and `/` in root ([`1ebd6c7`](https://github.com/GitoxideLabs/gitoxide/commit/1ebd6c71457b292cdef97c218fb3e4176d10d502))
* **Uncategorized**
- Release gix-actor v0.31.5, gix-filter v0.11.3, gix-fs v0.11.2, gix-commitgraph v0.24.3, gix-revwalk v0.13.2, gix-traverse v0.39.2, gix-worktree-stream v0.13.1, gix-archive v0.13.2, gix-config-value v0.14.7, gix-tempfile v14.0.1, gix-ref v0.45.0, gix-sec v0.10.7, gix-config v0.38.0, gix-prompt v0.8.6, gix-url v0.27.4, gix-credentials v0.24.3, gix-ignore v0.11.3, gix-index v0.33.1, gix-worktree v0.34.1, gix-diff v0.44.1, gix-discover v0.33.0, gix-pathspec v0.7.6, gix-dir v0.6.0, gix-mailmap v0.23.5, gix-negotiate v0.13.2, gix-pack v0.51.1, gix-odb v0.61.1, gix-transport v0.42.2, gix-protocol v0.45.2, gix-revision v0.27.2, gix-refspec v0.23.1, gix-status v0.11.0, gix-submodule v0.12.0, gix-worktree-state v0.11.1, gix v0.64.0, gix-fsck v0.4.1, gitoxide-core v0.39.0, gitoxide v0.37.0 ([`6232824`](https://github.com/GitoxideLabs/gitoxide/commit/6232824301847a9786dea0b926796a3187493587))
- Release gix-glob v0.16.4, gix-attributes v0.22.3, gix-command v0.3.8, gix-filter v0.11.3, gix-fs v0.11.2, gix-commitgraph v0.24.3, gix-revwalk v0.13.2, gix-traverse v0.39.2, gix-worktree-stream v0.13.1, gix-archive v0.13.2, gix-config-value v0.14.7, gix-tempfile v14.0.1, gix-ref v0.45.0, gix-sec v0.10.7, gix-config v0.38.0, gix-prompt v0.8.6, gix-url v0.27.4, gix-credentials v0.24.3, gix-ignore v0.11.3, gix-index v0.33.1, gix-worktree v0.34.1, gix-diff v0.44.1, gix-discover v0.33.0, gix-pathspec v0.7.6, gix-dir v0.6.0, gix-mailmap v0.23.5, gix-negotiate v0.13.2, gix-pack v0.51.1, gix-odb v0.61.1, gix-transport v0.42.2, gix-protocol v0.45.2, gix-revision v0.27.2, gix-refspec v0.23.1, gix-status v0.11.0, gix-submodule v0.12.0, gix-worktree-state v0.11.1, gix v0.64.0, gix-fsck v0.4.1, gitoxide-core v0.39.0, gitoxide v0.37.0 ([`a1b73a6`](https://github.com/GitoxideLabs/gitoxide/commit/a1b73a67c19d9102a2c5a7f574a7a53a86d0094c))
- Update manifests (by cargo-smart-release) ([`0470df3`](https://github.com/GitoxideLabs/gitoxide/commit/0470df3b8ebb136b219f0057f1e9a7031975cce5))
- Prepare changelog prior to release ([`99c00cc`](https://github.com/GitoxideLabs/gitoxide/commit/99c00cc3ae9827555e2e1162328bc57038619d1f))
- Merge branch 'fixes' ([`b4dba1c`](https://github.com/GitoxideLabs/gitoxide/commit/b4dba1c187baba44ee927daa538783f7f424b2f2))
- Release gix-path v0.10.9 ([`15f1cf7`](https://github.com/GitoxideLabs/gitoxide/commit/15f1cf76764221d14afa66d03a6528b19b9c30c9))
- Release gix-actor v0.31.4, gix-object v0.42.3 ([`bf3d82a`](https://github.com/GitoxideLabs/gitoxide/commit/bf3d82abc7c875109f9a5d6b6713ce68153b6456))
- Merge branch 'heredocs' ([`7330844`](https://github.com/GitoxideLabs/gitoxide/commit/73308446e5ffee053af35b108e3d49c71db31e99))
- Use `<<` rather than `<<-` heredoc operator ([`2641f8b`](https://github.com/GitoxideLabs/gitoxide/commit/2641f8b36008ade04d59d76bd6d546005ad76a21))
- Release gix-path v0.10.8 ([`8d89b86`](https://github.com/GitoxideLabs/gitoxide/commit/8d89b865c84d1fb153d93343d1ce4e1d64e53541))
- Merge branch 'tar-only' ([`1dfa90d`](https://github.com/GitoxideLabs/gitoxide/commit/1dfa90d641306b4099a6ecd52e2056b231467807))
- Remove binary files in favor of `tar` files ([`dcab79a`](https://github.com/GitoxideLabs/gitoxide/commit/dcab79a6958cbf5cd69184c24497dc27c6f94961))
- Merge branch 'main' into config-key-take-2 ([`9fa1054`](https://github.com/GitoxideLabs/gitoxide/commit/9fa1054a01071180d7b08c8c2b5bd61e9d0d32da))
- Merge pull request #1361 from EliahKagan/freebsd ([`9c65d98`](https://github.com/GitoxideLabs/gitoxide/commit/9c65d9886328f53129b966aecdc91644297c54be))
- Make bash script shebangs more portable ([`68cbea8`](https://github.com/GitoxideLabs/gitoxide/commit/68cbea815aa979acb0b86943db83ab77bbc728c4))
- Release gix-fs v0.11.1, gix-glob v0.16.3 ([`2cefe77`](https://github.com/GitoxideLabs/gitoxide/commit/2cefe77203131878d0d8f5346f20f0e25b76cbea))
## 0.5.0 (2024-05-22)
A maintenance release without user-facing changes.
### Commit Statistics
- 9 commits contributed to the release over the course of 33 calendar days.
- 33 days passed between releases.
- 0 commits were understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' were seen in commit messages
### Commit Details
view details
* **Uncategorized**
- Release gix-features v0.38.2, gix-actor v0.31.2, gix-validate v0.8.5, gix-object v0.42.2, gix-command v0.3.7, gix-filter v0.11.2, gix-fs v0.11.0, gix-revwalk v0.13.1, gix-traverse v0.39.1, gix-worktree-stream v0.13.0, gix-archive v0.13.0, gix-tempfile v14.0.0, gix-lock v14.0.0, gix-ref v0.44.0, gix-config v0.37.0, gix-prompt v0.8.5, gix-index v0.33.0, gix-worktree v0.34.0, gix-diff v0.44.0, gix-discover v0.32.0, gix-pathspec v0.7.5, gix-dir v0.5.0, gix-macros v0.1.5, gix-mailmap v0.23.1, gix-negotiate v0.13.1, gix-pack v0.51.0, gix-odb v0.61.0, gix-transport v0.42.1, gix-protocol v0.45.1, gix-revision v0.27.1, gix-status v0.10.0, gix-submodule v0.11.0, gix-worktree-state v0.11.0, gix v0.63.0, gitoxide-core v0.38.0, gitoxide v0.36.0, safety bump 19 crates ([`4f98e94`](https://github.com/GitoxideLabs/gitoxide/commit/4f98e94e0e8b79ed2899b35bef40f3c30b3025b0))
- Adjust changelogs prior to release ([`9511416`](https://github.com/GitoxideLabs/gitoxide/commit/9511416a6cd0c571233f958c165329c8705c2498))
- Merge branch 'various-fixes' ([`d6cd449`](https://github.com/GitoxideLabs/gitoxide/commit/d6cd44930fb204b06e2b70fc6965e7705530c47a))
- Merge pull request from GHSA-7w47-3wg8-547c ([`79dce79`](https://github.com/GitoxideLabs/gitoxide/commit/79dce79c62f6072aa2653780d590dc3993dfa401))
- Address review comments ([`fcc3b69`](https://github.com/GitoxideLabs/gitoxide/commit/fcc3b69867db1628f6a44d0e0dad8f7417f566bc))
- Adapt to changes in `gix-worktree` ([`1ca6a3c`](https://github.com/GitoxideLabs/gitoxide/commit/1ca6a3ce22887c7eb42ec3e0a19f6e1202715745))
- Merge branch 'status' ([`68fd5b3`](https://github.com/GitoxideLabs/gitoxide/commit/68fd5b34e1214d5c2cc7d00dd06e19ee86c00c66))
- Cleanup path classificaiton after fixes in `gix-pathspec` ([`44a2e00`](https://github.com/GitoxideLabs/gitoxide/commit/44a2e005ea8241d026ae542dd4a71cfb6cfd8308))
- Merge branch 'cargo-fixes' ([`977346e`](https://github.com/GitoxideLabs/gitoxide/commit/977346ee61de6207c66f3de003db6e8c722fb81c))
## 0.4.1 (2024-04-18)
### Bug Fixes
- assure worktree-roots aren't pruned with pathspecs that are never meant for them.
Previously, when pathspecs were defined, the classification of the worktree-root
would also be using them. This means that depending on the pathspec, worktree-roots would
be pruned, which in turn makes it impossible to recurse into them.
Now pathspecs are disabled when classifying the worktree-root directory.
### Commit Statistics
- 5 commits contributed to the release.
- 5 days passed between releases.
- 1 commit was understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' were seen in commit messages
### Commit Details
view details
* **Uncategorized**
- Release gix-index v0.32.1, gix-pathspec v0.7.4, gix-worktree v0.33.1, gix-dir v0.4.1 ([`54ac559`](https://github.com/GitoxideLabs/gitoxide/commit/54ac55946bb04635cd74582a1ce2e4bee70f2e60))
- Prepare changelog prior to `gix-dir` patch release ([`6ca6fa6`](https://github.com/GitoxideLabs/gitoxide/commit/6ca6fa69b5c21c8d8e9e07e21558e98201504cda))
- Assure worktree-roots aren't pruned with pathspecs that are never meant for them. ([`7f2f3ff`](https://github.com/GitoxideLabs/gitoxide/commit/7f2f3ff8adbecd631c2b4513995d6c94b21742eb))
- Merge pull request #1345 from EliahKagan/shell-scripts ([`fe24c89`](https://github.com/GitoxideLabs/gitoxide/commit/fe24c89e326670deaa3aaa643276d612d866072e))
- Add missing +x bit on scripts that are run and not sourced ([`41bf65a`](https://github.com/GitoxideLabs/gitoxide/commit/41bf65adef6f7d2cdd28fede262173ec7ba10822))
## 0.4.0 (2024-04-13)
A maintenance release without user-facing changes.
### Commit Statistics
- 6 commits contributed to the release.
- 0 commits were understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' were seen in commit messages
### Commit Details
view details
* **Uncategorized**
- Release gix-trace v0.1.9, gix-utils v0.1.12, gix-packetline-blocking v0.17.4, gix-filter v0.11.1, gix-fs v0.10.2, gix-traverse v0.39.0, gix-worktree-stream v0.12.0, gix-archive v0.12.0, gix-config v0.36.1, gix-url v0.27.3, gix-index v0.32.0, gix-worktree v0.33.0, gix-diff v0.43.0, gix-pathspec v0.7.3, gix-dir v0.4.0, gix-pack v0.50.0, gix-odb v0.60.0, gix-transport v0.42.0, gix-protocol v0.45.0, gix-status v0.9.0, gix-worktree-state v0.10.0, gix v0.62.0, gix-fsck v0.4.0, gitoxide-core v0.37.0, gitoxide v0.35.0, safety bump 14 crates ([`095c673`](https://github.com/GitoxideLabs/gitoxide/commit/095c6739b2722a8b9af90776b435ef2da454c0e6))
- Prepare changelogs prior to release ([`5755271`](https://github.com/GitoxideLabs/gitoxide/commit/57552717f46f96c35ba4ddc0a64434354ef845e9))
- Merge pull request #1341 from szepeviktor/typos ([`55f379b`](https://github.com/GitoxideLabs/gitoxide/commit/55f379bc47065822d078393d83d30c0835a89782))
- Fix typos ([`f72ecce`](https://github.com/GitoxideLabs/gitoxide/commit/f72ecce45babcad2a0c9b73c79d01ff502907a57))
- Merge pull request #1334 from EliahKagan/nonstandard-worktree ([`37732fb`](https://github.com/GitoxideLabs/gitoxide/commit/37732fb13efdff5a1b8a836943e9e575196724b5))
- Let nonstandard worktree fixtures work even if Git < 2.37.2 ([`01d6be9`](https://github.com/GitoxideLabs/gitoxide/commit/01d6be997fced8695b60d0dc206f87e542037a8d))
## 0.3.0 (2024-03-18)
### Bug Fixes
- allow traversals to start from a symlink that points to a directory
Now symlinked repositories can be traversed as well.
### New Features (BREAKING)
- allow directory walk to be interrupted with `should_interrupt` flag.
That way, it can be much more responsive to interruption.
- assure symlinks to directories are ignored with `dir/` declarations in `.gitignore`.
Initially, symlinks appear like symlinks thanks to `lstat`, but to do
exclude handling correctly these need another `stat` call.
However, this is also not done in Git, but done in `libgit2` only,
so a toggle was added to act like Git by default, but allow obtaining
the same behaviour as git2 for compatibility.
### Commit Statistics
- 6 commits contributed to the release.
- 3 days passed between releases.
- 3 commits were understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' were seen in commit messages
### Commit Details
view details
* **Uncategorized**
- Release gix-actor v0.31.1, gix-object v0.42.1, gix-index v0.31.1, gix-pathspec v0.7.2, gix-dir v0.3.0, gix-status v0.8.0, gix v0.61.0, safety bump 2 crates ([`155cc45`](https://github.com/GitoxideLabs/gitoxide/commit/155cc45730b259e662d7c4be42a469a3af3750e1))
- Prepare changelog prior to release ([`129ba3d`](https://github.com/GitoxideLabs/gitoxide/commit/129ba3deccc9ada0dc571466458845939502763d))
- Merge branch 'improvements-for-cargo' ([`41cd53e`](https://github.com/GitoxideLabs/gitoxide/commit/41cd53e2af76e35e047aac4eca6324774df4cb50))
- Allow directory walk to be interrupted with `should_interrupt` flag. ([`35b74e7`](https://github.com/GitoxideLabs/gitoxide/commit/35b74e7992a5a732b5ae8dbdc264479a91b1d60d))
- Allow traversals to start from a symlink that points to a directory ([`e7e91cf`](https://github.com/GitoxideLabs/gitoxide/commit/e7e91cfaed6d40a773a65fc077b99d2e26bb28f5))
- Assure symlinks to directories are ignored with `dir/` declarations in `.gitignore`. ([`cd0c8af`](https://github.com/GitoxideLabs/gitoxide/commit/cd0c8af78fd7a4f06e33ec2ce06b094b5a490877))
## 0.2.0 (2024-03-14)
### Bug Fixes
- make it possible to use a submodule root for a full walk.
Previously, it would not allow to enter the repository, making
a walk impossible.
### Commit Statistics
- 6 commits contributed to the release over the course of 4 calendar days.
- 18 days passed between releases.
- 1 commit was understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' were seen in commit messages
### Commit Details
view details
* **Uncategorized**
- Release gix-date v0.8.5, gix-hash v0.14.2, gix-trace v0.1.8, gix-utils v0.1.11, gix-features v0.38.1, gix-actor v0.31.0, gix-validate v0.8.4, gix-object v0.42.0, gix-path v0.10.7, gix-glob v0.16.2, gix-quote v0.4.12, gix-attributes v0.22.2, gix-command v0.3.6, gix-filter v0.11.0, gix-fs v0.10.1, gix-chunk v0.4.8, gix-commitgraph v0.24.2, gix-hashtable v0.5.2, gix-revwalk v0.13.0, gix-traverse v0.38.0, gix-worktree-stream v0.11.0, gix-archive v0.11.0, gix-config-value v0.14.6, gix-tempfile v13.1.1, gix-lock v13.1.1, gix-ref v0.43.0, gix-sec v0.10.6, gix-config v0.36.0, gix-prompt v0.8.4, gix-url v0.27.2, gix-credentials v0.24.2, gix-ignore v0.11.2, gix-bitmap v0.2.11, gix-index v0.31.0, gix-worktree v0.32.0, gix-diff v0.42.0, gix-discover v0.31.0, gix-pathspec v0.7.1, gix-dir v0.2.0, gix-macros v0.1.4, gix-mailmap v0.23.0, gix-negotiate v0.13.0, gix-pack v0.49.0, gix-odb v0.59.0, gix-packetline v0.17.4, gix-transport v0.41.2, gix-protocol v0.44.2, gix-revision v0.27.0, gix-refspec v0.23.0, gix-status v0.7.0, gix-submodule v0.10.0, gix-worktree-state v0.9.0, gix v0.60.0, safety bump 26 crates ([`b050327`](https://github.com/GitoxideLabs/gitoxide/commit/b050327e76f234b19be921b78b7b28e034319fdb))
- Prepare changelogs prior to release ([`52c3bbd`](https://github.com/GitoxideLabs/gitoxide/commit/52c3bbd36b9e94a0f3a78b4ada84d0c08eba27f6))
- Merge branch 'status' ([`3e5c974`](https://github.com/GitoxideLabs/gitoxide/commit/3e5c974dd62ac134711c6c2f5a5490187a6ea55e))
- Keep lower-bound of `thiserror` low in `gix-dir` ([`917634f`](https://github.com/GitoxideLabs/gitoxide/commit/917634fa694a1e91d37f6407e57ae96b3b0aec4b))
- Fix lints for nightly, and clippy ([`f8ce3d0`](https://github.com/GitoxideLabs/gitoxide/commit/f8ce3d0721b6a53713a9392f2451874f520bc44c))
- Make it possible to use a submodule root for a full walk. ([`434f543`](https://github.com/GitoxideLabs/gitoxide/commit/434f5434d7242f7f3d6b595f767195c51a3acd86))
## 0.1.0 (2024-02-25)
### Bug Fixes
- pathspec prefixes still allows directory collapsing.
- proper submodule handling
Previously it was possible for `.git` files in directories to
not trigger repository detection.
- assure `Action::Cancel` doesn't run into unreachable code.
### New Features (BREAKING)
- Represent `DotGit` as `ExtendedKind`
This cleans up the model despite also making it harder to detect
whether something is a DotGit.
- simplify `walk()` signature to compute `root` with pathspec directory.
This makes the overall handling more unified, while assuring it's always
in the worktree.
And as a pathspec directory isn't exactly the same as a user-specified root,
it's also possible to override this automation.
- allow to emit all collapsed entries.
This is useful for rename tracking as it allows to see all files
that may take part in a rename (i.e. when a directory is renamed).
### Commit Statistics
- 19 commits contributed to the release over the course of 57 calendar days.
- 6 commits were understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' were seen in commit messages
### Commit Details
view details
* **Uncategorized**
- Release gix-date v0.8.4, gix-utils v0.1.10, gix-actor v0.30.1, gix-object v0.41.1, gix-path v0.10.6, gix-glob v0.16.1, gix-quote v0.4.11, gix-attributes v0.22.1, gix-command v0.3.5, gix-filter v0.10.0, gix-commitgraph v0.24.1, gix-worktree-stream v0.10.0, gix-archive v0.10.0, gix-config-value v0.14.5, gix-ref v0.42.0, gix-sec v0.10.5, gix-config v0.35.0, gix-prompt v0.8.3, gix-url v0.27.1, gix-credentials v0.24.1, gix-ignore v0.11.1, gix-index v0.30.0, gix-worktree v0.31.0, gix-diff v0.41.0, gix-discover v0.30.0, gix-pathspec v0.7.0, gix-dir v0.1.0, gix-pack v0.48.0, gix-odb v0.58.0, gix-transport v0.41.1, gix-protocol v0.44.1, gix-revision v0.26.1, gix-refspec v0.22.1, gix-status v0.6.0, gix-submodule v0.9.0, gix-worktree-state v0.8.0, gix v0.59.0, gix-fsck v0.3.0, gitoxide-core v0.36.0, gitoxide v0.34.0, safety bump 10 crates ([`45b4470`](https://github.com/GitoxideLabs/gitoxide/commit/45b447045bc826f252129c300c531acde2652c64))
- Prepare changelogs prior to release ([`f2e111f`](https://github.com/GitoxideLabs/gitoxide/commit/f2e111f768fc1bc6182355261c20b63610cffec7))
- Merge branch 'status' ([`d53504a`](https://github.com/GitoxideLabs/gitoxide/commit/d53504a1fad41cec7b6ca2a4abb7f185d8941e3f))
- Make it even harder to remove your own CWD ([`4d5767c`](https://github.com/GitoxideLabs/gitoxide/commit/4d5767cd394d755104aa7f0c1ed5b8e01bf74b12))
- Assure that we don't artificially make non-recursable directories visible ([`1a26732`](https://github.com/GitoxideLabs/gitoxide/commit/1a26732fe897161f9bfa397efdb07aa57f3c7341))
- Represent `DotGit` as `ExtendedKind` ([`bd5f449`](https://github.com/GitoxideLabs/gitoxide/commit/bd5f44925306aa342b2b1c547779799b72372212))
- Pathspec prefixes still allows directory collapsing. ([`95d10ee`](https://github.com/GitoxideLabs/gitoxide/commit/95d10ee9371196cbcb8e599d28d9d05fa8b68221))
- Merge branch 'status' ([`bb48c4c`](https://github.com/GitoxideLabs/gitoxide/commit/bb48c4ce22650b8c76af3b147e252ebe7cedb205))
- More natural top-level handling ([`44ccc67`](https://github.com/GitoxideLabs/gitoxide/commit/44ccc67a5b4a481f769399c41f0d3fc956fd8ec8))
- Simplify `walk()` signature to compute `root` with pathspec directory. ([`b6ea37a`](https://github.com/GitoxideLabs/gitoxide/commit/b6ea37a4d20e008c0b447090992c6aade0191265))
- Allow to emit all collapsed entries. ([`4567dbb`](https://github.com/GitoxideLabs/gitoxide/commit/4567dbb2abf3d05bebe2206afafc40002a376d26))
- Proper submodule handling ([`dc200bf`](https://github.com/GitoxideLabs/gitoxide/commit/dc200bf6f2cb10b6f0e45dd83bf9f82173cbb04f))
- Assure `Action::Cancel` doesn't run into unreachable code. ([`c04954a`](https://github.com/GitoxideLabs/gitoxide/commit/c04954a89dfdd8c230050b6175e2a132c73bdbfa))
- Merge branch 'status' ([`b8def77`](https://github.com/GitoxideLabs/gitoxide/commit/b8def77e91ddc82a39ec342b89f558702a8f1d8c))
- Make sure that `*foo*` prefixes don't end up matching any directory. ([`482d6f3`](https://github.com/GitoxideLabs/gitoxide/commit/482d6f3f773fd74ddcea4be0b36ebea89017397a))
- Merge branch 'dirwalk' ([`face359`](https://github.com/GitoxideLabs/gitoxide/commit/face359443ba33e8985ec1525d5ec38b743ea7a9))
- Implementation of the Git-style directory walk. ([`3252cfd`](https://github.com/GitoxideLabs/gitoxide/commit/3252cfd570b0c0897c51939e1a8c45b35c861c53))
- Merge branch 'gix-status' ([`c3983c6`](https://github.com/GitoxideLabs/gitoxide/commit/c3983c6b8d63d85ec713ae8d661723f9cf0bd55b))
- Initial version of the `gix-dir` crate ([`22acf0d`](https://github.com/GitoxideLabs/gitoxide/commit/22acf0def5c62563300aa8eaef01cb94bcd15645))
gix-dir-0.11.0/Cargo.toml 0000644 00000007351 00000000001 0010462 0 ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.65"
name = "gix-dir"
version = "0.11.0"
authors = ["Sebastian Thiel "]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "A crate of the gitoxide project dealing with directory walks"
readme = false
license = "MIT OR Apache-2.0"
repository = "https://github.com/GitoxideLabs/gitoxide"
[lib]
name = "gix_dir"
path = "src/lib.rs"
test = false
doctest = false
[[test]]
name = "dir"
path = "tests/dir/main.rs"
[[test]]
name = "dir_cwd"
path = "tests/dir_cwd.rs"
[dependencies.bstr]
version = "1.5.0"
default-features = false
[dependencies.gix-discover]
version = "^0.37.0"
[dependencies.gix-fs]
version = "^0.12.1"
[dependencies.gix-ignore]
version = "^0.12.1"
[dependencies.gix-index]
version = "^0.37.0"
[dependencies.gix-object]
version = "^0.46.1"
[dependencies.gix-path]
version = "^0.10.13"
[dependencies.gix-pathspec]
version = "^0.8.1"
[dependencies.gix-trace]
version = "^0.1.11"
[dependencies.gix-utils]
version = "^0.1.13"
features = ["bstr"]
[dependencies.gix-worktree]
version = "^0.38.0"
default-features = false
[dependencies.thiserror]
version = "2.0.0"
[dev-dependencies.pretty_assertions]
version = "1.4.0"
[lints.clippy]
bool_to_int_with_if = "allow"
borrow_as_ptr = "allow"
cast_lossless = "allow"
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
checked_conversions = "allow"
copy_iterator = "allow"
default_trait_access = "allow"
doc_markdown = "allow"
empty_docs = "allow"
enum_glob_use = "allow"
explicit_deref_methods = "allow"
explicit_into_iter_loop = "allow"
explicit_iter_loop = "allow"
filter_map_next = "allow"
fn_params_excessive_bools = "allow"
from_iter_instead_of_collect = "allow"
if_not_else = "allow"
ignored_unit_patterns = "allow"
implicit_clone = "allow"
inconsistent_struct_constructor = "allow"
inefficient_to_string = "allow"
inline_always = "allow"
items_after_statements = "allow"
iter_not_returning_iterator = "allow"
iter_without_into_iter = "allow"
manual_assert = "allow"
manual_is_variant_and = "allow"
manual_let_else = "allow"
manual_string_new = "allow"
many_single_char_names = "allow"
match_bool = "allow"
match_same_arms = "allow"
match_wild_err_arm = "allow"
match_wildcard_for_single_variants = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
must_use_candidate = "allow"
mut_mut = "allow"
naive_bytecount = "allow"
needless_for_each = "allow"
needless_pass_by_value = "allow"
needless_raw_string_hashes = "allow"
no_effect_underscore_binding = "allow"
option_option = "allow"
range_plus_one = "allow"
redundant_else = "allow"
return_self_not_must_use = "allow"
should_panic_without_expect = "allow"
similar_names = "allow"
single_match_else = "allow"
stable_sort_primitive = "allow"
struct_excessive_bools = "allow"
struct_field_names = "allow"
too_long_first_doc_paragraph = "allow"
too_many_lines = "allow"
transmute_ptr_to_ptr = "allow"
trivially_copy_pass_by_ref = "allow"
unnecessary_join = "allow"
unnecessary_wraps = "allow"
unreadable_literal = "allow"
unused_self = "allow"
used_underscore_binding = "allow"
wildcard_imports = "allow"
[lints.clippy.pedantic]
level = "warn"
priority = -1
[lints.rust]
gix-dir-0.11.0/Cargo.toml.orig 0000644 0000000 0000000 00000002322 10461020230 0014134 0 ustar 0000000 0000000 lints.workspace = true
[package]
name = "gix-dir"
version = "0.11.0"
repository = "https://github.com/GitoxideLabs/gitoxide"
license = "MIT OR Apache-2.0"
description = "A crate of the gitoxide project dealing with directory walks"
authors = ["Sebastian Thiel "]
edition = "2021"
rust-version = "1.65"
[lib]
doctest = false
test = false
[dependencies]
gix-trace = { version = "^0.1.11", path = "../gix-trace" }
gix-index = { version = "^0.37.0", path = "../gix-index" }
gix-discover = { version = "^0.37.0", path = "../gix-discover" }
gix-fs = { version = "^0.12.1", path = "../gix-fs" }
gix-path = { version = "^0.10.13", path = "../gix-path" }
gix-pathspec = { version = "^0.8.1", path = "../gix-pathspec" }
gix-worktree = { version = "^0.38.0", path = "../gix-worktree", default-features = false }
gix-object = { version = "^0.46.1", path = "../gix-object" }
gix-ignore = { version = "^0.12.1", path = "../gix-ignore" }
gix-utils = { version = "^0.1.13", path = "../gix-utils", features = ["bstr"] }
bstr = { version = "1.5.0", default-features = false }
thiserror = "2.0.0"
[dev-dependencies]
gix-testtools = { path = "../tests/tools" }
gix-fs = { path = "../gix-fs" }
pretty_assertions = "1.4.0"
gix-dir-0.11.0/LICENSE-APACHE 0000644 0000000 0000000 00000024746 10461020230 0013207 0 ustar 0000000 0000000
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
gix-dir-0.11.0/LICENSE-MIT 0000644 0000000 0000000 00000001777 10461020230 0012716 0 ustar 0000000 0000000 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
gix-dir-0.11.0/src/entry.rs 0000644 0000000 0000000 00000021677 10461020230 0013561 0 ustar 0000000 0000000 use crate::walk::ForDeletionMode;
use crate::{Entry, EntryRef};
use std::borrow::Cow;
use std::fs::FileType;
/// A way of attaching additional information to an [Entry] .
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum Property {
/// The entry was named `.git`, matched according to the case-sensitivity rules of the repository.
DotGit,
/// The entry is a directory, and that directory is empty.
EmptyDirectory,
/// The entry is a directory, it is empty and the current working directory.
///
/// The caller should pay special attention to this very special case, as it is indeed only possible to run into it
/// while traversing the directory for deletion.
/// Non-empty directory will never be collapsed, hence if they are working directories, they naturally become unobservable.
EmptyDirectoryAndCWD,
/// Always in conjunction with a directory on disk that is also known as cone-mode sparse-checkout exclude marker
/// - i.e. a directory that is excluded, so its whole content is excluded and not checked out nor is part of the index.
///
/// Note that evne if the directory is empty, it will only have this state, not `EmptyDirectory`.
TrackedExcluded,
}
/// The kind of the entry, seated in their kinds available on disk.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum Kind {
/// Something that is not a regular file, directory, or symbolic link.
///
/// These can only exist in the filesystem,
/// because Git repositories do not support them, thus they cannot be tracked.
/// Hence, they do not appear as blobs in a repository, and their type is not specifiable in a tree object.
/// Examples include named pipes (FIFOs), character devices, block devices, and sockets.
Untrackable,
/// The entry is a blob, representing a regular file, executable or not.
File,
/// The entry is a symlink.
Symlink,
/// The entry is an ordinary directory.
///
/// Note that since we don't check for bare repositories, this could in fact be a collapsed
/// bare repository. To be sure, check it again with [`gix_discover::is_git()`] and act accordingly.
Directory,
/// The entry is a directory which *contains* a `.git` folder, or a submodule entry in the index.
Repository,
}
/// The kind of entry as obtained from a directory.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum Status {
/// The entry was removed from the walk due to its other properties, like [Property] or [PathspecMatch]
///
/// Note that entries flagged as `DotGit` directory will always be considered `Pruned`, but if they are
/// also ignored, in delete mode, they will be considered `Ignored` instead. This way, it's easier to remove them
/// while they will not be available for any interactions in read-only mode.
Pruned,
/// The entry is tracked in Git.
Tracked,
/// The entry is ignored as per `.gitignore` files and their rules.
///
/// If this is a directory, then its entire contents is ignored. Otherwise, possibly due to configuration, individual ignored files are listed.
Ignored(gix_ignore::Kind),
/// The entry is not tracked by git yet, it was not found in the [index](gix_index::State).
///
/// If it's a directory, the entire directory contents is untracked.
Untracked,
}
/// Describe how a pathspec pattern matched.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
pub enum PathspecMatch {
/// The match happened because there wasn't any pattern, which matches all, or because there was a nil pattern or one with an empty path.
/// Thus, this is not a match by merit.
Always,
/// A match happened, but the pattern excludes everything it matches, which means this entry was excluded.
Excluded,
/// The first part of a pathspec matches, like `dir/` that matches `dir/a`.
Prefix,
/// The whole pathspec matched and used a wildcard match, like `a/*` matching `a/file`.
WildcardMatch,
/// The entire pathspec matched, letter by letter, e.g. `a/file` matching `a/file`.
Verbatim,
}
impl PathspecMatch {
pub(crate) fn should_ignore(&self) -> bool {
match self {
PathspecMatch::Always | PathspecMatch::Excluded => true,
PathspecMatch::Prefix | PathspecMatch::WildcardMatch | PathspecMatch::Verbatim => false,
}
}
}
impl From for PathspecMatch {
fn from(kind: gix_pathspec::search::MatchKind) -> Self {
match kind {
gix_pathspec::search::MatchKind::Always => Self::Always,
gix_pathspec::search::MatchKind::Prefix => Self::Prefix,
gix_pathspec::search::MatchKind::WildcardMatch => Self::WildcardMatch,
gix_pathspec::search::MatchKind::Verbatim => Self::Verbatim,
}
}
}
impl From> for PathspecMatch {
fn from(m: gix_pathspec::search::Match<'_>) -> Self {
if m.is_excluded() {
PathspecMatch::Excluded
} else {
m.kind.into()
}
}
}
/// Conversion
impl EntryRef<'_> {
/// Strip the lifetime to obtain a fully owned copy.
pub fn to_owned(&self) -> Entry {
Entry {
rela_path: self.rela_path.clone().into_owned(),
status: self.status,
property: self.property,
disk_kind: self.disk_kind,
index_kind: self.index_kind,
pathspec_match: self.pathspec_match,
}
}
/// Turn this instance into a fully owned copy.
pub fn into_owned(self) -> Entry {
Entry {
rela_path: self.rela_path.into_owned(),
status: self.status,
property: self.property,
disk_kind: self.disk_kind,
index_kind: self.index_kind,
pathspec_match: self.pathspec_match,
}
}
}
/// Conversion
impl Entry {
/// Obtain an [`EntryRef`] from this instance.
pub fn to_ref(&self) -> EntryRef<'_> {
EntryRef {
rela_path: Cow::Borrowed(self.rela_path.as_ref()),
status: self.status,
property: self.property,
disk_kind: self.disk_kind,
index_kind: self.index_kind,
pathspec_match: self.pathspec_match,
}
}
}
impl From for Kind {
fn from(value: FileType) -> Self {
if value.is_dir() {
Kind::Directory
} else if value.is_symlink() {
Kind::Symlink
} else if value.is_file() {
Kind::File
} else {
Kind::Untrackable
}
}
}
impl Status {
/// Return true if this status is considered pruned. A pruned entry is typically hidden from view due to a pathspec.
pub fn is_pruned(&self) -> bool {
matches!(&self, Status::Pruned)
}
/// Return `true` if `file_type` is a directory on disk and isn't ignored, and is not a repository.
/// This implements the default rules of `git status`, which is good for a minimal traversal through
/// tracked and non-ignored portions of a worktree.
/// `for_deletion` is used to determine if recursion into a directory is allowed even though it otherwise wouldn't be.
/// If `worktree_root_is_repository` is `true`, then this status is part of the root of an iteration, and the corresponding
/// worktree root is a repository itself. This typically happens for submodules. In this case, recursion rules are relaxed
/// to allow traversing submodule worktrees.
///
/// Use `pathspec_match` to determine if a pathspec matches in any way, affecting the decision to recurse.
pub fn can_recurse(
&self,
file_type: Option,
pathspec_match: Option,
for_deletion: Option,
worktree_root_is_repository: bool,
) -> bool {
let is_dir_on_disk = file_type.map_or(false, |ft| {
if worktree_root_is_repository {
ft.is_dir()
} else {
ft.is_recursable_dir()
}
});
if !is_dir_on_disk {
return false;
}
match self {
Status::Pruned => false,
Status::Ignored(_) => {
for_deletion.map_or(false, |fd| {
matches!(
fd,
ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories
| ForDeletionMode::FindRepositoriesInIgnoredDirectories
)
}) || pathspec_match.map_or(false, |m| !m.should_ignore())
}
Status::Untracked | Status::Tracked => true,
}
}
}
impl Kind {
pub(super) fn is_recursable_dir(&self) -> bool {
matches!(self, Kind::Directory)
}
/// Return `true` if this is a directory on disk. Note that this is true for repositories as well.
pub fn is_dir(&self) -> bool {
matches!(self, Kind::Directory | Kind::Repository)
}
}
gix-dir-0.11.0/src/lib.rs 0000644 0000000 0000000 00000006615 10461020230 0013161 0 ustar 0000000 0000000 //! A crate for handling a git-style directory walk.
#![deny(missing_docs, rust_2018_idioms)]
#![forbid(unsafe_code)]
use bstr::{BStr, BString};
use std::borrow::Cow;
/// A directory entry, typically obtained using [`walk()`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct EntryRef<'a> {
/// The repository-relative path at which the file or directory could be found, with unix-style component separators.
///
/// To obtain the respective file, join it with the `worktree_root` passed to [`walk()`].
/// The rationale here is that this is a compressed and normalized version compared to the paths we would otherwise get,
/// which is preferable especially when converted to [`Entry`] due to lower memory requirements.
///
/// This also means that the original path to be presented to the user needs to be computed separately, as it's also relative
/// to their prefix, i.e. their current working directory within the repository.
///
/// Note that this value can be empty if information about the `worktree_root` is provided, which is fine as
/// [joining](std::path::Path::join) with an empty string is a no-op.
///
/// Note that depending on the way entries are emitted, even refs might already contain an owned `rela_path`, for use with
/// [into_owned()](EntryRef::into_owned())
///
pub rela_path: Cow<'a, BStr>,
/// The status of entry, most closely related to what we know from `git status`, but not the same.
///
/// Note that many entries with status `Pruned` will not show up as their kind hasn't yet been determined when they were
/// pruned very early on.
pub status: entry::Status,
/// Additional properties of the entry.
pub property: Option,
/// Further specify what the entry is on disk, similar to a file mode.
/// This is `None` if we decided it's not worth it to exit early and avoid trying to obtain this information.
pub disk_kind: Option,
/// The kind of entry according to the index, if tracked. *Usually* the same as `disk_kind`.
pub index_kind: Option,
/// Determines how the pathspec matched.
/// Note that it can also be `Some(PathspecMatch::Excluded)` if a negative pathspec matched.
pub pathspec_match: Option,
}
/// Just like [`EntryRef`], but with all fields owned (and thus without a lifetime to consider).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Entry {
/// See [EntryRef::rela_path] for details.
pub rela_path: BString,
/// The status of entry, most closely related to what we know from `git status`, but not the same.
pub status: entry::Status,
/// Additional flags that further clarify properties of the entry.
pub property: Option,
/// Further specify what the entry is on disk, similar to a file mode.
pub disk_kind: Option,
/// The kind of entry according to the index, if tracked. *Usually* the same as `disk_kind`.
/// Note that even if tracked, this might be `None` which indicates this is a worktree placed
/// within the parent repository.
pub index_kind: Option,
/// Indicate how the pathspec matches the entry. See more in [`EntryRef::pathspec_match`].
pub pathspec_match: Option,
}
///
pub mod entry;
///
pub mod walk;
pub use walk::function::walk;
gix-dir-0.11.0/src/walk/classify.rs 0000644 0000000 0000000 00000043323 10461020230 0015163 0 ustar 0000000 0000000 use crate::{entry, Entry, EntryRef};
use std::borrow::Cow;
use crate::entry::PathspecMatch;
use crate::walk::{Context, Error, ForDeletionMode, Options};
use bstr::{BStr, BString, ByteSlice};
use std::path::{Path, PathBuf};
/// Classify the `worktree_relative_root` path and return the first `PathKind` that indicates that
/// it isn't a directory, leaving `buf` with the path matching the returned `PathKind`,
/// which is at most equal to `worktree_relative_root`.
pub fn root(
worktree_root: &Path,
buf: &mut BString,
worktree_relative_root: &Path,
options: Options<'_>,
ctx: &mut Context<'_>,
) -> Result<(Outcome, bool), Error> {
buf.clear();
let mut last_length = None;
let mut path_buf = worktree_root.to_owned();
// These initial values kick in if worktree_relative_root.is_empty();
let file_kind = path_buf.symlink_metadata().ok().map(|m| m.file_type().into());
let mut out = path(&mut path_buf, buf, 0, file_kind, || None, options, ctx)?;
let worktree_root_is_repository = out
.disk_kind
.map_or(false, |kind| matches!(kind, entry::Kind::Repository));
for component in worktree_relative_root.components() {
if last_length.is_some() {
buf.push(b'/');
}
path_buf.push(component);
buf.extend_from_slice(gix_path::os_str_into_bstr(component.as_os_str()).expect("no illformed UTF8"));
let file_kind = path_buf.symlink_metadata().ok().map(|m| m.file_type().into());
out = path(
&mut path_buf,
buf,
last_length.map(|l| l + 1 /* slash */).unwrap_or_default(),
file_kind,
|| None,
options,
ctx,
)?;
if !out.status.can_recurse(
out.disk_kind,
out.pathspec_match,
options.for_deletion,
worktree_root_is_repository,
) {
break;
}
last_length = Some(buf.len());
}
Ok((out, worktree_root_is_repository))
}
/// The product of [`path()`] calls.
#[derive(Debug, Copy, Clone)]
pub struct Outcome {
/// The computed status of an entry. It can be seen as aggregate of things we know about an entry.
pub status: entry::Status,
/// An additional property.
pub property: Option,
/// What the entry is on disk, or `None` if we aborted the classification early or an IO-error occurred
/// when querying the disk.
///
/// Note that the index is used to avoid disk access provided its entries are marked uptodate
/// (possibly by a prior call to update the status).
pub disk_kind: Option,
/// What the entry looks like in the index, or `None` if we aborted early.
pub index_kind: Option,
/// If a pathspec matched, this is how it matched. Maybe `None` if computation didn't see the need to evaluate it.
pub pathspec_match: Option,
}
impl Outcome {
fn with_status(mut self, status: entry::Status) -> Self {
self.status = status;
self
}
fn with_kind(mut self, disk_kind: Option, index_kind: Option) -> Self {
self.disk_kind = disk_kind;
self.index_kind = index_kind;
self
}
}
impl From<&Entry> for Outcome {
fn from(e: &Entry) -> Self {
Outcome {
status: e.status,
property: e.property,
disk_kind: e.disk_kind,
index_kind: e.index_kind,
pathspec_match: e.pathspec_match,
}
}
}
impl<'a> EntryRef<'a> {
pub(super) fn from_outcome(rela_path: Cow<'a, BStr>, info: crate::walk::classify::Outcome) -> Self {
EntryRef {
rela_path,
property: info.property,
status: info.status,
disk_kind: info.disk_kind,
index_kind: info.index_kind,
pathspec_match: info.pathspec_match,
}
}
}
/// Figure out what to do with `rela_path`, provided as worktree-relative path, with `disk_file_type` if it is known already
/// as it helps to match pathspecs correctly, which can be different for directories.
/// `path` is a disk-accessible variant of `rela_path` which is within the `worktree_root`, and will be modified temporarily but remain unchanged.
///
/// Note that `rela_path` is used as buffer for convenience, but will be left as is when this function returns.
/// `filename_start_idx` is the index at which the filename begins, i.e. `a/b` has `2` as index.
/// It may resemble a directory on the way to a leaf (like a file)
///
/// Returns `(status, file_kind, pathspec_matches_how)` to identify the `status` on disk, along with a classification `file_kind`,
/// and if `file_kind` is not a directory, the way the pathspec matched with `pathspec_matches_how`.
///
/// Note that non-files are pruned by default.
pub fn path(
path: &mut PathBuf,
rela_path: &mut BString,
filename_start_idx: usize,
disk_kind: Option,
on_demand_disk_kind: impl FnOnce() -> Option,
Options {
ignore_case,
recurse_repositories,
emit_ignored,
for_deletion,
classify_untracked_bare_repositories,
symlinks_to_directories_are_ignored_like_directories,
worktree_relative_worktree_dirs,
..
}: Options<'_>,
ctx: &mut Context<'_>,
) -> Result {
let mut out = Outcome {
status: entry::Status::Pruned,
property: None,
disk_kind,
index_kind: None,
pathspec_match: None,
};
if is_eq(rela_path[filename_start_idx..].as_bstr(), ".git", ignore_case) {
out.pathspec_match = ctx
.pathspec
.pattern_matching_relative_path(
rela_path.as_bstr(),
disk_kind.map(|ft| ft.is_dir()),
ctx.pathspec_attributes,
)
.map(Into::into);
if for_deletion.is_some() {
if let Some(excluded) = ctx
.excludes
.as_mut()
.map_or(Ok(None), |stack| {
stack
.at_entry(
rela_path.as_bstr(),
disk_kind.map(|ft| is_dir_to_mode(ft.is_dir())),
ctx.objects,
)
.map(|platform| platform.excluded_kind())
})
.map_err(Error::ExcludesAccess)?
.filter(|_| filename_start_idx > 0)
{
out.status = entry::Status::Ignored(excluded);
}
}
out.property = entry::Property::DotGit.into();
return Ok(out);
}
let pathspec_could_match = ctx
.pathspec
.can_match_relative_path(rela_path.as_bstr(), disk_kind.map(|ft| ft.is_dir()));
if !pathspec_could_match {
return Ok(out.with_status(entry::Status::Pruned));
}
let (uptodate_index_kind, index_kind, property) = resolve_file_type_with_index(
rela_path,
ctx.index,
ctx.ignore_case_index_lookup.filter(|_| ignore_case),
);
let mut kind = uptodate_index_kind.or(disk_kind).or_else(on_demand_disk_kind);
// We always check the pathspec to have the value filled in reliably.
out.pathspec_match = ctx
.pathspec
.pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes)
.map(Into::into);
if worktree_relative_worktree_dirs.map_or(false, |worktrees| worktrees.contains(&*rela_path)) {
return Ok(out
.with_kind(Some(entry::Kind::Repository), None)
.with_status(entry::Status::Tracked));
}
let maybe_status = if property.is_none() {
(index_kind.map(|k| k.is_dir()) == kind.map(|k| k.is_dir())).then_some(entry::Status::Tracked)
} else {
out.property = property;
Some(entry::Status::Pruned)
};
let is_dir = if symlinks_to_directories_are_ignored_like_directories
&& ctx.excludes.is_some()
&& kind.map_or(false, |ft| ft == entry::Kind::Symlink)
{
path.metadata().ok().map(|md| is_dir_to_mode(md.is_dir()))
} else {
kind.map(|ft| is_dir_to_mode(ft.is_dir()))
};
let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| {
maybe_upgrade_to_repository(
current_kind,
find_harder,
recurse_repositories,
path,
ctx.current_dir,
ctx.git_dir_realpath,
)
};
if let Some(status) = maybe_status {
if kind == Some(entry::Kind::Directory) && index_kind == Some(entry::Kind::Repository) {
kind = maybe_upgrade_to_repository(kind, false);
}
return Ok(out.with_status(status).with_kind(kind, index_kind));
}
debug_assert!(maybe_status.is_none(), "It only communicates a single state right now");
if let Some(excluded) = ctx
.excludes
.as_mut()
.filter(|_| !rela_path.is_empty())
.map_or(Ok(None), |stack| {
stack
.at_entry(rela_path.as_bstr(), is_dir, ctx.objects)
.map(|platform| platform.excluded_kind())
})
.map_err(Error::ExcludesAccess)?
{
if emit_ignored.is_some() {
if matches!(
for_deletion,
Some(
ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories
| ForDeletionMode::FindRepositoriesInIgnoredDirectories
)
) {
kind = maybe_upgrade_to_repository(
kind,
matches!(
for_deletion,
Some(ForDeletionMode::FindRepositoriesInIgnoredDirectories)
),
);
}
if kind.map_or(false, |d| d.is_recursable_dir())
&& (out.pathspec_match.is_none()
|| worktree_relative_worktree_dirs.map_or(false, |worktrees| {
for_deletion.is_some()
&& worktrees
.iter()
.any(|dir| dir.starts_with_str(&*rela_path) && dir.get(rela_path.len()) == Some(&b'/'))
}))
{
// We have patterns that didn't match at all, *yet*, or there are contained worktrees.
// We want to look inside.
out.pathspec_match = Some(PathspecMatch::Prefix);
}
}
return Ok(out
.with_status(entry::Status::Ignored(excluded))
.with_kind(kind, index_kind));
}
debug_assert!(maybe_status.is_none());
let mut status = entry::Status::Untracked;
if kind.map_or(false, |ft| ft.is_dir()) {
kind = maybe_upgrade_to_repository(kind, classify_untracked_bare_repositories);
} else if out.pathspec_match.is_none() {
status = entry::Status::Pruned;
}
Ok(out.with_status(status).with_kind(kind, index_kind))
}
pub fn maybe_upgrade_to_repository(
current_kind: Option,
find_harder: bool,
recurse_repositories: bool,
path: &mut PathBuf,
current_dir: &Path,
git_dir_realpath: &Path,
) -> Option {
if recurse_repositories {
return current_kind;
}
if find_harder {
let mut is_nested_repo = gix_discover::is_git(path).is_ok();
if is_nested_repo {
let git_dir_is_our_own = gix_path::realpath_opts(path, current_dir, gix_path::realpath::MAX_SYMLINKS)
.ok()
.map_or(false, |realpath_candidate| realpath_candidate == git_dir_realpath);
is_nested_repo = !git_dir_is_our_own;
}
if is_nested_repo {
return Some(entry::Kind::Repository);
}
}
path.push(gix_discover::DOT_GIT_DIR);
let mut is_nested_nonbare_repo = gix_discover::is_git(path).is_ok();
if is_nested_nonbare_repo {
let git_dir_is_our_own = gix_path::realpath_opts(path, current_dir, gix_path::realpath::MAX_SYMLINKS)
.ok()
.map_or(false, |realpath_candidate| realpath_candidate == git_dir_realpath);
is_nested_nonbare_repo = !git_dir_is_our_own;
}
path.pop();
if is_nested_nonbare_repo {
Some(entry::Kind::Repository)
} else {
current_kind
}
}
/// Note that `rela_path` is used as buffer for convenience, but will be left as is when this function returns.
/// Also note `maybe_file_type` will be `None` for entries that aren't up-to-date and files, for directories at least one entry must be uptodate.
/// Returns `(maybe_file_type, Option, flags)`, with the last option being a flag set only for sparse directories in the index.
/// `index_file_type` is the type of `rela_path` as available in the index.
///
/// ### Shortcoming
///
/// In case-insensitive mode, if there is an entry `d` and a `D/a` both in the index, we will always find the file `d` first, and always consider
/// the entry as not uptodate, while classifying it as file (the first one we found). As quite a huge exception, this isn't properly represented
/// in the data model, and we emit a trace to make it more obvious when it happens, in case this leads to less expected results.
fn resolve_file_type_with_index(
rela_path: &mut BString,
index: &gix_index::State,
ignore_case: Option<&gix_index::AccelerateLookup<'_>>,
) -> (Option, Option, Option) {
// TODO: either get this to work for icase as well, or remove the need for it. Logic is different in both branches.
let mut special_property = None;
fn entry_to_kinds(entry: &gix_index::Entry) -> (Option, Option) {
let kind = if entry.mode.is_submodule() {
entry::Kind::Repository.into()
} else if entry.mode.contains(gix_index::entry::Mode::FILE) {
entry::Kind::File.into()
} else if entry.mode.contains(gix_index::entry::Mode::SYMLINK) {
entry::Kind::Symlink.into()
} else {
None
};
(
kind.filter(|_| entry.flags.contains(gix_index::entry::Flags::UPTODATE)),
kind,
)
}
fn icase_directory_to_kinds(dir: Option<&gix_index::Entry>) -> (Option, Option) {
let index_kind = dir.map(|_| entry::Kind::Directory);
let uptodate_kind = dir
.filter(|entry| entry.flags.contains(gix_index::entry::Flags::UPTODATE))
.map(|_| entry::Kind::Directory);
(uptodate_kind, index_kind)
}
// TODO(perf): multi-threaded hash-table so it's always used, even for case-sensitive lookups, just like Git does it.
let (uptodate_kind, index_kind) = if let Some(accelerate) = ignore_case {
match index.entry_by_path_icase(rela_path.as_bstr(), true, accelerate) {
None => {
icase_directory_to_kinds(index.entry_closest_to_directory_icase(rela_path.as_bstr(), true, accelerate))
}
Some(entry) => {
let icase_dir = index.entry_closest_to_directory_icase(rela_path.as_bstr(), true, accelerate);
let directory_matches_exactly = icase_dir.map_or(false, |dir| {
let path = dir.path(index);
let slash_idx = path.rfind_byte(b'/').expect("dir");
path[..slash_idx].as_bstr() == rela_path
});
if directory_matches_exactly {
icase_directory_to_kinds(icase_dir)
} else {
entry_to_kinds(entry)
}
}
}
} else {
match index.entry_by_path(rela_path.as_bstr()) {
None => {
rela_path.push(b'/');
let res = index.prefixed_entries_range(rela_path.as_bstr());
rela_path.pop();
let mut one_index_signalling_with_cone = None;
let mut all_excluded_from_worktree_non_cone = false;
let is_tracked = res.is_some();
let kind = res
.filter(|range| {
if range.len() == 1 {
one_index_signalling_with_cone = range.start.into();
}
let entries = &index.entries()[range.clone()];
let any_up_to_date = entries
.iter()
.any(|e| e.flags.contains(gix_index::entry::Flags::UPTODATE));
if !any_up_to_date && one_index_signalling_with_cone.is_none() {
all_excluded_from_worktree_non_cone = entries
.iter()
.all(|e| e.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE));
}
any_up_to_date
})
.map(|_| entry::Kind::Directory);
if all_excluded_from_worktree_non_cone
|| one_index_signalling_with_cone
.filter(|_| kind.is_none())
.map_or(false, |idx| index.entries()[idx].mode.is_sparse())
{
special_property = Some(entry::Property::TrackedExcluded);
}
(kind, is_tracked.then_some(entry::Kind::Directory))
}
Some(entry) => entry_to_kinds(entry),
}
};
(uptodate_kind, index_kind, special_property)
}
fn is_eq(lhs: &BStr, rhs: impl AsRef, ignore_case: bool) -> bool {
if ignore_case {
lhs.eq_ignore_ascii_case(rhs.as_ref().as_ref())
} else {
lhs == rhs.as_ref()
}
}
fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode {
if is_dir {
gix_index::entry::Mode::DIR
} else {
gix_index::entry::Mode::FILE
}
}
gix-dir-0.11.0/src/walk/function.rs 0000644 0000000 0000000 00000020143 10461020230 0015166 0 ustar 0000000 0000000 use std::borrow::Cow;
use std::path::{Path, PathBuf};
use bstr::{BStr, BString, ByteSlice};
use crate::walk::{classify, readdir, Action, Context, Delegate, Error, ForDeletionMode, Options, Outcome};
use crate::{entry, EntryRef};
/// A function to perform a git-style, unsorted, directory walk.
///
/// * `worktree_root` - the top-most root of the worktree, which must be a prefix to `root`.
/// - If [`Options::precompose_unicode`] is enabled, this path must be precomposed.
/// - The starting point of the traversal (traversal root) is calculated from by doing `worktree_root + pathspec.common_prefix()`.
/// - Note that if the traversal root leading to this directory or it itself is excluded, it will be provided to [`Delegate::emit()`]
/// without further traversal.
/// - If [`Options::precompose_unicode`] is enabled, all involved paths must be precomposed.
/// - Must be contained in `worktree_root`.
/// * `ctx` - everything needed to classify the paths seen during the traversal.
/// * `delegate` - an implementation of [`Delegate`] to control details of the traversal and receive its results.
///
/// Returns `(outcome, traversal_root)`, with the `traversal_root` actually being used for the traversal,
/// useful to transform the paths returned for the user. It's always within the `worktree_root`, or the same,
/// but is hard to guess due to additional logic affecting it.
///
/// ### Performance Notes
///
/// In theory, parallel directory traversal can be significantly faster, and what's possible for our current
/// `gix_features::fs::WalkDir` implementation is to abstract a `filter_entry()` method so it works both for
/// the iterator from the `walkdir` crate as well as from `jwalk`. However, doing so as initial version
/// has the risk of not being significantly harder if not impossible to implement as flow-control is very
/// limited.
///
/// Thus the decision was made to start out with something akin to the Git implementation, get all tests and
/// baseline comparison to pass, and see if an iterator with just `filter_entry` would be capable of dealing with
/// it. Note that `filter_entry` is the only just-in-time traversal control that `walkdir` offers, even though
/// one could consider switching to `jwalk` and just use its single-threaded implementation if a unified interface
/// is necessary to make this work - `jwalk` has a more powerful API for this to work.
///
/// If that was the case, we are talking about 0.5s for single-threaded traversal (without doing any extra work)
/// or 0.25s for optimal multi-threaded performance, all in the WebKit directory with 388k items to traverse.
/// Thus, the speedup could easily be 2x or more and thus worth investigating in due time.
pub fn walk(
worktree_root: &Path,
mut ctx: Context<'_>,
options: Options<'_>,
delegate: &mut dyn Delegate,
) -> Result<(Outcome, PathBuf), Error> {
let root = match ctx.explicit_traversal_root {
Some(root) => root.to_owned(),
None => ctx
.pathspec
.longest_common_directory()
.and_then(|candidate| {
let candidate = worktree_root.join(candidate);
candidate.is_dir().then_some(candidate)
})
.unwrap_or_else(|| worktree_root.join(ctx.pathspec.prefix_directory())),
};
let _span = gix_trace::coarse!("walk", root = ?root, worktree_root = ?worktree_root, options = ?options);
let (mut current, worktree_root_relative) = assure_no_symlink_in_root(worktree_root, &root)?;
let mut out = Outcome::default();
let mut buf = BString::default();
let (root_info, worktree_root_is_repository) = classify::root(
worktree_root,
&mut buf,
worktree_root_relative.as_ref(),
options,
&mut ctx,
)?;
let can_recurse = can_recurse(
buf.as_bstr(),
if root == worktree_root && root_info.disk_kind == Some(entry::Kind::Symlink) && current.is_dir() {
classify::Outcome {
disk_kind: Some(entry::Kind::Directory),
..root_info
}
} else {
root_info
},
options.for_deletion,
worktree_root_is_repository,
delegate,
);
if !can_recurse {
if buf.is_empty() && !root_info.disk_kind.map_or(false, |kind| kind.is_dir()) {
return Err(Error::WorktreeRootIsFile { root: root.to_owned() });
}
if options.precompose_unicode {
buf = gix_utils::str::precompose_bstr(buf.into()).into_owned();
}
let _ = emit_entry(
Cow::Borrowed(buf.as_bstr()),
root_info,
None,
options,
&mut out,
delegate,
);
return Ok((out, root.to_owned()));
}
let mut state = readdir::State::new(worktree_root, ctx.current_dir, options.for_deletion.is_some());
let may_collapse = root != worktree_root && state.may_collapse(¤t);
let (action, _) = readdir::recursive(
may_collapse,
&mut current,
&mut buf,
root_info,
&mut ctx,
options,
delegate,
&mut out,
&mut state,
)?;
if action != Action::Cancel {
state.emit_remaining(may_collapse, options, &mut out, delegate);
assert_eq!(state.on_hold.len(), 0, "BUG: after emission, on hold must be empty");
}
gix_trace::debug!(statistics = ?out);
Ok((out, root.to_owned()))
}
/// Note that we only check symlinks on the way from `worktree_root` to `root`,
/// so `worktree_root` may go through a symlink.
/// Returns `(worktree_root, normalized_worktree_relative_root)`.
fn assure_no_symlink_in_root<'root>(
worktree_root: &Path,
root: &'root Path,
) -> Result<(PathBuf, Cow<'root, Path>), Error> {
let mut current = worktree_root.to_owned();
let worktree_relative = root
.strip_prefix(worktree_root)
.expect("BUG: root was created from worktree_root + prefix");
let worktree_relative = gix_path::normalize(worktree_relative.into(), Path::new(""))
.ok_or(Error::NormalizeRoot { root: root.to_owned() })?;
for (idx, component) in worktree_relative.components().enumerate() {
current.push(component);
let meta = current.symlink_metadata().map_err(|err| Error::SymlinkMetadata {
source: err,
path: current.to_owned(),
})?;
if meta.is_symlink() {
return Err(Error::SymlinkInRoot {
root: root.to_owned(),
worktree_root: worktree_root.to_owned(),
component_index: idx,
});
}
}
Ok((current, worktree_relative))
}
pub(super) fn can_recurse(
rela_path: &BStr,
info: classify::Outcome,
for_deletion: Option,
worktree_root_is_repository: bool,
delegate: &mut dyn Delegate,
) -> bool {
let is_dir = info.disk_kind.map_or(false, |k| k.is_dir());
if !is_dir {
return false;
}
delegate.can_recurse(
EntryRef::from_outcome(Cow::Borrowed(rela_path), info),
for_deletion,
worktree_root_is_repository,
)
}
/// Possibly emit an entry to `for_each` in case the provided information makes that possible.
#[allow(clippy::too_many_arguments)]
pub(super) fn emit_entry(
rela_path: Cow<'_, BStr>,
info: classify::Outcome,
dir_status: Option,
Options {
emit_pruned,
emit_tracked,
emit_ignored,
emit_empty_directories,
..
}: Options<'_>,
out: &mut Outcome,
delegate: &mut dyn Delegate,
) -> Action {
out.seen_entries += 1;
if (!emit_empty_directories && info.property == Some(entry::Property::EmptyDirectory)
|| !emit_tracked && info.status == entry::Status::Tracked)
|| emit_ignored.is_none() && matches!(info.status, entry::Status::Ignored(_))
|| !emit_pruned
&& (info.status.is_pruned()
|| info
.pathspec_match
.map_or(true, |m| m == entry::PathspecMatch::Excluded))
{
return Action::Continue;
}
out.returned_entries += 1;
delegate.emit(EntryRef::from_outcome(rela_path, info), dir_status)
}
gix-dir-0.11.0/src/walk/mod.rs 0000644 0000000 0000000 00000041405 10461020230 0014124 0 ustar 0000000 0000000 use crate::{entry, EntryRef};
use bstr::{BStr, BString};
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
/// A type returned by the [`Delegate::emit()`] as passed to [`walk()`](function::walk()).
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[must_use]
pub enum Action {
/// Continue the traversal as normal.
Continue,
/// Do not continue the traversal, but exit it.
Cancel,
}
/// Ready-made delegate implementations.
pub mod delegate {
use crate::walk::Action;
use crate::{entry, walk, Entry, EntryRef};
type Entries = Vec<(Entry, Option)>;
/// A [`Delegate`](walk::Delegate) implementation that collects all `entries` along with their directory status, if present.
///
/// Note that this allocates for each entry.
#[derive(Default)]
pub struct Collect {
/// All collected entries, in any order.
pub unorded_entries: Entries,
}
impl Collect {
/// Return the list of entries that were emitted, sorted ascending by their repository-relative tree path.
pub fn into_entries_by_path(mut self) -> Entries {
self.unorded_entries.sort_by(|a, b| a.0.rela_path.cmp(&b.0.rela_path));
self.unorded_entries
}
}
impl walk::Delegate for Collect {
fn emit(&mut self, entry: EntryRef<'_>, dir_status: Option) -> Action {
self.unorded_entries.push((entry.to_owned(), dir_status));
walk::Action::Continue
}
}
}
/// A way for the caller to control the traversal based on provided data.
pub trait Delegate {
/// Called for each observed `entry` *inside* a directory, or the directory itself if the traversal is configured
/// to simplify the result (i.e. if every file in a directory is ignored, emit the containing directory instead
/// of each file), or if the root of the traversal passes through a directory that can't be traversed.
///
/// It will also be called if the `root` in [`walk()`](crate::walk()) itself is matching a particular status,
/// even if it is a file.
///
/// Note that tracked entries will only be emitted if [`Options::emit_tracked`] is `true`.
/// Further, not all pruned entries will be observable as they might be pruned so early that the kind of
/// item isn't yet known. Pruned entries are also only emitted if [`Options::emit_pruned`] is `true`.
///
/// `collapsed_directory_status` is `Some(dir_status)` if this entry was part of a directory with the given
/// `dir_status` that wasn't the same as the one of `entry` and if [Options::emit_collapsed] was
/// [CollapsedEntriesEmissionMode::OnStatusMismatch]. It will also be `Some(dir_status)` if that option
/// was [CollapsedEntriesEmissionMode::All].
fn emit(&mut self, entry: EntryRef<'_>, collapsed_directory_status: Option) -> Action;
/// Return `true` if the given entry can be recursed into. Will only be called if the entry is a physical directory.
/// The base implementation will act like Git does by default in `git status` or `git clean`.
///
/// Use `for_deletion` to specify if the seen entries should ultimately be deleted, which may affect the decision
/// of whether to resource or not.
///
/// If `worktree_root_is_repository` is `true`, then this status is part of the root of an iteration, and the corresponding
/// worktree root is a repository itself. This typically happens for submodules. In this case, recursion rules are relaxed
/// to allow traversing submodule worktrees.
///
/// Note that this method will see all directories, even though not all of them may end up being [emitted](Self::emit()).
/// If this method returns `false`, the `entry` will always be emitted.
fn can_recurse(
&mut self,
entry: EntryRef<'_>,
for_deletion: Option,
worktree_root_is_repository: bool,
) -> bool {
entry.status.can_recurse(
entry.disk_kind,
entry.pathspec_match,
for_deletion,
worktree_root_is_repository,
)
}
}
/// The way entries are emitted using the [Delegate].
///
/// The choice here controls if entries are emitted immediately, or have to be held back.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum EmissionMode {
/// Emit each entry as it matches exactly, without doing any kind of simplification.
///
/// Emissions in this mode are happening as they occur, without any buffering or ordering.
#[default]
Matching,
/// Emit only a containing directory if all of its entries are of the same type.
///
/// Note that doing so is more expensive as it requires us to keep track of all entries in the directory structure
/// until it's clear what to finally emit.
CollapseDirectory,
}
/// The way entries that are contained in collapsed directories are emitted using the [Delegate].
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum CollapsedEntriesEmissionMode {
/// Emit only entries if their status does not match the one of the parent directory that is
/// going to be collapsed.
///
/// E.g. if a directory is determined to be untracked, and the entries in question are ignored,
/// they will be emitted.
///
/// Entries that have the same status will essentially be 'merged' into the collapsing directory
/// and won't be observable anymore.
#[default]
OnStatusMismatch,
/// Emit all entries inside of a collapsed directory to make them observable.
All,
}
/// When the walk is for deletion, assure that we don't collapse directories that have precious files in
/// them, and otherwise assure that no entries are observable that shouldn't be deleted.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum ForDeletionMode {
/// We will stop traversing into ignored directories which may save a lot of time, but also may include nested repositories
/// which might end up being deleted.
#[default]
IgnoredDirectoriesCanHideNestedRepositories,
/// Instead of skipping over ignored directories entirely, we will dive in and find ignored non-bare repositories
/// so these are emitted separately and prevent collapsing. These are assumed to be a directory with `.git` inside.
/// Only relevant when ignored entries are emitted.
FindNonBareRepositoriesInIgnoredDirectories,
/// This is a more expensive form of the above variant as it finds all repositories, bare or non-bare.
FindRepositoriesInIgnoredDirectories,
}
/// Options for use in [`walk()`](function::walk()) function.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Options<'a> {
/// If `true`, the filesystem will store paths as decomposed unicode, i.e. `รค` becomes `"a\u{308}"`, which means that
/// we have to turn these forms back from decomposed to precomposed unicode before storing it in the index or generally
/// using it. This also applies to input received from the command-line, so callers may have to be aware of this and
/// perform conversions accordingly.
/// If `false`, no conversions will be performed.
pub precompose_unicode: bool,
/// If true, the filesystem ignores the case of input, which makes `A` the same file as `a`.
/// This is also called case-folding.
/// Note that [pathspecs](Context::pathspec) must also be using the same defaults, which makes them match case-insensitive
/// automatically.
pub ignore_case: bool,
/// If `true`, we will stop figuring out if any directory that is a candidate for recursion is also a nested repository,
/// which saves time but leads to recurse into it. If `false`, nested repositories will not be traversed.
pub recurse_repositories: bool,
/// If `true`, entries that are pruned and whose [Kind](crate::entry::Kind) is known will be emitted.
pub emit_pruned: bool,
/// If `Some(mode)`, entries that are ignored will be emitted according to the given `mode`.
/// If `None`, ignored entries will not be emitted at all.
pub emit_ignored: Option,
/// When the walk is for deletion, this must be `Some(_)` to assure we don't collapse directories that have precious files in
/// them, and otherwise assure that no entries are observable that shouldn't be deleted.
/// If `None`, precious files are treated like expendable files, which is usually what you want when displaying them
/// for addition to the repository, and the collapse of folders can be more generous in relation to ignored files.
pub for_deletion: Option,
/// If `true`, we will not only find non-bare repositories in untracked directories, but also bare ones.
///
/// Note that this is very costly, but without it, bare repositories will appear like untracked directories when collapsed,
/// and they will be recursed into.
pub classify_untracked_bare_repositories: bool,
/// If `true`, we will also emit entries for tracked items. Otherwise these will remain 'hidden', even if a pathspec directly
/// refers to it.
pub emit_tracked: bool,
/// Controls the way untracked files are emitted. By default, this is happening immediately and without any simplification.
pub emit_untracked: EmissionMode,
/// If `true`, emit empty directories as well. Note that a directory also counts as empty if it has any amount or depth of nested
/// subdirectories, as long as none of them includes a file.
/// Thus, this makes leaf-level empty directories visible, as those don't have any content.
pub emit_empty_directories: bool,
/// If `None`, no entries inside of collapsed directories are emitted. Otherwise, act as specified by `Some(mode)`.
pub emit_collapsed: Option,
/// This is a `libgit2` compatibility flag, and if enabled, symlinks that point to directories will be considered a directory
/// when checking for exclusion.
///
/// This is relevant if `src2` points to `src`, and is excluded with `src2/`. If `false`, `src2` will not be excluded,
/// if `true` it will be excluded as the symlink is considered a directory.
///
/// In other words, for Git compatibility this flag should be `false`, the default, for `git2` compatibility it should be `true`.
pub symlinks_to_directories_are_ignored_like_directories: bool,
/// A set of all git worktree checkouts that are located within the main worktree directory.
///
/// They will automatically be detected as 'tracked', but without providing index information (as there is no actual index entry).
/// Note that the unicode composition must match the `precompose_unicode` field so that paths will match verbatim.
pub worktree_relative_worktree_dirs: Option<&'a BTreeSet>,
}
/// All information that is required to perform a dirwalk, and classify paths properly.
pub struct Context<'a> {
/// If not `None`, it will be checked before entering any directory to trigger early interruption.
///
/// If this flag is `true` at any point in the iteration, it will abort with an error.
pub should_interrupt: Option<&'a AtomicBool>,
/// The `git_dir` of the parent repository, after a call to [`gix_path::realpath()`].
///
/// It's used to help us differentiate our own `.git` directory from nested unrelated repositories,
/// which is needed if `core.worktree` is used to nest the `.git` directory deeper within.
pub git_dir_realpath: &'a std::path::Path,
/// The current working directory as returned by `gix_fs::current_dir()` to assure it respects `core.precomposeUnicode`.
/// It's used to produce the realpath of the git-dir of a repository candidate to assure it's not our own repository.
///
/// It is also used to assure that when the walk is for deletion, that the current working dir will not be collapsed.
pub current_dir: &'a std::path::Path,
/// The index to quickly understand if a file or directory is tracked or not.
///
/// ### Important
///
/// The index must have been validated so that each entry that is considered up-to-date will have the [gix_index::entry::Flags::UPTODATE] flag
/// set. Otherwise the index entry is not considered and a disk-access may occur which is costly.
pub index: &'a gix_index::State,
/// A utility to lookup index entries faster, and deal with ignore-case handling.
///
/// Must be set if `ignore_case` is `true`, or else some entries won't be found if their case is different.
///
/// ### Deviation
///
/// Git uses a name-based hash (for looking up entries, not directories) even when operating
/// in case-sensitive mode. It does, however, skip the directory hash creation (for looking
/// up directories) unless `core.ignoreCase` is enabled.
///
/// We only use the hashmap when available and when [`ignore_case`](Options::ignore_case) is enabled in the options.
pub ignore_case_index_lookup: Option<&'a gix_index::AccelerateLookup<'a>>,
/// A pathspec to use as filter - we only traverse into directories if it matches.
/// Note that the `ignore_case` setting it uses should match our [Options::ignore_case].
/// If no such filtering is desired, pass an empty `pathspec` which will match everything.
pub pathspec: &'a mut gix_pathspec::Search,
/// The `attributes` callback for use in [gix_pathspec::Search::pattern_matching_relative_path()], which happens when
/// pathspecs use attributes for filtering.
/// If `pathspec` isn't empty, this function may be called if pathspecs perform attribute lookups.
pub pathspec_attributes: &'a mut dyn FnMut(
&BStr,
gix_pathspec::attributes::glob::pattern::Case,
bool,
&mut gix_pathspec::attributes::search::Outcome,
) -> bool,
/// A way to query the `.gitignore` files to see if a directory or file is ignored.
/// Set to `None` to not perform any work on checking for ignored, which turns previously ignored files into untracked ones, a useful
/// operation when trying to add ignored files to a repository.
pub excludes: Option<&'a mut gix_worktree::Stack>,
/// Access to the object database for use with `excludes` - it's possible to access `.gitignore` files in the index if configured.
pub objects: &'a dyn gix_object::Find,
/// If not `None`, override the traversal root that is computed and use this one instead.
///
/// This can be useful if the traversal root may be a file, in which case the traversal will
/// still be returning possibly matching root entries.
///
/// ### Panics
///
/// If the `traversal_root` is not in the `worktree_root` passed to [walk()](crate::walk()).
pub explicit_traversal_root: Option<&'a std::path::Path>,
}
/// Additional information collected as outcome of [`walk()`](function::walk()).
#[derive(Default, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct Outcome {
/// The amount of calls to read the directory contents.
pub read_dir_calls: u32,
/// The amount of returned entries provided to the callback. This number can be lower than `seen_entries`.
pub returned_entries: usize,
/// The amount of entries, prior to pathspecs filtering them out or otherwise excluding them.
pub seen_entries: u32,
}
/// The error returned by [`walk()`](function::walk()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Interrupted")]
Interrupted,
#[error("Worktree root at '{}' is not a directory", root.display())]
WorktreeRootIsFile { root: PathBuf },
#[error("Traversal root '{}' contains relative path components and could not be normalized", root.display())]
NormalizeRoot { root: PathBuf },
#[error("A symlink was found at component {component_index} of traversal root '{}' as seen from worktree root '{}'", root.display(), worktree_root.display())]
SymlinkInRoot {
root: PathBuf,
worktree_root: PathBuf,
/// This index starts at 0, with 0 being the first component.
component_index: usize,
},
#[error("Failed to update the excludes stack to see if a path is excluded")]
ExcludesAccess(std::io::Error),
#[error("Failed to read the directory at '{}'", path.display())]
ReadDir { path: PathBuf, source: std::io::Error },
#[error("Could not obtain directory entry in root of '{}'", parent_directory.display())]
DirEntry {
parent_directory: PathBuf,
source: std::io::Error,
},
#[error("Could not obtain filetype of directory entry '{}'", path.display())]
DirEntryFileType { path: PathBuf, source: std::io::Error },
#[error("Could not obtain symlink metadata on '{}'", path.display())]
SymlinkMetadata { path: PathBuf, source: std::io::Error },
}
mod classify;
pub(crate) mod function;
mod readdir;
gix-dir-0.11.0/src/walk/readdir.rs 0000644 0000000 0000000 00000034501 10461020230 0014756 0 ustar 0000000 0000000 use bstr::{BStr, BString, ByteSlice};
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use crate::entry::{PathspecMatch, Status};
use crate::walk::function::{can_recurse, emit_entry};
use crate::walk::EmissionMode::CollapseDirectory;
use crate::walk::{
classify, Action, CollapsedEntriesEmissionMode, Context, Delegate, Error, ForDeletionMode, Options, Outcome,
};
use crate::{entry, walk, Entry, EntryRef};
/// ### Deviation
///
/// Git mostly silently ignores IO errors and stops iterating seemingly quietly, while we error loudly.
#[allow(clippy::too_many_arguments)]
pub(super) fn recursive(
may_collapse: bool,
current: &mut PathBuf,
current_bstr: &mut BString,
current_info: classify::Outcome,
ctx: &mut Context<'_>,
opts: Options<'_>,
delegate: &mut dyn Delegate,
out: &mut Outcome,
state: &mut State,
) -> Result<(Action, bool), Error> {
if ctx.should_interrupt.map_or(false, |flag| flag.load(Ordering::Relaxed)) {
return Err(Error::Interrupted);
}
out.read_dir_calls += 1;
let entries = gix_fs::read_dir(current, opts.precompose_unicode).map_err(|err| Error::ReadDir {
path: current.to_owned(),
source: err,
})?;
let mut num_entries = 0;
let mark = state.mark(may_collapse);
let mut prevent_collapse = false;
for entry in entries {
let entry = entry.map_err(|err| Error::DirEntry {
parent_directory: current.to_owned(),
source: err,
})?;
// Important to count right away, otherwise the directory could be seen as empty even though it's not.
// That is, this should be independent of the kind.
num_entries += 1;
let prev_len = current_bstr.len();
if prev_len != 0 {
current_bstr.push(b'/');
}
let file_name = entry.file_name();
current_bstr.extend_from_slice(
gix_path::try_os_str_into_bstr(Cow::Borrowed(file_name.as_ref()))
.expect("no illformed UTF-8")
.as_ref(),
);
current.push(file_name);
let mut info = classify::path(
current,
current_bstr,
if prev_len == 0 { 0 } else { prev_len + 1 },
None,
|| entry.file_type().ok().map(Into::into),
opts,
ctx,
)?;
if can_recurse(
current_bstr.as_bstr(),
info,
opts.for_deletion,
false, /* is root */
delegate,
) {
let subdir_may_collapse = state.may_collapse(current);
let (action, subdir_prevent_collapse) = recursive(
subdir_may_collapse,
current,
current_bstr,
info,
ctx,
opts,
delegate,
out,
state,
)?;
prevent_collapse |= subdir_prevent_collapse;
if action != Action::Continue {
return Ok((action, prevent_collapse));
}
} else {
if opts.for_deletion == Some(ForDeletionMode::IgnoredDirectoriesCanHideNestedRepositories)
&& info.disk_kind == Some(entry::Kind::Directory)
&& matches!(info.status, Status::Ignored(_))
{
info.disk_kind = classify::maybe_upgrade_to_repository(
info.disk_kind,
true,
false,
current,
ctx.current_dir,
ctx.git_dir_realpath,
);
}
if !state.held_for_directory_collapse(current_bstr.as_bstr(), info, &opts) {
let action = emit_entry(Cow::Borrowed(current_bstr.as_bstr()), info, None, opts, out, delegate);
if action != Action::Continue {
return Ok((action, prevent_collapse));
}
}
}
current_bstr.truncate(prev_len);
current.pop();
}
let res = mark.reduce_held_entries(
num_entries,
state,
&mut prevent_collapse,
current,
current_bstr.as_bstr(),
current_info,
opts,
out,
ctx,
delegate,
);
Ok((res, prevent_collapse))
}
pub(super) struct State {
/// The entries to hold back until it's clear what to do with them.
pub on_hold: Vec,
/// The path the user is currently in, as seen from the workdir root.
worktree_relative_current_dir: Option,
}
impl State {
/// Hold the entry with the given `status` if it's a candidate for collapsing the containing directory.
fn held_for_directory_collapse(&mut self, rela_path: &BStr, info: classify::Outcome, opts: &Options<'_>) -> bool {
if opts.should_hold(info.status) {
self.on_hold
.push(EntryRef::from_outcome(Cow::Borrowed(rela_path), info).into_owned());
true
} else {
false
}
}
/// Keep track of state we need to later resolve the state.
/// Top-level directories are special, as they don't fold.
fn mark(&self, may_collapse: bool) -> Mark {
Mark {
start_index: self.on_hold.len(),
may_collapse,
}
}
pub(super) fn new(worktree_root: &Path, current_dir: &Path, is_delete_mode: bool) -> Self {
let worktree_relative_current_dir = if is_delete_mode {
gix_path::realpath_opts(worktree_root, current_dir, gix_path::realpath::MAX_SYMLINKS)
.ok()
.and_then(|real_worktree_root| current_dir.strip_prefix(real_worktree_root).ok().map(ToOwned::to_owned))
.map(|relative_cwd| worktree_root.join(relative_cwd))
} else {
None
};
Self {
on_hold: Vec::new(),
worktree_relative_current_dir,
}
}
/// Returns `true` if the worktree-relative `directory_to_traverse` is not the current working directory.
/// This is only the case when
pub(super) fn may_collapse(&self, directory_to_traverse: &Path) -> bool {
self.worktree_relative_current_dir
.as_ref()
.map_or(true, |cwd| cwd != directory_to_traverse)
}
pub(super) fn emit_remaining(
&mut self,
may_collapse: bool,
opts: Options<'_>,
out: &mut walk::Outcome,
delegate: &mut dyn walk::Delegate,
) {
if self.on_hold.is_empty() {
return;
}
_ = Mark {
start_index: 0,
may_collapse,
}
.emit_all_held(self, opts, out, delegate);
}
}
struct Mark {
start_index: usize,
may_collapse: bool,
}
impl Mark {
#[allow(clippy::too_many_arguments)]
fn reduce_held_entries(
mut self,
num_entries: usize,
state: &mut State,
prevent_collapse: &mut bool,
dir_path: &Path,
dir_rela_path: &BStr,
dir_info: classify::Outcome,
opts: Options<'_>,
out: &mut walk::Outcome,
ctx: &mut Context<'_>,
delegate: &mut dyn walk::Delegate,
) -> walk::Action {
if num_entries == 0 {
let empty_info = classify::Outcome {
property: {
assert_ne!(
dir_info.disk_kind,
Some(entry::Kind::Repository),
"BUG: it shouldn't be possible to classify an empty dir as repository"
);
if !state.may_collapse(dir_path) {
*prevent_collapse = true;
entry::Property::EmptyDirectoryAndCWD.into()
} else if dir_info.property.is_none() {
entry::Property::EmptyDirectory.into()
} else {
dir_info.property
}
},
pathspec_match: ctx
.pathspec
.pattern_matching_relative_path(dir_rela_path, Some(true), ctx.pathspec_attributes)
.map(|m| m.kind.into()),
..dir_info
};
if opts.should_hold(empty_info.status) {
state
.on_hold
.push(EntryRef::from_outcome(Cow::Borrowed(dir_rela_path), empty_info).into_owned());
Action::Continue
} else {
emit_entry(Cow::Borrowed(dir_rela_path), empty_info, None, opts, out, delegate)
}
} else if *prevent_collapse {
self.emit_all_held(state, opts, out, delegate)
} else if let Some(action) = self.try_collapse(dir_rela_path, dir_info, state, out, opts, ctx, delegate) {
action
} else {
*prevent_collapse = true;
self.emit_all_held(state, opts, out, delegate)
}
}
fn emit_all_held(
&mut self,
state: &mut State,
opts: Options<'_>,
out: &mut walk::Outcome,
delegate: &mut dyn walk::Delegate,
) -> Action {
for entry in state.on_hold.drain(self.start_index..) {
let info = classify::Outcome::from(&entry);
let action = emit_entry(Cow::Owned(entry.rela_path), info, None, opts, out, delegate);
if action != Action::Continue {
return action;
}
}
Action::Continue
}
#[allow(clippy::too_many_arguments)]
fn try_collapse(
&self,
dir_rela_path: &BStr,
dir_info: classify::Outcome,
state: &mut State,
out: &mut walk::Outcome,
opts: Options<'_>,
ctx: &mut Context<'_>,
delegate: &mut dyn walk::Delegate,
) -> Option {
if !self.may_collapse {
return None;
}
let (mut expendable, mut precious, mut untracked, mut entries, mut matching_entries) = (0, 0, 0, 0, 0);
for (kind, status, pathspec_match) in state.on_hold[self.start_index..]
.iter()
.map(|e| (e.disk_kind, e.status, e.pathspec_match))
{
entries += 1;
if kind == Some(entry::Kind::Repository) {
return None;
}
if pathspec_match.map_or(false, |m| {
matches!(m, PathspecMatch::Verbatim | PathspecMatch::Excluded)
}) {
return None;
}
matching_entries += usize::from(pathspec_match.map_or(false, |m| !m.should_ignore()));
match status {
Status::Pruned => {
unreachable!("BUG: pruned aren't ever held, check `should_hold()`")
}
Status::Tracked => { /* causes the folder not to be collapsed */ }
Status::Ignored(gix_ignore::Kind::Expendable) => expendable += 1,
Status::Ignored(gix_ignore::Kind::Precious) => precious += 1,
Status::Untracked => untracked += 1,
}
}
if matching_entries != 0 && matching_entries != entries {
return None;
}
let dir_status = if opts.emit_untracked == CollapseDirectory
&& untracked != 0
&& untracked + expendable + precious == entries
&& (opts.for_deletion.is_none()
|| (precious == 0 && expendable == 0)
|| (precious == 0 && opts.emit_ignored.is_some()))
{
entry::Status::Untracked
} else if opts.emit_ignored == Some(CollapseDirectory) {
if expendable != 0 && expendable == entries {
entry::Status::Ignored(gix_ignore::Kind::Expendable)
} else if precious != 0 && precious == entries {
entry::Status::Ignored(gix_ignore::Kind::Precious)
} else {
return None;
}
} else {
return None;
};
if !matches!(dir_status, entry::Status::Untracked | entry::Status::Ignored(_)) {
return None;
}
if !ctx.pathspec.directory_matches_prefix(dir_rela_path, false) {
return None;
}
// Pathspecs affect the collapse of the next level, hence find the highest-value one.
let dir_pathspec_match = state.on_hold[self.start_index..]
.iter()
.filter_map(|e| e.pathspec_match)
.max()
.or_else(|| {
// Only take directory matches as value if they are above the 'guessed' ones.
// Otherwise we end up with seemingly matched entries in the parent directory which
// affects proper folding.
filter_dir_pathspec(dir_info.pathspec_match)
});
let mut removed_without_emitting = 0;
let mut action = Action::Continue;
for entry in state.on_hold.drain(self.start_index..) {
if action != Action::Continue {
removed_without_emitting += 1;
continue;
}
match opts.emit_collapsed {
Some(mode) => {
if mode == CollapsedEntriesEmissionMode::All || entry.status != dir_status {
let info = classify::Outcome::from(&entry);
action = emit_entry(Cow::Owned(entry.rela_path), info, Some(dir_status), opts, out, delegate);
} else {
removed_without_emitting += 1;
}
}
None => removed_without_emitting += 1,
}
}
out.seen_entries += removed_without_emitting as u32;
state.on_hold.push(
EntryRef::from_outcome(
Cow::Borrowed(dir_rela_path),
classify::Outcome {
status: dir_status,
pathspec_match: dir_pathspec_match,
..dir_info
},
)
.into_owned(),
);
Some(action)
}
}
fn filter_dir_pathspec(current: Option) -> Option {
current.filter(|m| {
matches!(
m,
PathspecMatch::Always | PathspecMatch::WildcardMatch | PathspecMatch::Verbatim
)
})
}
impl Options<'_> {
fn should_hold(&self, status: entry::Status) -> bool {
if status.is_pruned() {
return false;
}
self.emit_ignored == Some(CollapseDirectory) || self.emit_untracked == CollapseDirectory
}
}
gix-dir-0.11.0/tests/dir/main.rs 0000644 0000000 0000000 00000000137 10461020230 0014461 0 ustar 0000000 0000000 pub use gix_testtools::Result;
mod walk;
#[path = "../walk_utils/mod.rs"]
pub mod walk_utils;
gix-dir-0.11.0/tests/dir/walk.rs 0000644 0000000 0000000 00000433422 10461020230 0014502 0 ustar 0000000 0000000 use gix_dir::{walk, EntryRef};
use pretty_assertions::assert_eq;
use std::collections::BTreeSet;
use std::sync::atomic::AtomicBool;
use crate::walk_utils::{
collect, collect_filtered, collect_filtered_with_cwd, entry, entry_dirstat, entry_nokind, entry_nomatch, entryps,
entryps_dirstat, fixture, fixture_in, options, options_emit_all, try_collect, try_collect_filtered_opts,
try_collect_filtered_opts_collect, try_collect_filtered_opts_collect_with_root, EntryExt, Options,
};
use gix_dir::entry;
use gix_dir::entry::Kind::*;
use gix_dir::entry::PathspecMatch::*;
use gix_dir::entry::Property::*;
use gix_dir::entry::Status::*;
use gix_dir::walk::CollapsedEntriesEmissionMode::{All, OnStatusMismatch};
use gix_dir::walk::EmissionMode::*;
use gix_dir::walk::ForDeletionMode;
use gix_ignore::Kind::*;
#[test]
#[cfg(unix)]
fn root_is_fifo() {
let root = fixture_in("fifo", "top-level");
let err = try_collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
gix_dir::walk::Options {
emit_ignored: Some(Matching),
..options()
},
keep,
)
})
.unwrap_err();
assert!(
matches!(err, gix_dir::walk::Error::WorktreeRootIsFile { .. }),
"roots simply need to be directories to work"
);
}
#[test]
#[cfg(unix)]
fn one_top_level_fifo() {
let root = fixture_in("fifo", "single-top-level-fifo");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
gix_dir::walk::Options {
emit_pruned: false,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 2,
}
);
assert_eq!(
entries,
&[entry("top", Untracked, Untrackable),],
"Untrackable entries are like normal files, but with a different state"
);
}
#[test]
#[cfg(unix)]
fn fifo_in_traversal() {
let root = fixture_in("fifo", "two-fifos-two-files");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
gix_dir::walk::Options {
emit_pruned: true,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry("dir-with-file/nested-file", Untracked, File),
entry("dir/nested", Untracked, Untrackable),
entry("file", Untracked, File),
entry("top", Untracked, Untrackable),
],
"Untrackable entries only differ by their disk-kind"
);
}
#[test]
fn symlink_to_dir_can_be_excluded() -> crate::Result {
let root = fixture_in("many-symlinks", "excluded-symlinks-to-dir");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
gix_dir::walk::Options {
emit_ignored: Some(Matching),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 9,
}
);
assert_eq!(
entries,
&[
entry("file1", Ignored(Expendable), Symlink),
entry("file2", Untracked, Symlink),
entry("ignored", Ignored(Expendable), Directory),
entry("ignored-must-be-dir", Ignored(Expendable), Directory),
entry("src/file", Untracked, File),
entry("src1", Ignored(Expendable), Symlink),
entry("src2", Untracked, Symlink), /* marked as src2/ in .gitignore */
],
"by default, symlinks are counted as files only, even if they point to a directory, when handled by the exclude machinery"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
gix_dir::walk::Options {
emit_ignored: Some(Matching),
symlinks_to_directories_are_ignored_like_directories: true,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 9,
}
);
assert_eq!(
entries,
&[
entry("file1", Ignored(Expendable), Symlink),
entry("file2", Untracked, Symlink),
entry("ignored", Ignored(Expendable), Directory),
entry("ignored-must-be-dir", Ignored(Expendable), Directory),
entry("src/file", Untracked, File),
entry("src1", Ignored(Expendable), Symlink),
entry("src2", Ignored(Expendable), Symlink), /* marked as src2/ in .gitignore */
],
"with libgit2 compatibility enabled, symlinks to directories are treated like a directory, not symlink"
);
Ok(())
}
#[test]
fn root_may_not_lead_through_symlinks() -> crate::Result {
for (name, intermediate, expected) in [
("immediate-breakout-symlink", "", 0),
("breakout-symlink", "hide", 1),
("breakout-symlink", "hide/../hide", 1),
] {
let root = fixture_in("many-symlinks", name);
let troot = root.join(intermediate).join("breakout");
let err = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| walk(&root, ctx, options(), keep),
None::<&str>,
Default::default(),
)
.unwrap_err();
assert!(
matches!(err, walk::Error::SymlinkInRoot { component_index, .. } if component_index == expected),
"{name} should have component {expected}"
);
}
Ok(())
}
#[test]
fn root_may_be_a_symlink_if_it_is_the_worktree() -> crate::Result {
let root = fixture_in("many-symlinks", "worktree-root-is-symlink");
let ((_out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
gix_dir::walk::Options {
emit_ignored: Some(Matching),
symlinks_to_directories_are_ignored_like_directories: true,
..options()
},
keep,
)
});
assert_eq!(
entries,
&[
entry("file1", Ignored(Expendable), Symlink),
entry("file2", Untracked, Symlink),
entry("ignored", Ignored(Expendable), Directory),
entry("ignored-must-be-dir", Ignored(Expendable), Directory),
entry("src/file", Untracked, File),
entry("src1", Ignored(Expendable), Symlink),
entry("src2", Ignored(Expendable), Symlink), /* marked as src2/ in .gitignore */
],
"it traversed the directory normally - without this capability, symlinked repos can't be traversed"
);
Ok(())
}
#[test]
fn should_interrupt_works_even_in_empty_directories() {
let root = fixture("empty");
let should_interrupt = AtomicBool::new(true);
let err = try_collect_filtered_opts_collect(
&root,
None,
|keep, ctx| walk(&root, ctx, gix_dir::walk::Options { ..options() }, keep),
None::<&str>,
Options {
should_interrupt: Some(&should_interrupt),
..Default::default()
},
)
.unwrap_err();
assert!(matches!(err, gix_dir::walk::Error::Interrupted));
}
#[test]
fn empty_root() -> crate::Result {
let root = fixture("empty");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries.len(),
0,
"by default, nothing is shown as the directory is empty"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_empty_directories: true,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry("", Untracked, Directory).with_property(EmptyDirectory)],
"this is how we can indicate the worktree is entirely untracked"
);
Ok(())
}
#[test]
fn complex_empty() -> crate::Result {
let root = fixture("complex-empty");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 9,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
&[
entry("dirs-and-files/dir/file", Untracked, File),
entry("dirs-and-files/sub", Untracked, Directory).with_property(EmptyDirectory),
entry("empty-toplevel", Untracked, Directory).with_property(EmptyDirectory),
entry("only-dirs/other", Untracked, Directory).with_property(EmptyDirectory),
entry("only-dirs/sub/subsub", Untracked, Directory).with_property(EmptyDirectory),
],
"we see each and every directory, and get it classified as empty as it's set to be emitted"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_empty_directories: false,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 9,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
&[entry("dirs-and-files/dir/file", Untracked, File),],
"by default, no empty directory shows up"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_empty_directories: true,
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 9,
returned_entries: entries.len(),
seen_entries: 9,
}
);
assert_eq!(
entries,
&[
entry("dirs-and-files", Untracked, Directory),
entry("empty-toplevel", Untracked, Directory).with_property(EmptyDirectory),
entry("only-dirs", Untracked, Directory),
],
"empty directories collapse just fine"
);
Ok(())
}
#[test]
fn ignored_with_prefix_pathspec_collapses_just_like_untracked() -> crate::Result {
let root = fixture("untracked-and-ignored-for-collapse");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_untracked: CollapseDirectory,
emit_ignored: Some(CollapseDirectory),
..options()
},
keep,
)
},
["untracked", "no-match"],
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 6,
}
);
assert_eq!(
entries,
[entryps("untracked", Untracked, Directory, Prefix)],
"prefix matches allow untracked directories to collapse"
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_untracked: CollapseDirectory,
emit_ignored: Some(CollapseDirectory),
..options()
},
keep,
)
},
["ignored", "ignored-inside"],
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 4,
returned_entries: entries.len(),
seen_entries: 8,
}
);
assert_eq!(
entries,
[
entryps("ignored", Ignored(Expendable), Directory, Prefix),
entryps("ignored-inside", Ignored(Expendable), Directory, Prefix)
],
"prefix matches allow ignored directories to collapse as well"
);
Ok(())
}
#[test]
fn ignored_dir_with_cwd_handling() -> crate::Result {
let root = fixture("untracked-and-ignored-for-collapse");
let ((out, _root), entries) = collect_filtered_with_cwd(
&root,
Some(&root.join("ignored")),
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_ignored: Some(CollapseDirectory),
..options()
},
keep,
)
},
None::<&str>,
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
entries,
[entryps("ignored", Ignored(Expendable), Directory, Prefix)],
"even if the traversal root is for deletion, unless the CWD is set it will be collapsed (no special cases)"
);
let real_root = gix_path::realpath(&root)?;
let ((out, _root), entries) = collect_filtered_with_cwd(
&real_root,
Some(&real_root.join("ignored")),
Some("ignored"),
|keep, ctx| {
walk(
&real_root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_ignored: Some(CollapseDirectory),
..options()
},
keep,
)
},
None::<&str>,
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 2,
}
);
assert_eq!(
entries,
[
entryps("ignored/b", Ignored(Expendable), File, Prefix),
],
"the traversal starts from the top, but we automatically prevent the 'd' directory from being deleted by stopping its collapse."
);
let real_root = gix_path::realpath(fixture("subdir-untracked-and-ignored"))?;
let ((out, _root), entries) = collect_filtered_with_cwd(
&real_root,
None,
Some("d/d/generated"),
|keep, ctx| {
walk(
&real_root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_ignored: Some(CollapseDirectory),
emit_pruned: false,
..options()
},
keep,
)
},
Some(":/*generated/*"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 8,
returned_entries: entries.len(),
seen_entries: 26
}
);
assert_eq!(
entries,
[
entryps("d/d/generated/b", Ignored(Expendable), File, WildcardMatch),
entryps("d/generated", Ignored(Expendable), Directory, WildcardMatch),
entryps("generated", Ignored(Expendable), Directory, WildcardMatch),
],
"'d/d/generated/b' is there because the parent directory isn't allowed to fold due to the CWD rule."
);
Ok(())
}
#[test]
fn ignored_with_cwd_handling() -> crate::Result {
let root = gix_path::realpath(fixture("ignored-with-empty"))?;
let ((out, _root), entries) = collect_filtered_with_cwd(
&root,
None,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_ignored: Some(CollapseDirectory),
emit_empty_directories: true,
..options()
},
keep,
)
},
None::<&str>,
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
entries,
[entry("target", Ignored(Expendable), Directory),],
"the baseline shows the content"
);
let ((out, _root), entries) = collect_filtered_with_cwd(
&root,
Some(&root),
Some("target/empty"),
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_ignored: Some(CollapseDirectory),
emit_empty_directories: true,
..options()
},
keep,
)
},
Some("target"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 5,
returned_entries: entries.len(),
seen_entries: 7,
}
);
assert_eq!(
entries,
[
entryps("target/debug", Ignored(Expendable), Directory, Prefix),
entryps("target/empty", Ignored(Expendable), Directory, Prefix).with_property(EmptyDirectoryAndCWD),
entryps("target/release", Ignored(Expendable), Directory, Prefix),
],
"it detects empty as CWD (very special case) and lists it as usual, while also preventing collapse to assure \
to not accidentally end up trying to delete a parent directory"
);
Ok(())
}
#[test]
fn only_untracked_with_cwd_handling() -> crate::Result {
let root = fixture("only-untracked");
let ((out, _root), entries) = collect_filtered_with_cwd(
&root,
None,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
},
None::<&str>,
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 9,
}
);
assert_eq!(
entries,
[
entry("a", Untracked, File),
entry("b", Untracked, File),
entry("c", Untracked, File),
entry("d", Untracked, Directory),
],
"the top-level is never collapsed, as our CWD is the worktree root"
);
let ((out, _root), entries) = collect_filtered_with_cwd(
&root,
Some(&root.join("d")),
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
},
None::<&str>,
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
[entryps("d", Untracked, Directory, Prefix),],
"even if the traversal root is for deletion, unless the CWD is set it will be collapsed (no special cases)"
);
let real_root = gix_path::realpath(&root)?;
let ((out, _root), entries) = collect_filtered_with_cwd(
&real_root,
Some(&real_root),
Some("d"),
|keep, ctx| {
walk(
&real_root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
},
None::<&str>,
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 8,
}
);
assert_eq!(
entries,
[
entry("a", Untracked, File),
entry("b", Untracked, File),
entry("c", Untracked, File),
entry("d/a", Untracked, File),
entry("d/b", Untracked, File),
entry("d/d", Untracked, Directory),
],
"the traversal starts from the top, but we automatically prevent the 'd' directory from being deleted by stopping its collapse."
);
let ((out, _root), entries) = collect_filtered_with_cwd(
&real_root,
None,
Some("d"),
|keep, ctx| {
walk(
&real_root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
},
Some("../d/"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 4,
}
);
assert_eq!(
entries,
[
entryps("d/a", Untracked, File, Prefix),
entryps("d/b", Untracked, File, Prefix),
entryps("d/d", Untracked, Directory, Prefix),
],
"this will correctly detect that the pathspec leads back into our CWD, which wouldn't be the case otherwise"
);
Ok(())
}
#[test]
fn only_untracked_with_pathspec() -> crate::Result {
let root = fixture("only-untracked");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
},
Some("d/"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
[entryps("d", Untracked, Directory, Prefix)],
"this is equivalent as if we use a prefix, as we end up starting the traversal from 'd'"
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: None,
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
},
Some("d/"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
[entryps("d", Untracked, Directory, Prefix)],
"When not deleting things, it's once again the same effect as with a prefix"
);
Ok(())
}
#[test]
fn only_untracked_with_prefix_deletion() -> crate::Result {
let root = fixture("only-untracked");
let troot = root.join("d");
let ((out, _root), entries) = collect(&root, Some(&troot), |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(Default::default()),
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
[entryps("d", Untracked, Directory, Prefix),],
"This is like being inside of 'd', but the CWD is now explicit so we happily fold"
);
let ((out, _root), entries) = collect(&root, Some(&troot), |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: None,
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
[entryps("d", Untracked, Directory, Prefix)],
"However, when not deleting, we can collapse, as we could still add all with 'git add .'"
);
Ok(())
}
#[test]
fn only_untracked() -> crate::Result {
let root = fixture("only-untracked");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 7,
}
);
assert_eq!(
entries,
[
entry("a", Untracked, File),
entry("b", Untracked, File),
entry("c", Untracked, File),
entry("d/a", Untracked, File),
entry("d/b", Untracked, File),
entry("d/d/a", Untracked, File),
]
);
let ((out, _root), entries) =
collect_filtered(&root, None, |keep, ctx| walk(&root, ctx, options(), keep), Some("d/*"));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
entries,
[
entryps("d/a", Untracked, File, WildcardMatch),
entryps("d/b", Untracked, File, WildcardMatch),
entryps("d/d/a", Untracked, File, WildcardMatch),
]
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 7 + 2,
},
"There are 2 extra directories that we fold into, but ultimately discard"
);
assert_eq!(
entries,
[
entry("a", Untracked, File),
entry("b", Untracked, File),
entry("c", Untracked, File),
entry("d", Untracked, Directory),
]
);
Ok(())
}
#[test]
fn only_untracked_explicit_pathspec_selection() -> crate::Result {
let root = fixture("only-untracked");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_untracked: Matching,
..options()
},
keep,
)
},
["d/a", "d/d/a"],
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3,
},
);
assert_eq!(
entries,
[
entryps("d/a", Untracked, File, Verbatim),
entryps("d/d/a", Untracked, File, Verbatim)
],
"this works just like expected, as nothing is collapsed anyway"
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_untracked: CollapseDirectory,
emit_pruned: true,
..options()
},
keep,
)
},
["d/a", "d/d/a"],
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3,
},
"no collapsing happens"
);
assert_eq!(
entries,
[
entryps("d/a", Untracked, File, Verbatim),
entry_nokind("d/b", Pruned),
entryps("d/d/a", Untracked, File, Verbatim)],
"we actually want to mention the entries that matched the pathspec precisely, so two of them would be needed here \
while preventing the directory collapse from happening"
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
},
Some("d/*"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 2 + 3,
},
"collapsing happens just like Git"
);
assert_eq!(
entries,
[entryps("d", Untracked, Directory, WildcardMatch),],
"wildcard matches on the top-level without deletion show just the top level"
);
Ok(())
}
#[test]
fn expendable_and_precious() {
let root = fixture("expendable-and-precious");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 6,
returned_entries: entries.len(),
seen_entries: 18,
}
);
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitignore", Tracked, File),
entry("a.o", Ignored(Expendable), File),
entry("all-expendable", Ignored(Expendable), Directory),
entry("all-expendable-by-filematch/e.o", Ignored(Expendable), File),
entry("all-expendable-by-filematch/f.o", Ignored(Expendable), File),
entry("all-precious", Ignored(Precious), Directory),
entry("all-precious-by-filematch/a.precious", Ignored(Precious), File),
entry("all-precious-by-filematch/b.precious", Ignored(Precious), File),
entry("mixed/b.o", Ignored(Expendable), File),
entry("mixed/precious", Ignored(Precious), File),
entry("precious", Ignored(Precious), File),
entry("some-expendable/file", Tracked, File),
entry("some-expendable/file.o", Ignored(Expendable), File),
entry("some-expendable/new", Untracked, File),
entry("some-precious/file", Tracked, File),
entry("some-precious/file.precious", Ignored(Precious), File),
entry("some-precious/new", Untracked, File),
],
"listing everything is a 'matching' preset, which is among the most efficient."
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_tracked: true,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 6,
returned_entries: entries.len(),
seen_entries: 18 + 2,
}
);
assert_eq!(
entries,
[
entry(".gitignore", Tracked, File),
entry("a.o", Ignored(Expendable), File),
entry("all-expendable", Ignored(Expendable), Directory),
entry("all-expendable-by-filematch", Ignored(Expendable), Directory),
entry("all-precious", Ignored(Precious), Directory),
entry("all-precious-by-filematch", Ignored(Precious), Directory),
entry("mixed/b.o", Ignored(Expendable), File),
entry("mixed/precious", Ignored(Precious), File),
entry("precious", Ignored(Precious), File),
entry("some-expendable/file", Tracked, File),
entry("some-expendable/file.o", Ignored(Expendable), File),
entry("some-expendable/new", Untracked, File),
entry("some-precious/file", Tracked, File),
entry("some-precious/file.precious", Ignored(Precious), File),
entry("some-precious/new", Untracked, File),
],
"those that have tracked and ignored won't be collapsed, nor will be folders that have mixed precious and ignored files,\
those with all files of one type will be collapsed though"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: None,
emit_untracked: CollapseDirectory,
emit_tracked: false,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 6,
returned_entries: entries.len(),
seen_entries: 16 + 2,
}
);
assert_eq!(
entries,
[
entry("some-expendable/new", Untracked, File),
entry("some-precious/new", Untracked, File),
],
"even with collapsing, once there is a tracked file in the directory, we show the untracked file directly"
);
}
#[test]
fn subdir_untracked() -> crate::Result {
let root = fixture("subdir-untracked");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 7,
}
);
assert_eq!(entries, [entry("d/d/a", Untracked, File)]);
let ((out, actual_root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&root),
|keep, ctx| walk(&root, ctx, options(), keep),
Some("d/d/*"),
Default::default(),
)?;
assert_eq!(actual_root, root);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 7,
},
"pruning has no actual effect here as there is no extra directories that could be avoided"
);
assert_eq!(entries, &[entryps("d/d/a", Untracked, File, WildcardMatch)]);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 7 + 1,
},
"there is a folded directory we added"
);
assert_eq!(entries, [entry("d/d", Untracked, Directory)]);
Ok(())
}
#[test]
fn only_untracked_from_subdir() -> crate::Result {
let root = fixture("only-untracked");
let troot = root.join("d").join("d");
let ((out, _root), entries) = collect(&root, Some(&troot), |keep, ctx| walk(&root, ctx, options(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entryps("d/d/a", Untracked, File, Prefix)],
"even from subdirs, paths are worktree relative"
);
Ok(())
}
#[test]
fn untracked_and_ignored_pathspec_guidance() -> crate::Result {
for for_deletion in [None, Some(Default::default())] {
let root = fixture("subdir-untracked-and-ignored");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
for_deletion,
..options()
},
keep,
)
},
Some("d/d/generated/b"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 1,
},
"we have to read the parent directory, just like git, as we can't assume a directory"
);
assert_eq!(
entries,
[entryps("d/d/generated/b", Ignored(Expendable), File, Verbatim)],
"pathspecs allow reaching into otherwise ignored directories, ignoring the flag to collapse"
);
}
Ok(())
}
#[test]
fn untracked_and_ignored_for_deletion_negative_wildcard_spec() -> crate::Result {
let root = fixture("subdir-untracked-and-ignored");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_pruned: true,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
},
Some(":!*generated*"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 5,
returned_entries: entries.len(),
seen_entries: 23,
},
);
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitignore", Untracked, File),
entry("a.o", Ignored(Expendable), File),
entry("b.o", Ignored(Expendable), File),
entry("c.o", Ignored(Expendable), File),
entry("d/a.o", Ignored(Expendable), File),
entry("d/b.o", Ignored(Expendable), File),
entry("d/d/a", Untracked, File),
entry("d/d/a.o", Ignored(Expendable), File),
entry("d/d/b.o", Ignored(Expendable), File),
entryps("d/d/generated", Ignored(Expendable), Directory, Excluded),
entryps("d/generated", Ignored(Expendable), Directory, Excluded),
entryps("generated", Ignored(Expendable), Directory, Excluded),
entry("objs", Ignored(Expendable), Directory),
],
"'generated' folders are excluded, and collapsing is done where possible. \
Note that Git wants to incorrectly delete `d/d` as it doesn't see the excluded \
ignored file inside, which would incorrectly delete something the users didn't want deleted."
);
Ok(())
}
#[test]
fn untracked_and_ignored_for_deletion_positive_wildcard_spec() -> crate::Result {
let root = fixture("subdir-untracked-and-ignored");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_pruned: true,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
},
Some("*generated*"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 8,
returned_entries: entries.len(),
seen_entries: 27,
},
);
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).no_kind().with_property(DotGit),
entry_nomatch(".gitignore", Pruned, File),
entry_nomatch("a.o", Ignored(Expendable), File),
entry_nomatch("b.o", Ignored(Expendable), File),
entry_nomatch("c.o", Ignored(Expendable), File),
entry_nomatch("d/a.o", Ignored(Expendable), File),
entry_nomatch("d/b.o", Ignored(Expendable), File),
entry_nomatch("d/d/a", Pruned, File),
entry_nomatch("d/d/a.o", Ignored(Expendable), File),
entry_nomatch("d/d/b.o", Ignored(Expendable), File),
entryps("d/d/generated", Ignored(Expendable), Directory, WildcardMatch),
entryps("d/generated", Ignored(Expendable), Directory, WildcardMatch),
entryps("generated", Ignored(Expendable), Directory, WildcardMatch),
entry_nomatch("objs", Ignored(Expendable), Directory),
],
"'generated' folders are included, and collapsing is done where possible"
);
Ok(())
}
#[test]
fn untracked_and_ignored_for_deletion_nonmatching_wildcard_spec() -> crate::Result {
let root = fixture("subdir-untracked-and-ignored");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_pruned: true,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
},
Some("*foo*"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 8,
returned_entries: entries.len(),
seen_entries: 28,
},
);
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).no_match().with_property(DotGit),
entry_nomatch(".gitignore", Pruned, File),
entry_nomatch("a.o", Ignored(Expendable), File),
entry_nomatch("b.o", Ignored(Expendable), File),
entry_nomatch("c.o", Ignored(Expendable), File),
entry_nomatch("d/a.o", Ignored(Expendable), File),
entry_nomatch("d/b.o", Ignored(Expendable), File),
entry_nomatch("d/d", Ignored(Expendable), Directory),
entry_nomatch("d/d/a", Pruned, File),
entry_nomatch("d/generated", Ignored(Expendable), Directory),
entry_nomatch("generated", Ignored(Expendable), Directory),
entry_nomatch("objs", Ignored(Expendable), Directory),
],
"'generated' folders are included, and collapsing is done where possible"
);
Ok(())
}
#[test]
fn nested_precious_repo_respects_wildcards() -> crate::Result {
let root = fixture("precious-nested-repository");
for for_deletion in [
Some(ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories),
Some(ForDeletionMode::FindRepositoriesInIgnoredDirectories),
] {
let (_out, entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_pruned: false,
for_deletion,
..options()
},
keep,
)
},
Some("*foo/"),
);
// NOTE: do not use `_out` as `.git` directory contents can change, it's controlled by Git, causing flakiness.
assert_eq!(entries, [], "nothing matches, of course");
}
Ok(())
}
#[test]
fn nested_ignored_dirs_for_deletion_nonmatching_wildcard_spec() -> crate::Result {
let root = fixture("ignored-dir-nested-minimal");
let (_out, entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_pruned: false,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
},
Some("*foo*"),
);
// NOTE: do not use `_out` as `.git` directory contents can change, it's controlled by Git, causing flakiness.
assert_eq!(
entries,
[],
"it figures out that nothing actually matches, even though it has to check everything"
);
let (_out, entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_pruned: true,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
},
Some("*foo*"),
);
// NOTE: do not use `_out` as `.git` directory contents can change, it's controlled by Git, causing flakiness.
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).with_property(DotGit),
entry_nomatch(".gitignore", Pruned, File),
entry_nomatch("bare/HEAD", Pruned, File),
entry_nomatch("bare/info/exclude", Pruned, File),
entry_nomatch("bare/objects", Untracked, Directory),
entry_nomatch("bare/refs", Untracked, Directory),
entry_nomatch("dir", Ignored(Expendable), Directory),
],
"it's possible to observe pruned entries like before"
);
Ok(())
}
#[test]
fn expendable_and_precious_in_ignored_dir_with_pathspec() -> crate::Result {
let root = fixture("expendable-and-precious-nested-in-ignored-dir");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_pruned: true,
emit_tracked: true,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
},
);
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitignore", Tracked, File).with_index_kind(File),
entry("ignored", Ignored(Expendable), Directory),
entry("other", Ignored(Expendable), Directory),
],
"without pathspec, it collapses completely. \
It's interesting that 'other' claims to be ignored - due to the collapse of `other/ignored` it inherits the sub-directory status.\
However, it's what we want, compared to the alternative of leaving it empty, and then detecting it as empty\
the next time we run."
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_pruned: true,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
},
Some("*ignored*"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 9,
returned_entries: entries.len(),
seen_entries: 19,
},
);
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).with_property(DotGit).no_match(),
entry_nokind("ignored/d/.git", Ignored(Expendable) ).with_property(DotGit).with_match(WildcardMatch),
entryps("ignored/d/.gitignore", Ignored(Expendable), File, WildcardMatch),
entryps("ignored/d/a.o", Ignored(Expendable), File, WildcardMatch),
entryps(
"ignored/d/all-expendable",
Ignored(Expendable),
Directory,
WildcardMatch
),
entryps("ignored/d/all-precious", Ignored(Precious), Directory, WildcardMatch),
entryps("ignored/d/mixed", Ignored(Expendable), Directory, WildcardMatch),
entryps("ignored/d/precious", Ignored(Expendable), File, WildcardMatch),
entryps("other", Ignored(Expendable), Directory, WildcardMatch),
],
"with pathspec, we match what's inside and expect to have all the lowest-level paths that have 'ignored' in them.\
It seems strange that 'precious' isn't precious, while 'all-precious' is. However, the ignore-search is special
as it goes backward through directories (using directory-declarations), and aborts if it matched. Thus it finds
that '$/all-precious/' matched, but in the other cases it matches 'ignored/'.
'other' gets folded and inherits, just like before.
Also, look how the ignore-state overrides the prune-default for DotGit kinds, to have more finegrained classification."
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_pruned: true,
for_deletion: None,
..options()
},
keep,
)
},
Some("*ignored*"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 9,
returned_entries: entries.len(),
seen_entries: 19,
},
);
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).with_property(DotGit).no_match(),
entry_nokind("ignored/d/.git", Pruned)
.with_property(DotGit)
.with_match(WildcardMatch),
entryps("ignored/d/.gitignore", Ignored(Expendable), File, WildcardMatch),
entryps("ignored/d/a.o", Ignored(Expendable), File, WildcardMatch),
entryps(
"ignored/d/all-expendable",
Ignored(Expendable),
Directory,
WildcardMatch
),
entryps("ignored/d/all-precious", Ignored(Precious), Directory, WildcardMatch),
entryps("ignored/d/mixed", Ignored(Expendable), Directory, WildcardMatch),
entryps("ignored/d/precious", Ignored(Expendable), File, WildcardMatch),
entryps("other", Ignored(Expendable), Directory, WildcardMatch),
],
"The same as above, but without delete mode, we don't upgrade the status of ignored dot-git entries"
);
Ok(())
}
#[test]
fn untracked_and_ignored() -> crate::Result {
let root = fixture("subdir-untracked-and-ignored");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(Matching),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 5,
returned_entries: entries.len(),
seen_entries: 21,
},
"some untracked ones are hidden by default"
);
assert_eq!(
entries,
[
entry(".gitignore", Untracked, File),
entry("a.o", Ignored(Expendable), File),
entry("b.o", Ignored(Expendable), File),
entry("c.o", Ignored(Expendable), File),
entry("d/a.o", Ignored(Expendable), File),
entry("d/b.o", Ignored(Expendable), File),
entry("d/d/a", Untracked, File),
entry("d/d/a.o", Ignored(Expendable), File),
entry("d/d/b.o", Ignored(Expendable), File),
entry("d/d/generated", Ignored(Expendable), Directory),
entry("d/generated", Ignored(Expendable), Directory),
entry("generated", Ignored(Expendable), Directory),
entry("objs/a.o", Ignored(Expendable), File),
entry("objs/b.o", Ignored(Expendable), File),
entry("objs/sub/other.o", Ignored(Expendable), File),
]
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_pruned: true,
..options()
},
keep,
)
},
Some("**/a*"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 5,
returned_entries: entries.len(),
seen_entries: 21,
},
"basically the same resultโฆ"
);
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).with_property(DotGit).no_match(),
entry_nomatch(".gitignore", Pruned, File),
entryps("d/d/a", Untracked, File, WildcardMatch),
],
"โฆbut with different classification as the ignore file is pruned so it's not untracked anymore"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: None,
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 5,
returned_entries: entries.len(),
seen_entries: 21 + 1,
},
"we still encounter the same amount of entries, and 1 folded directory"
);
assert_eq!(
entries,
[entry(".gitignore", Untracked, File), entry("d/d", Untracked, Directory)],
"aggregation kicks in here"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 5,
returned_entries: entries.len(),
seen_entries: 21 + 2,
},
"some untracked ones are hidden by default, folded directories"
);
assert_eq!(
entries,
[
entry(".gitignore", Untracked, File),
entry("a.o", Ignored(Expendable), File),
entry("b.o", Ignored(Expendable), File),
entry("c.o", Ignored(Expendable), File),
entry("d/a.o", Ignored(Expendable), File),
entry("d/b.o", Ignored(Expendable), File),
entry("d/d/a", Untracked, File),
entry("d/d/a.o", Ignored(Expendable), File),
entry("d/d/b.o", Ignored(Expendable), File),
entry("d/d/generated", Ignored(Expendable), Directory),
entry("d/generated", Ignored(Expendable), Directory),
entry("generated", Ignored(Expendable), Directory),
entry("objs", Ignored(Expendable), Directory),
],
"objects are aggregated"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 5,
returned_entries: entries.len(),
seen_entries: 21 + 3,
},
"some untracked ones are hidden by default, and folded directories"
);
assert_eq!(
entries,
[
entry(".gitignore", Untracked, File),
entry("a.o", Ignored(Expendable), File),
entry("b.o", Ignored(Expendable), File),
entry("c.o", Ignored(Expendable), File),
entry("d/a.o", Ignored(Expendable), File),
entry("d/b.o", Ignored(Expendable), File),
entry("d/d", Untracked, Directory),
entryps_dirstat("d/d/a.o", Ignored(Expendable), File, Always, Untracked),
entryps_dirstat("d/d/b.o", Ignored(Expendable), File, Always, Untracked),
entryps_dirstat("d/d/generated", Ignored(Expendable), Directory, Always, Untracked),
entry("d/generated", Ignored(Expendable), Directory),
entry("generated", Ignored(Expendable), Directory),
entry("objs", Ignored(Expendable), Directory),
],
"ignored ones are aggregated, and we get the same effect as with `git status --ignored` - collapsing of untracked happens\
and we still list the ignored files that were inside.\
Also note the entries that would be dropped in case of `git clean` are marked with `entry_dirstat`, which would display what's\
done differently."
);
Ok(())
}
#[test]
fn untracked_and_ignored_collapse_handling_mixed() -> crate::Result {
let root = fixture("subdir-untracked-and-ignored");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: None,
..options()
},
keep,
)
},
Some("d/d/b.o"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 4,
},
"it has to read 'd/d' as 'd/d/b.o' isn't a directory candidate"
);
assert_eq!(
entries,
[entryps("d/d/b.o", Ignored(Expendable), File, Verbatim)],
"when files are selected individually, they are never collapsed"
);
for (spec, pathspec_match) in [("d/d/*", WildcardMatch), ("d/d", Prefix), ("d/d/", Prefix)] {
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&root),
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: None,
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
},
Some(spec),
Default::default(),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 4,
returned_entries: entries.len(),
seen_entries: 21,
},
);
assert_eq!(
entries,
[
entryps("d/d", Untracked, Directory, pathspec_match),
entryps_dirstat("d/d/a.o", Ignored(Expendable), File, pathspec_match, Untracked),
entryps_dirstat("d/d/b.o", Ignored(Expendable), File, pathspec_match, Untracked),
entryps_dirstat(
"d/d/generated",
Ignored(Expendable),
Directory,
pathspec_match,
Untracked
),
],
"with wildcard matches, it's OK to collapse though"
);
}
Ok(())
}
#[test]
fn untracked_and_ignored_collapse_handling_mixed_with_prefix() -> crate::Result {
let root = fixture("subdir-untracked-and-ignored");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: None,
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
},
Some("d/d"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 11
},
"this is not a directory, so the prefix is only 'd', not 'd/d'"
);
assert_eq!(
entries,
[
entryps("d/d", Untracked, Directory, Prefix),
entryps_dirstat("d/d/a.o", Ignored(Expendable), File, Prefix, Untracked),
entryps_dirstat("d/d/b.o", Ignored(Expendable), File, Prefix, Untracked),
entryps_dirstat("d/d/generated", Ignored(Expendable), Directory, Prefix, Untracked),
],
"as it's not the top-level anymore (which is 'd', not 'd/d'), we will collapse"
);
for (spec, pathspec_match) in [("d/d/*", WildcardMatch), ("d/d/", Prefix)] {
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: None,
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
},
Some(spec),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 6,
},
);
assert_eq!(
entries,
[
entryps("d/d", Untracked, Directory, pathspec_match),
entryps_dirstat("d/d/a.o", Ignored(Expendable), File, pathspec_match, Untracked),
entryps_dirstat("d/d/b.o", Ignored(Expendable), File, pathspec_match, Untracked),
entryps_dirstat(
"d/d/generated",
Ignored(Expendable),
Directory,
pathspec_match,
Untracked
),
],
"{spec}: with wildcard matches, it's OK to collapse though"
);
}
// TODO: try for deletion, and prefix combinations
Ok(())
}
#[test]
fn untracked_and_ignored_collapse_handling_for_deletion_with_wildcards() -> crate::Result {
let root = fixture("subdir-untracked-and-ignored");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
},
Some("*.o"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 8,
returned_entries: entries.len(),
seen_entries: 26
},
);
assert_eq!(
entries,
[
entryps("a.o", Ignored(Expendable), File, WildcardMatch),
entryps("b.o", Ignored(Expendable), File, WildcardMatch),
entryps("c.o", Ignored(Expendable), File, WildcardMatch),
entryps("d/a.o", Ignored(Expendable), File, WildcardMatch),
entryps("d/b.o", Ignored(Expendable), File, WildcardMatch),
entryps("d/d/a.o", Ignored(Expendable), File, WildcardMatch,),
entryps("d/d/b.o", Ignored(Expendable), File, WildcardMatch,),
entryps("generated/a.o", Ignored(Expendable), File, WildcardMatch),
entryps("objs", Ignored(Expendable), Directory, WildcardMatch),
],
"when using wildcards like these, we actually want to see only the suffixed items even if they all match, like Git does. \
However, we have no way to differentiate `*` from `*.o`, in which case Git decides to delete the directory instead of its \
contents, so it's not perfect there either. \
Thus we stick to the rule: if everything in the directory is going to be deleted, we delete the whole directory."
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: Some(Default::default()),
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
},
Some("*"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 8,
returned_entries: entries.len(),
seen_entries: 28
},
);
assert_eq!(
entries,
[
entryps(".gitignore", Untracked, File, WildcardMatch),
entryps("a.o", Ignored(Expendable), File, WildcardMatch),
entryps("b.o", Ignored(Expendable), File, WildcardMatch),
entryps("c.o", Ignored(Expendable), File, WildcardMatch),
entryps("d/a.o", Ignored(Expendable), File, WildcardMatch),
entryps("d/b.o", Ignored(Expendable), File, WildcardMatch),
entryps("d/d", Untracked, Directory, WildcardMatch,),
entryps_dirstat("d/d/a.o", Ignored(Expendable), File, WildcardMatch, Untracked),
entryps_dirstat("d/d/b.o", Ignored(Expendable), File, WildcardMatch, Untracked),
entryps_dirstat(
"d/d/generated",
Ignored(Expendable),
Directory,
WildcardMatch,
Untracked
),
entryps("d/generated", Ignored(Expendable), Directory, WildcardMatch),
entryps("generated", Ignored(Expendable), Directory, WildcardMatch),
entryps("objs", Ignored(Expendable), Directory, WildcardMatch),
],
"In this case, Git is doing exactly the same"
);
Ok(())
}
#[test]
fn untracked_and_ignored_collapse_handling_for_deletion_with_prefix_wildcards() -> crate::Result {
let root = fixture("subdir-untracked-and-ignored");
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
},
Some("generated/*.o"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 2,
},
);
assert_eq!(
entries,
[entryps("generated/a.o", Ignored(Expendable), File, WildcardMatch)],
"this is the same result as '*.o', but limited to a subdirectory"
);
Ok(())
}
#[test]
fn untracked_and_ignored_collapse_handling_for_deletion_mixed() -> crate::Result {
let root = fixture("subdir-untracked-and-ignored");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: None,
emit_untracked: CollapseDirectory,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 5,
returned_entries: entries.len(),
seen_entries: 21,
},
);
assert_eq!(
entries,
[entry(".gitignore", Untracked, File), entry("d/d/a", Untracked, File)],
"without ignored files, we only see untracked ones, without a chance to collapse. This actually is something Git fails to do."
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: Some(Default::default()),
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 5,
returned_entries: entries.len(),
seen_entries: 24,
},
);
assert_eq!(
entries,
[
entry(".gitignore", Untracked, File),
entry("a.o", Ignored(Expendable), File),
entry("b.o", Ignored(Expendable), File),
entry("c.o", Ignored(Expendable), File),
entry("d/a.o", Ignored(Expendable), File),
entry("d/b.o", Ignored(Expendable), File),
entry("d/d", Untracked, Directory),
entryps_dirstat("d/d/a.o", Ignored(Expendable), File, Always, Untracked),
entryps_dirstat("d/d/b.o", Ignored(Expendable), File, Always, Untracked),
entryps_dirstat("d/d/generated", Ignored(Expendable), Directory, Always, Untracked),
entry("d/generated", Ignored(Expendable), Directory),
entry("generated", Ignored(Expendable), Directory),
entry("objs", Ignored(Expendable), Directory),
],
"with ignored files, we can collapse untracked and ignored like before"
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: Some(Default::default()),
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
},
Some("d/d/*"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 6,
},
);
assert_eq!(
entries,
[
entryps("d/d", Untracked, Directory, WildcardMatch),
entryps_dirstat("d/d/a.o", Ignored(Expendable), File, WildcardMatch, Untracked),
entryps_dirstat("d/d/b.o", Ignored(Expendable), File, WildcardMatch, Untracked),
entryps_dirstat("d/d/generated", Ignored(Expendable), Directory, WildcardMatch, Untracked),
],
"everything is filtered down to the pathspec, otherwise it's like before. Not how all-matching 'generated' collapses, \
but also how 'd/d' collapses as our current working directory the worktree"
);
let real_root = gix_path::realpath(&root)?;
let ((out, _root), entries) = collect_filtered_with_cwd(
&real_root,
Some(&real_root),
Some("d/d"),
|keep, ctx| {
walk(
&real_root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: Some(Default::default()),
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
},
Some("d/d/*"), // NOTE: this would be '*' in the real world and automatically prefixed, but the test-setup is limited
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
},
);
assert_eq!(
entries,
[
entryps("d/d/a", Untracked, File, WildcardMatch),
entryps("d/d/a.o", Ignored(Expendable), File, WildcardMatch),
entryps("d/d/b.o", Ignored(Expendable), File, WildcardMatch),
entryps("d/d/generated", Ignored(Expendable), Directory, WildcardMatch),
],
"Now the CWD is 'd/d', which means we can't collapse it."
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_tracked: true,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
},
Some("d/d/*.o"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
},
);
assert_eq!(
entries,
[
entryps("d/d/a.o", Ignored(Expendable), File, WildcardMatch),
entryps("d/d/b.o", Ignored(Expendable), File, WildcardMatch),
],
"If the wildcard doesn't match everything, it can't be collapsed"
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: Some(Default::default()),
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
},
Some("d/d/"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 6,
},
);
assert_eq!(
entries,
[
entryps("d/d", Untracked, Directory, Prefix),
entryps_dirstat("d/d/a.o", Ignored(Expendable), File, Prefix, Untracked),
entryps_dirstat("d/d/b.o", Ignored(Expendable), File, Prefix, Untracked),
entryps_dirstat("d/d/generated", Ignored(Expendable), Directory, Prefix, Untracked),
],
"Now the whole folder is matched and can collapse, as no CWD is set - the prefix-based root isn't special anymore \
as it is not easily predictable, and has its own rules."
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: None,
emit_untracked: CollapseDirectory,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
},
Some("d/d/"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 4,
},
);
assert_eq!(
entries,
[entryps("d/d/a", Untracked, File, Prefix)],
"a prefix match works similarly"
);
Ok(())
}
#[test]
fn precious_are_not_expendable() {
let root = fixture("untracked-and-precious");
let (_out, entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(Matching),
emit_untracked: Matching,
..options_emit_all()
},
keep,
)
});
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitignore", Tracked, File),
entry("a.o", Ignored(Expendable), File),
entry("d/a", Tracked, File),
entry("d/a.o", Ignored(Expendable), File),
entry("d/b", Tracked, File),
entry("d/b.o", Ignored(Expendable), File),
entry("d/d/a.precious", Ignored(Precious), File),
entry("d/d/new", Untracked, File),
],
"just to have an overview"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 10,
},
);
assert_eq!(
entries,
[
entry("a.o", Ignored(Expendable), File),
entry("d/a.o", Ignored(Expendable), File),
entry("d/b.o", Ignored(Expendable), File),
entry("d/d", Untracked, Directory),
entryps_dirstat("d/d/a.precious", Ignored(Precious), File, Always, Untracked),
],
"by default precious files are treated no differently than expendable files, which is fine\
unless you want to delete `d/d`. Then we shouldn't ever see `d/d` and have to deal with \
a collapsed precious file."
);
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
},
Some("d"),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 10,
},
"'d' is assumed to be a file, hence it's stripped to its base '', yielding one more call."
);
assert_eq!(
entries,
[
entryps("d/a.o", Ignored(Expendable), File, Prefix),
entryps("d/b.o", Ignored(Expendable), File, Prefix),
entryps("d/d", Untracked, Directory, Prefix),
entryps_dirstat("d/d/a.precious", Ignored(Precious), File, Prefix, Untracked),
],
"should yield the same entries - note how collapsed directories inherit the pathspec"
);
for (equivalent_pathspec, expected_match) in [("d/*", WildcardMatch), ("d/", Prefix)] {
let ((out, _root), entries) = collect_filtered(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
emit_collapsed: Some(OnStatusMismatch),
..options()
},
keep,
)
},
Some(equivalent_pathspec),
);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 7,
},
"{equivalent_pathspec}: should yield same result, they also see the 'd' prefix directory"
);
assert_eq!(
entries,
[
entryps("d/a.o", Ignored(Expendable), File, expected_match),
entryps("d/b.o", Ignored(Expendable), File, expected_match),
entryps("d/d", Untracked, Directory, expected_match),
entryps_dirstat("d/d/a.precious", Ignored(Precious), File, expected_match, Untracked),
],
"'{equivalent_pathspec}' should yield the same entries - note how collapsed directories inherit the pathspec"
);
}
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: Some(Default::default()),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 9,
},
);
assert_eq!(
entries,
[
entry("a.o", Ignored(Expendable), File),
entry("d/a.o", Ignored(Expendable), File),
entry("d/b.o", Ignored(Expendable), File),
entry("d/d/a.precious", Ignored(Precious), File),
entryps("d/d/new", Untracked, File, Always),
],
"If collapses are for deletion, we don't treat precious files like expendable/ignored anymore so they show up individually \
and prevent collapsing into a folder in the first place"
);
}
#[test]
#[cfg_attr(
not(target_vendor = "apple"),
ignore = "Needs filesystem that folds unicode composition"
)]
fn decomposed_unicode_in_directory_is_returned_precomposed() -> crate::Result {
let root = gix_testtools::tempfile::TempDir::new()?;
let decomposed = "a\u{308}";
let precomposed = "รค";
std::fs::create_dir(root.path().join(decomposed))?;
std::fs::write(root.path().join(decomposed).join(decomposed), [])?;
let ((out, _root), entries) = collect(root.path(), None, |keep, ctx| {
walk(
root.path(),
ctx,
walk::Options {
precompose_unicode: true,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry(format!("{precomposed}/{precomposed}").as_str(), Untracked, File)],
"even root paths are returned precomposed then"
);
let troot = root.path().join(decomposed);
let ((out, _root), entries) = collect(root.path(), Some(&troot), |keep, ctx| {
walk(
root.path(),
ctx,
walk::Options {
precompose_unicode: false,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 1,
},
"note how it starts directly in the right repository"
);
assert_eq!(
entries,
[entryps(
format!("{decomposed}/{decomposed}").as_str(),
Untracked,
File,
Prefix
)],
"if disabled, it stays decomposed as provided"
);
Ok(())
}
#[test]
fn worktree_root_can_be_symlink() -> crate::Result {
let root = fixture_in("many-symlinks", "symlink-to-breakout-symlink");
let troot = root.join("file");
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| walk(&root, ctx, options(), keep),
None::<&str>,
Default::default(),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry("file", Untracked, File)],
"it allows symlinks for the worktree itself"
);
Ok(())
}
#[test]
fn root_may_not_go_through_dot_git() -> crate::Result {
let root = fixture("with-nested-dot-git");
for (dir, expected_pathspec) in [("", Some(Verbatim)), ("subdir", None)] {
let troot = root.join("dir").join(".git").join(dir);
let ((out, _root), entries) = collect(&root, Some(&troot), |keep, ctx| {
walk(&root, ctx, options_emit_all(), keep)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[{
let mut e = entry("dir/.git", Pruned, Directory).with_property(DotGit);
e.0.pathspec_match = expected_pathspec;
e
}],
"{dir}: no traversal happened as root passes though .git"
);
}
Ok(())
}
#[test]
fn root_at_submodule_repository_allows_walk() -> crate::Result {
let root = fixture("repo-with-submodule");
let troot = root.join("submodule");
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&troot,
None,
Some(&troot),
|keep, ctx| {
walk(
&troot,
ctx,
walk::Options {
emit_tracked: true,
emit_untracked: Matching,
..options()
},
keep,
)
},
None::<&str>,
Options::git_dir("../.git/modules/submodule"),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
entries,
[entry("dir/file", Tracked, File), entry("untracked", Untracked, File)],
"this is a special case to allow walking submodules specifically, like a normal repository"
);
Ok(())
}
#[test]
fn root_in_submodule_repository_allows_walk() -> crate::Result {
let root = fixture("repo-with-submodule");
let troot = root.join("submodule");
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&troot,
None,
Some(&troot.join("dir")),
|keep, ctx| {
walk(
&troot,
ctx,
walk::Options {
emit_tracked: true,
emit_untracked: Matching,
..options()
},
keep,
)
},
None::<&str>,
Options::git_dir("../.git/modules/submodule"),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry("dir/file", Tracked, File)],
"it's also working if the traversal root is inside the subdmodule"
);
Ok(())
}
#[test]
fn root_in_submodule_from_superproject_repository_allows_walk() -> crate::Result {
let root = fixture("repo-with-submodule");
let troot = root.join("submodule").join("dir");
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| {
walk(
&troot,
ctx,
walk::Options {
emit_tracked: true,
emit_untracked: Matching,
..options()
},
keep,
)
},
None::<&str>,
Default::default(),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry("file", Untracked, File)],
"there is no index that has 'file' in it (it's 'dir/file'), hence it's untracked.\
But the traversal is possible, even though it might not make the most sense."
);
Ok(())
}
#[test]
fn root_enters_directory_with_dot_git_in_reconfigured_worktree_tracked() -> crate::Result {
let root = fixture("nonstandard-worktree");
let troot = root.join("dir-with-dot-git").join("inside");
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_tracked: true,
..options()
},
keep,
)
},
None::<&str>,
Options::git_dir("dir-with-dot-git/.git"),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry("dir-with-dot-git/inside", Tracked, File)],
"everything is tracked, so it won't try to detect git repositories anyway"
);
let troot = root.join("dir-with-dot-git").join("inside");
let ((out, _root), entries) = try_collect_filtered_opts_collect(
&root,
Some(&troot),
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_tracked: false,
..options()
},
keep,
)
},
None::<&str>,
Options::git_dir("dir-with-dot-git/.git"),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: 0,
seen_entries: 1,
}
);
assert!(entries.is_empty());
Ok(())
}
#[test]
fn root_enters_directory_with_dot_git_in_reconfigured_worktree_untracked() -> crate::Result {
let root = fixture("nonstandard-worktree-untracked");
let troot = root.join("dir-with-dot-git").join("inside");
let (_out, entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| walk(&root, ctx, options(), keep),
None::<&str>,
Options::git_dir("dir-with-dot-git/.git"),
)?;
assert_eq!(
entries,
[entry("dir-with-dot-git/inside", Untracked, File)],
"it can enter a dir and treat it as normal even if own .git is inside,\
which otherwise would be a repository"
);
Ok(())
}
#[test]
fn root_may_not_go_through_nested_repository_unless_enabled() -> crate::Result {
let root = fixture("nested-repository");
let troot = root.join("nested").join("file");
let (_out, entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
recurse_repositories: true,
..options()
},
keep,
)
},
None::<&str>,
Default::default(),
)?;
assert_eq!(
entries,
[entry("nested/file", Untracked, File)],
"it happily enters the repository and lists the file"
);
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| walk(&root, ctx, options(), keep),
None::<&str>,
Default::default(),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry("nested", Untracked, Repository)],
"thus it ends in the directory that is a repository"
);
Ok(())
}
#[test]
fn root_may_not_go_through_submodule() -> crate::Result {
let root = fixture("with-submodule");
let troot = root.join("submodule").join("dir").join("file");
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| walk(&root, ctx, options_emit_all(), keep),
None::<&str>,
Default::default(),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
},
);
assert_eq!(
entries,
[entry("submodule", Tracked, Repository)],
"it refuses to start traversal in a submodule, thus it ends in the directory that is the submodule, \
if the root is another repository"
);
Ok(())
}
#[test]
fn walk_with_submodule() -> crate::Result {
let root = fixture("with-submodule");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 4,
}
);
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitmodules", Tracked, File),
entry("dir/file", Tracked, File),
entry("submodule", Tracked, Repository)
],
"thus it ends in the directory that is the submodule"
);
Ok(())
}
#[test]
fn root_that_is_tracked_file_is_returned() -> crate::Result {
let root = fixture("dir-with-tracked-file");
let troot = &root.join("dir").join("file");
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(troot),
|keep, ctx| walk(&root, ctx, options_emit_all(), keep),
None::<&str>,
Default::default(),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry("dir/file", Tracked, File)],
"a tracked file as root just returns that file (even though no iteration is possible)"
);
Ok(())
}
#[test]
fn root_that_is_untracked_file_is_returned() -> crate::Result {
let root = fixture("dir-with-file");
let troot = root.join("dir").join("file");
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| walk(&root, ctx, options(), keep),
None::<&str>,
Default::default(),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry("dir/file", Untracked, File)],
"an untracked file as root just returns that file (even though no iteration is possible)"
);
Ok(())
}
#[test]
fn top_level_root_that_is_a_file() {
let root = fixture("just-a-file");
let err = try_collect(&root, None, |keep, ctx| walk(&root, ctx, options(), keep)).unwrap_err();
assert!(matches!(err, walk::Error::WorktreeRootIsFile { .. }));
}
#[test]
fn root_can_be_pruned_early_with_pathspec() -> crate::Result {
let root = fixture("dir-with-file");
let troot = root.join("dir");
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| walk(&root, ctx, options_emit_all(), keep),
Some("no-match/"),
Default::default(),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry_nomatch("dir", Pruned, Directory)],
"the pathspec didn't match the root, early abort"
);
Ok(())
}
#[test]
fn submodules() -> crate::Result {
let root = fixture("multiple-submodules");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
}
);
let expected_content = [
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitmodules", Tracked, File).with_index_kind(File),
entry("a/b", Tracked, Repository).with_index_kind(Repository),
entry("empty", Tracked, File).with_index_kind(File),
entry("submodule", Tracked, Repository).with_index_kind(Repository),
];
assert_eq!(entries, expected_content, "submodules are detected as repositories");
let ((out1, _root), entries) = try_collect_filtered_opts_collect(
&root,
None,
|keep, ctx| walk(&root, ctx, options_emit_all(), keep),
None::<&str>,
Options {
fresh_index: false,
..Default::default()
},
)?;
assert_eq!(out1, out, "the output matches precisely");
assert_eq!(
entries, expected_content,
"this is also the case if the index isn't considered fresh"
);
let ((out2, _root), entries) = try_collect_filtered_opts_collect(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
ignore_case: true,
..options_emit_all()
},
keep,
)
},
None::<&str>,
Options {
fresh_index: false,
..Default::default()
},
)?;
assert_eq!(out2, out, "the output matches precisely, even with ignore-case");
assert_eq!(
entries, expected_content,
"ignore case doesn't change anything (even though our search is quite different)"
);
Ok(())
}
#[test]
fn cancel_with_collection_does_not_fail() -> crate::Result {
struct CancelDelegate {
emits_left_until_cancel: usize,
}
impl gix_dir::walk::Delegate for CancelDelegate {
fn emit(&mut self, _entry: EntryRef<'_>, _collapsed_directory_status: Option) -> walk::Action {
if self.emits_left_until_cancel == 0 {
walk::Action::Cancel
} else {
self.emits_left_until_cancel -= 1;
walk::Action::Continue
}
}
}
for (idx, fixture_name) in [
"nonstandard-worktree",
"nonstandard-worktree-untracked",
"dir-with-file",
"expendable-and-precious",
"subdir-untracked-and-ignored",
"empty-and-untracked-dir",
"complex-empty",
"type-mismatch-icase-clash-file-is-dir",
]
.into_iter()
.enumerate()
{
let root = fixture(fixture_name);
let mut dlg = CancelDelegate {
emits_left_until_cancel: idx,
};
let _out = try_collect_filtered_opts(
&root,
None,
None,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_untracked: CollapseDirectory,
emit_ignored: Some(CollapseDirectory),
emit_empty_directories: true,
emit_tracked: true,
for_deletion: Some(Default::default()),
emit_pruned: true,
..options()
},
keep,
)
},
None::<&str>,
&mut dlg,
Options::default(),
)?;
// Note that this also doesn't trigger an error - the caller has to deal with that.
}
Ok(())
}
#[test]
fn file_root_is_shown_if_pathspec_matches_exactly() -> crate::Result {
let root = fixture("dir-with-file");
let troot = root.join("dir").join("file");
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| walk(&root, ctx, options(), keep),
Some("*dir/*"),
Default::default(),
)?;
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
},
);
assert_eq!(
entries,
[entryps("dir/file", Untracked, File, WildcardMatch)],
"the pathspec matched the root precisely"
);
Ok(())
}
#[test]
fn root_that_is_tracked_and_ignored_is_considered_tracked() -> crate::Result {
let root = fixture("tracked-is-ignored");
let walk_root = "dir/file";
let troot = root.join(walk_root);
let ((out, actual_root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| walk(&root, ctx, options_emit_all(), keep),
None::<&str>,
Default::default(),
)?;
assert_eq!(actual_root, troot, "it uses the root we provide");
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry(walk_root, Tracked, File)],
"tracking is checked first, so we can safe exclude checks for most entries"
);
Ok(())
}
#[test]
fn root_with_dir_that_is_tracked_and_ignored() -> crate::Result {
let root = fixture("tracked-is-ignored");
for emission in [Matching, CollapseDirectory] {
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(emission),
emit_tracked: true,
emit_untracked: emission,
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
entries,
[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitignore", Tracked, File),
entry("dir/file", Tracked, File)
],
"'tracked' is the overriding property here, so we even enter ignored directories if they have tracked contents,\
otherwise we might permanently miss new untracked files in there. Emission mode has no effect"
);
}
Ok(())
}
#[test]
fn empty_and_nested_untracked() -> crate::Result {
let root = fixture("empty-and-untracked-dir");
for for_deletion in [None, Some(Default::default())] {
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_untracked: Matching,
for_deletion,
emit_empty_directories: true,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 2,
}
);
assert_eq!(
entries,
[
entry("empty", Untracked, Directory).with_property(EmptyDirectory),
entry("untracked/file", Untracked, File)
],
"we find all untracked entries, no matter the deletion mode"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_untracked: CollapseDirectory,
emit_empty_directories: true,
for_deletion,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
entries,
[
entry("empty", Untracked, Directory).with_property(EmptyDirectory),
entry("untracked", Untracked, Directory)
],
"we find all untracked directories, no matter the deletion mode"
);
}
Ok(())
}
#[test]
fn root_that_is_ignored_is_listed_for_files_and_directories() -> crate::Result {
let root = fixture("ignored-dir");
for walk_root in ["dir", "dir/file"] {
let troot = root.join(walk_root);
for emission in [Matching, CollapseDirectory] {
let ((out, actual_root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(emission),
..options()
},
keep,
)
},
None::<&str>,
Default::default(),
)?;
assert_eq!(actual_root, troot);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry("dir", Ignored(Expendable), Directory)],
"excluded directories or files that walkdir are listed without further recursion"
);
}
}
Ok(())
}
#[test]
fn nested_bare_repos_in_ignored_directories() -> crate::Result {
let root = fixture("ignored-dir-with-nested-bare-repository");
let (_out, entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
for_deletion: Some(Default::default()),
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
// NOTE: do not use `_out` as `.git` directory contents can change, it's controlled by Git, causing flakiness.
assert_eq!(
entries,
[
entry(".gitignore", Untracked, File),
entry("bare", Untracked, Directory),
entry("dir", Ignored(Expendable), Directory),
],
"by default, only the directory is listed and recursion is stopped there, as it matches the ignore directives. \
Note the nested bare repository isn't seen, while the bare repository is just collapsed, and not detected as repository"
);
let (_out, entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
for_deletion: Some(ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories),
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
assert_eq!(
entries,
[
entry(".gitignore", Untracked, File),
entry("bare", Untracked, Directory),
entry("dir", Ignored(Expendable), Directory),
],
"When looking for non-bare repositories, we won't find bare ones, they just disappear as ignored collapsed directories"
);
let (_out, entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
for_deletion: Some(ForDeletionMode::FindRepositoriesInIgnoredDirectories),
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
assert_eq!(
entries,
[
entry(".gitignore", Untracked, File),
entry("bare", Untracked, Directory),
entry("dir/file", Ignored(Expendable), File),
entry("dir/subdir/nested-bare", Ignored(Expendable), Repository),
],
"Only in this mode we are able to find them, but it's expensive"
);
Ok(())
}
#[test]
fn nested_repos_in_untracked_directories() -> crate::Result {
let root = fixture("untracked-hidden-bare");
let (_out, entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
// NOTE: do not use `_out` as `.git` directory contents can change, it's controlled by Git, causing flakiness.
assert_eq!(
entries,
[entry("subdir", Untracked, Directory)],
"by default, the subdir is collapsed and we don't see the contained repository as it doesn't get classified"
);
let (_out, entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_untracked: CollapseDirectory,
classify_untracked_bare_repositories: true,
..options()
},
keep,
)
});
assert_eq!(
entries,
[
entry("subdir/file", Untracked, File),
entry("subdir/hidden-bare", Untracked, Repository)
],
"With this flag we are able to find the bare repository"
);
Ok(())
}
#[test]
fn nested_repos_in_ignored_directories() -> crate::Result {
let root = fixture("ignored-dir-with-nested-repository");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(Matching),
for_deletion: Some(Default::default()),
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 4,
}
);
assert_eq!(
entries,
[
entry("dir", Ignored(Expendable), Directory),
entry("objs/a.o", Ignored(Expendable), File),
],
"by default, only the directory is listed and recursion is stopped there, as it matches the ignore directives."
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(Matching),
emit_untracked: CollapseDirectory,
for_deletion: Some(ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 4,
returned_entries: entries.len(),
seen_entries: 6,
}
);
assert_eq!(
entries,
[
entry("dir/file", Ignored(Expendable), File),
entry("dir/subdir/a", Ignored(Expendable), File),
entry("dir/subdir/nested", Ignored(Expendable), Repository),
entry("objs/a.o", Ignored(Expendable), File)
],
"in this mode, we will list repositories nested in ignored directories separately"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: CollapseDirectory,
for_deletion: Some(ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories),
..options()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 4,
returned_entries: entries.len(),
seen_entries: 7,
}
);
assert_eq!(
entries,
[
entry("dir/file", Ignored(Expendable), File),
entry("dir/subdir/a", Ignored(Expendable), File),
entry("dir/subdir/nested", Ignored(Expendable), Repository),
entry("objs", Ignored(Expendable), Directory),
],
"finally, we can't fold if there are any nested repositories. Note how the folding isn't affected in unrelated directories"
);
Ok(())
}
#[test]
#[cfg_attr(
not(target_vendor = "apple"),
ignore = "Needs filesystem that folds unicode composition"
)]
fn decomposed_unicode_in_root_is_returned_precomposed() -> crate::Result {
let root = gix_testtools::tempfile::TempDir::new()?;
let decomposed = "a\u{308}";
let precomposed = "รค";
std::fs::write(root.path().join(decomposed), [])?;
let troot = root.path().join(decomposed);
let ((out, actual_root), entries) = try_collect_filtered_opts_collect_with_root(
root.path(),
None,
Some(&troot),
|keep, ctx| {
walk(
root.path(),
ctx,
walk::Options {
precompose_unicode: true,
..options()
},
keep,
)
},
None::<&str>,
Default::default(),
)?;
assert_eq!(actual_root, troot);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry(precomposed, Untracked, File)],
"even root paths are returned precomposed then"
);
let troot = root.path().join(decomposed);
let ((_out, actual_root), entries) = try_collect_filtered_opts_collect_with_root(
root.path(),
None,
Some(&troot),
|keep, ctx| {
walk(
root.path(),
ctx,
walk::Options {
precompose_unicode: false,
..options()
},
keep,
)
},
None::<&str>,
Default::default(),
)?;
assert_eq!(actual_root, troot);
assert_eq!(
entries,
[entry(decomposed, Untracked, File)],
"if disabled, it stays decomposed as provided"
);
Ok(())
}
#[test]
fn untracked_and_ignored_collapse_mix() {
let root = fixture("untracked-and-ignored-for-collapse");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(CollapseDirectory),
emit_untracked: Matching,
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 4,
returned_entries: entries.len(),
seen_entries: 7,
}
);
assert_eq!(
entries,
[
entry(".gitignore", Untracked, File),
entry("ignored", Ignored(Expendable), Directory),
entry("ignored-inside", Ignored(Expendable), Directory),
entry("mixed/c", Untracked, File),
entry("mixed/c.o", Ignored(Expendable), File),
entry("untracked/a", Untracked, File),
],
"ignored collapses separately from untracked"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(Matching),
emit_untracked: CollapseDirectory,
emit_collapsed: Some(OnStatusMismatch),
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 4,
returned_entries: entries.len(),
seen_entries: 8,
}
);
assert_eq!(
entries,
[
entry(".gitignore", Untracked, File),
entry("ignored", Ignored(Expendable), Directory),
entry("ignored-inside/d.o", Ignored(Expendable), File),
entry("mixed", Untracked, Directory),
entry_dirstat("mixed/c.o", Ignored(Expendable), File, Untracked),
entry("untracked", Untracked, Directory),
],
"untracked collapses separately from ignored, but note that matching directories are still emitted, i.e. ignored/"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(Matching),
emit_untracked: CollapseDirectory,
emit_collapsed: Some(All),
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 4,
returned_entries: entries.len(),
seen_entries: 8,
}
);
assert_eq!(
entries,
[
entry(".gitignore", Untracked, File),
entry("ignored", Ignored(Expendable), Directory),
entry("ignored-inside/d.o", Ignored(Expendable), File),
entry("mixed", Untracked, Directory),
entry_dirstat("mixed/c", Untracked, File, Untracked),
entry_dirstat("mixed/c.o", Ignored(Expendable), File, Untracked),
entry("untracked", Untracked, Directory),
entry_dirstat("untracked/a", Untracked, File, Untracked),
],
"we can also emit all collapsed entries"
);
}
#[test]
fn root_cannot_pass_through_case_altered_capital_dot_git_if_case_insensitive() -> crate::Result {
let root = fixture("with-nested-capitalized-dot-git");
for (dir, expected_pathspec) in [("", Some(Verbatim)), ("subdir", None)] {
let troot = root.join("dir").join(".GIT").join(dir);
let ((out, _root), entries) = collect(&root, Some(&troot), |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
ignore_case: true,
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[{
let mut e = entry("dir/.GIT", Pruned, Directory).with_property(DotGit);
e.0.pathspec_match = expected_pathspec;
e
}],
"{dir}: no traversal happened as root passes though .git, it compares in a case-insensitive fashion"
);
}
let troot = root.join("dir").join(".GIT").join("config");
let ((_out, actual_root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
ignore_case: false,
..options()
},
keep,
)
},
None::<&str>,
Default::default(),
)?;
assert_eq!(actual_root, troot);
assert_eq!(
entries,
[entry("dir/.GIT/config", Untracked, File)],
"it passes right through what now seems like any other directory"
);
Ok(())
}
#[test]
fn partial_checkout_cone_and_non_one() -> crate::Result {
for fixture_name in ["partial-checkout-cone-mode", "partial-checkout-non-cone"] {
let root = fixture(fixture_name);
let not_in_cone_but_created_locally_by_hand = "d/file-created-manually";
let troot = root.join(not_in_cone_but_created_locally_by_hand);
let ((out, actual_root), entries) = try_collect_filtered_opts_collect_with_root(
&root,
None,
Some(&troot),
|keep, ctx| walk(&root, ctx, options_emit_all(), keep),
None::<&str>,
Default::default(),
)?;
assert_eq!(actual_root, troot);
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1,
}
);
assert_eq!(
entries,
[entry("d", Pruned, Directory)
.with_index_kind(Directory)
.with_property(TrackedExcluded)],
"{fixture_name}: we avoid entering excluded sparse-checkout directories even if they are present on disk,\
no matter with cone or without."
);
}
Ok(())
}
#[test]
fn type_mismatch() {
let root = fixture("type-mismatch");
let ((out, _root), entries) = try_collect_filtered_opts_collect(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_tracked: true,
emit_untracked: Matching,
..options()
},
keep,
)
},
None::<&str>,
Options {
fresh_index: false,
..Default::default()
},
)
.expect("success");
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
entries,
[
entry("dir-is-file", Untracked, File).with_index_kind(Directory),
entry("file-is-dir/b", Untracked, File)
],
"as long as the index doesn't claim otherwise (i.e. uptodate) it will handle these changes correctly. \
Also, `dir-is-file` is tracked as directory, but not as file.\
The typechange is visible only when there is an entry in the index, of course"
);
let ((out, _root), entries) = try_collect_filtered_opts_collect(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_tracked: true,
emit_untracked: CollapseDirectory,
..options()
},
keep,
)
},
None::<&str>,
Options {
fresh_index: false,
..Default::default()
},
)
.expect("success");
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3 + 1,
}
);
assert_eq!(
entries,
[
entry("dir-is-file", Untracked, File).with_index_kind(Directory),
entry("file-is-dir", Untracked, Directory).with_index_kind(File)
],
"collapsing works as well, and we allow to see the typechange"
);
}
#[test]
fn type_mismatch_ignore_case() {
let root = fixture("type-mismatch-icase");
let ((out, _root), entries) = try_collect_filtered_opts_collect(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_tracked: true,
emit_untracked: Matching,
ignore_case: true,
..options()
},
keep,
)
},
None::<&str>,
Options {
fresh_index: false,
..Default::default()
},
)
.expect("success");
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
entries,
[
entry("Dir-is-File", Untracked, File).with_index_kind(Directory),
entry("File-is-Dir/b", Untracked, File)
],
"this is the same as in the non-icase version, which means that icase lookup works"
);
let ((out, _root), entries) = try_collect_filtered_opts_collect(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_tracked: true,
emit_untracked: CollapseDirectory,
ignore_case: true,
..options()
},
keep,
)
},
None::<&str>,
Options {
fresh_index: false,
..Default::default()
},
)
.expect("success");
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3 + 1,
}
);
assert_eq!(
entries,
[
entry("Dir-is-File", Untracked, File).with_index_kind(Directory),
entry("File-is-Dir", Untracked, Directory).with_index_kind(File)
],
"this is the same as in the non-icase version, which means that icase lookup works"
);
}
#[test]
fn type_mismatch_ignore_case_clash_dir_is_file() {
let root = fixture("type-mismatch-icase-clash-dir-is-file");
let ((out, _root), entries) = try_collect_filtered_opts_collect(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_tracked: true,
emit_untracked: Matching,
ignore_case: true,
..options()
},
keep,
)
},
None::<&str>,
Options {
fresh_index: false,
..Default::default()
},
)
.expect("success");
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 2,
}
);
assert_eq!(
entries,
[entry("d", Tracked, File)],
"file `d` exists on disk and it is found as well. This is just because we prefer finding files over dirs, coincidence"
);
}
#[test]
fn type_mismatch_ignore_case_clash_file_is_dir() {
let root = fixture("type-mismatch-icase-clash-file-is-dir");
let ((out, _root), entries) = try_collect_filtered_opts_collect(
&root,
None,
|keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_tracked: true,
emit_untracked: CollapseDirectory,
ignore_case: true,
..options()
},
keep,
)
},
None::<&str>,
Options {
fresh_index: false,
..Default::default()
},
)
.expect("success");
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 2,
}
);
assert_eq!(
entries,
[entry("D/a", Tracked, File)],
"`D` exists on disk as directory, and we manage to to find it in in the index, hence no collapsing happens.\
If there was no special handling for this, it would have found the file (`d` in the index, icase), which would have been wrong."
);
}
#[test]
fn top_level_slash_with_negations() -> crate::Result {
for repo_name in ["slash-in-root-and-negated", "star-in-root-and-negated"] {
let root = fixture(repo_name);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".github/workflow.yml", Tracked, File),
entry(".gitignore", Tracked, File),
entry("file", Untracked, File),
entry("readme.md", Tracked, File),
],
"the top-level is never considered ignored"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(ForDeletionMode::FindRepositoriesInIgnoredDirectories),
emit_tracked: false,
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry("file", Untracked, File)
],
"And the negated file is correctly detected as untracked"
);
}
Ok(())
}
#[test]
fn subdir_slash_with_negations() -> crate::Result {
for repo_name in ["slash-in-subdir-and-negated", "star-in-subdir-and-negated"] {
let root = fixture(repo_name);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry("sub/.github/workflow.yml", Tracked, File),
entry("sub/.gitignore", Tracked, File),
entry("sub/file", Untracked, File),
entry("sub/readme.md", Tracked, File),
],
"subdirectory matches work as expected, also with a `/` which has no bearing."
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(ForDeletionMode::FindRepositoriesInIgnoredDirectories),
emit_tracked: false,
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 3,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry("sub/file", Untracked, File)
],
"This is expected, and the `.git` top-level is pruned."
);
}
Ok(())
}
#[test]
fn one_ignored_submodule() -> crate::Result {
let root = fixture("one-ignored-submodule");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitignore", Untracked, File),
entry(".gitmodules", Tracked, File),
entry("empty", Tracked, File),
entry("submodule", Tracked, Repository),
],
"when traversing the worktree root, this is correct, the submodule doesn't count as ignored"
);
let troot = root.join("submodule");
let ((out, _root), entries) = collect(&root, Some(&troot), |keep, ctx| {
walk(&root, ctx, options_emit_all(), keep)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 0,
returned_entries: entries.len(),
seen_entries: 1
}
);
assert_eq!(
entries,
&[entryps("submodule", Tracked, Repository, Verbatim)],
"The submodule is simply tracked, it doesn't count as ignored"
);
Ok(())
}
#[test]
fn ignored_sub_repo() -> crate::Result {
let root = fixture("with-sub-repo");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitignore", Tracked, File),
entry("sub-repo", Ignored(Expendable), Directory),
],
"without intent to delete, this looks like just like an untracked directory"
);
for ignored_emission_mode in [Matching, CollapseDirectory] {
for untracked_emission_mode in [Matching, CollapseDirectory] {
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: Some(ForDeletionMode::IgnoredDirectoriesCanHideNestedRepositories),
emit_tracked: false,
emit_ignored: Some(ignored_emission_mode),
emit_untracked: untracked_emission_mode,
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 1,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry("sub-repo", Ignored(Expendable), Repository),
],
"Even when ignored directories can hide repositories, we are able to detect top-level repositories"
);
}
}
Ok(())
}
#[test]
fn in_repo_worktree() -> crate::Result {
let root = fixture("in-repo-worktree");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 4,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry("dir/file", Tracked, File),
entry("dir/worktree", Untracked, Repository),
entry("worktree", Untracked, Repository),
],
"without passing worktree information, they count as untracked repositories, making them vulnerable"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
worktree_relative_worktree_dirs: Some(&BTreeSet::from(["worktree".into(), "dir/worktree".into()])),
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 4,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry("dir/file", Tracked, File),
entry("dir/worktree", Tracked, Repository).no_index_kind(),
entry("worktree", Tracked, Repository).no_index_kind(),
],
"But when worktree information is passed, it is identified as tracked to look similarly to a submodule.\
What gives it away is that the index-kind is None, which is unusual for a tracked file."
);
Ok(())
}
#[test]
fn in_repo_hidden_worktree() -> crate::Result {
let root = fixture("in-repo-hidden-worktree");
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 4,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitignore", Untracked, File),
entry("dir/file", Tracked, File),
entry("hidden", Ignored(Expendable), Directory),
],
"if worktree information isn't provided, they would not be discovered in hidden directories"
);
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
for_deletion: None,
worktree_relative_worktree_dirs: Some(&BTreeSet::from(["hidden/subdir/worktree".into()])),
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 4,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitignore", Untracked, File),
entry("dir/file", Tracked, File),
entry("hidden", Ignored(Expendable), Directory),
],
"Without the intend to delete, the worktree remains hidden, which is what we want to see in a `status` for example"
);
for ignored_emission_mode in [Matching, CollapseDirectory] {
for deletion_mode in [
ForDeletionMode::IgnoredDirectoriesCanHideNestedRepositories,
ForDeletionMode::FindRepositoriesInIgnoredDirectories,
ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories,
] {
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
walk(
&root,
ctx,
walk::Options {
emit_ignored: Some(ignored_emission_mode),
for_deletion: Some(deletion_mode),
worktree_relative_worktree_dirs: Some(&BTreeSet::from(["hidden/subdir/worktree".into()])),
..options_emit_all()
},
keep,
)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 4,
returned_entries: entries.len(),
seen_entries: 5,
}
);
assert_eq!(
entries,
&[
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
entry(".gitignore", Untracked, File),
entry("dir/file", Tracked, File),
entry("hidden/file", Ignored(Expendable), File),
entry("hidden/subdir/worktree", Tracked, Repository).no_index_kind(),
],
"Worktrees within hidden directories are also detected and protected by counting them as tracked (like submodules)"
);
}
}
Ok(())
}
gix-dir-0.11.0/tests/dir_cwd.rs 0000644 0000000 0000000 00000002004 10461020230 0014365 0 ustar 0000000 0000000 use crate::walk_utils::{collect, entryps, fixture, options};
use gix_dir::entry::Kind::File;
use gix_dir::entry::PathspecMatch::Prefix;
use gix_dir::entry::Status::Untracked;
use gix_dir::walk;
use pretty_assertions::assert_eq;
use std::path::Path;
pub mod walk_utils;
#[test]
fn prefixes_work_as_expected() -> gix_testtools::Result {
let root = fixture("only-untracked");
std::env::set_current_dir(root.join("d"))?;
let troot = Path::new("..").join("d");
let ((out, _root), entries) = collect(Path::new(".."), Some(&troot), |keep, ctx| {
walk(Path::new(".."), ctx, options(), keep)
});
assert_eq!(
out,
walk::Outcome {
read_dir_calls: 2,
returned_entries: entries.len(),
seen_entries: 3,
}
);
assert_eq!(
&entries,
&[
entryps("d/a", Untracked, File, Prefix),
entryps("d/b", Untracked, File, Prefix),
entryps("d/d/a", Untracked, File, Prefix),
]
);
Ok(())
}
gix-dir-0.11.0/tests/fixtures/fifo.sh 0000755 0000000 0000000 00000000436 10461020230 0015546 0 ustar 0000000 0000000 #!/usr/bin/env bash
set -eu -o pipefail
mkfifo top-level
git init single-top-level-fifo
(cd single-top-level-fifo
mkfifo top
)
git init two-fifos-two-files
(cd two-fifos-two-files
mkdir dir dir-with-file
touch file dir-with-file/nested-file
mkfifo top
mkfifo dir/nested
)
gix-dir-0.11.0/tests/fixtures/generated-archives/.gitignore 0000644 0000000 0000000 00000000043 10461020230 0022006 0 ustar 0000000 0000000 many.tar
many-symlinks.tar
fifo.tar gix-dir-0.11.0/tests/fixtures/many-symlinks.sh 0000755 0000000 0000000 00000001431 10461020230 0017432 0 ustar 0000000 0000000 #!/usr/bin/env bash
set -eu -o pipefail
# These fixtures use symlinks. See `many.sh` for some that don't.
git init breakout-symlink
(cd breakout-symlink
mkdir hide
ln -s ../.. hide/breakout
touch file
)
ln -s breakout-symlink symlink-to-breakout-symlink
git init immediate-breakout-symlink
(cd immediate-breakout-symlink
ln -s .. breakout
)
git init excluded-symlinks-to-dir
(cd excluded-symlinks-to-dir
cat <<'EOF' >.gitignore
src1
src2/
file1
file2/
ignored
ignored-must-be-dir/
EOF
git add .gitignore && git commit -m "init"
mkdir src
>src/file
mkdir ignored-must-be-dir ignored
touch ignored-must-be-dir/file ignored/file
ln -s src src1
ln -s src src2
ln -s src/file file1
ln -s src/file file2
)
ln -s excluded-symlinks-to-dir worktree-root-is-symlink
gix-dir-0.11.0/tests/fixtures/many.sh 0000755 0000000 0000000 00000022656 10461020230 0015577 0 ustar 0000000 0000000 #!/usr/bin/env bash
set -eu -o pipefail
# These fixtures don't use symlinks. See `many-symlinks.sh` for some that do.
git init with-nested-dot-git
(cd with-nested-dot-git
mkdir -p dir/.git/subdir
touch dir/.git/config dir/.git/subdir/bar
)
git init with-nested-capitalized-dot-git
(cd with-nested-capitalized-dot-git
mkdir -p dir/.GIT/subdir
touch dir/.GIT/config dir/.GIT/subdir/bar
)
git init dir-with-file
(cd dir-with-file
mkdir dir
touch dir/file
)
git init dir-with-tracked-file
(cd dir-with-tracked-file
mkdir dir
touch dir/file
git add .
git commit -m "init"
)
git init repo-with-submodule
(cd repo-with-submodule
git submodule add ../dir-with-tracked-file submodule
git commit -m "add submodule"
touch submodule/untracked
)
git init ignored-dir
(cd ignored-dir
mkdir dir
touch dir/file
echo "dir/" > .gitignore
)
cp -R ignored-dir ignored-dir-with-nested-repository
(cd ignored-dir-with-nested-repository
echo "*.o" >> .gitignore
git add .
mkdir dir/subdir objs
(cd dir/subdir
touch a
git init nested
)
>objs/a.o
)
cp -R ignored-dir ignored-dir-with-nested-bare-repository
(cd ignored-dir-with-nested-bare-repository
mkdir dir/subdir
(cd dir/subdir
git init --bare nested-bare
)
git init --bare bare
)
cp -R ignored-dir-with-nested-bare-repository ignored-dir-nested-minimal
(cd ignored-dir-nested-minimal
(cd bare
rm -Rf hooks config description
)
(cd dir/subdir/nested-bare
rm -Rf refs hooks config description
)
)
mkdir untracked-hidden-bare
(cd untracked-hidden-bare
mkdir subdir
git init --bare subdir/hidden-bare
>subdir/file
)
git init tracked-is-ignored
(cd tracked-is-ignored
mkdir dir
touch dir/file
echo "dir/" > .gitignore
git add --force . && git commit -m "init"
)
git init nested-repository
(cd nested-repository
touch file
git add . && git commit -m "init"
git init nested
(cd nested
touch file
git add file && git commit -m "init"
)
)
git clone dir-with-tracked-file with-submodule
(cd with-submodule
git submodule add ../dir-with-tracked-file submodule
git commit -m "submodule added"
)
git init nonstandard-worktree
(cd nonstandard-worktree
mkdir dir-with-dot-git
touch dir-with-dot-git/inside
touch seemingly-outside
git add dir-with-dot-git/inside seemingly-outside
mv .git dir-with-dot-git
git -C dir-with-dot-git config core.worktree "$PWD"
git -C dir-with-dot-git commit -m "init"
)
git init nonstandard-worktree-untracked
(cd nonstandard-worktree-untracked
mkdir dir-with-dot-git
touch dir-with-dot-git/inside
touch seemingly-outside
git add dir-with-dot-git/inside seemingly-outside
mv .git dir-with-dot-git
git -C dir-with-dot-git config core.worktree "$PWD"
git -C dir-with-dot-git commit -m "init"
rm dir-with-dot-git/.git/index
)
git init partial-checkout-cone-mode
(cd partial-checkout-cone-mode
touch a b
mkdir c1
(cd c1 && touch a b && mkdir c2 && cd c2 && touch a b)
(cd c1 && mkdir c3 && cd c3 && touch a b)
mkdir d
(cd d && touch a b && mkdir c4 && cd c4 && touch a b c5)
git add .
git commit -m "init"
git sparse-checkout set c1/c2 --sparse-index
mkdir d && touch d/file-created-manually
)
git init partial-checkout-non-cone
(cd partial-checkout-non-cone
touch a b
mkdir c1
(cd c1 && touch a b && mkdir c2 && cd c2 && touch a b)
(cd c1 && mkdir c3 && cd c3 && touch a b)
mkdir d
(cd d && touch a b && mkdir c4 && cd c4 && touch a b c5)
git add .
git commit -m "init"
git sparse-checkout set c1/c2 --no-cone
mkdir d && touch d/file-created-manually
)
git init precious-nested-repository
(cd precious-nested-repository
echo '$precious*/' > .gitignore
git init precious-repo
git add .gitignore && git commit -m "init"
)
git init only-untracked
(cd only-untracked
>a
>b
mkdir d
>d/a
>d/b
mkdir d/d
>d/d/a
>c
)
git init ignored-with-empty
(cd ignored-with-empty
echo "/target/" >> .gitignore
git add .gitignore && git commit -m "init"
mkdir -p target/empty target/debug target/release
touch target/debug/a target/release/b
)
cp -R only-untracked subdir-untracked
(cd subdir-untracked
git add .
git rm --cached d/d/a
git commit -m "init"
)
cp -R subdir-untracked subdir-untracked-and-ignored
(cd subdir-untracked-and-ignored
>a.o
>b.o
>d/a.o
>d/b.o
>d/d/a.o
>d/d/b.o
>c.o
mkdir generated d/generated d/d/generated
touch generated/a generated/a.o d/generated/b d/d/generated/b
mkdir -p objs/sub
touch objs/a.o objs/b.o objs/sub/other.o
echo "*.o" > .gitignore
echo "generated/" >> .gitignore
)
mkdir untracked-and-ignored-for-collapse
(cd untracked-and-ignored-for-collapse
echo "ignored/" >> .gitignore
echo "*.o" >> .gitignore
mkdir -p untracked ignored/empty mixed ignored-inside
touch untracked/a ignored/b mixed/c mixed/c.o ignored-inside/d.o
)
git init untracked-and-precious
(cd untracked-and-precious
echo '*.o' >> .gitignore
echo '$*.precious' >> .gitignore
mkdir -p d/d
touch d/a d/b && git add .
touch a.o d/a.o d/b.o
touch d/d/new d/d/a.precious
git commit -m "init"
)
git init expendable-and-precious
(cd expendable-and-precious
echo "*.o" >> .gitignore
echo '$precious' >> .gitignore
echo '$/mixed/precious' >> .gitignore
echo '$/all-precious/' >> .gitignore
echo "/all-expendable/" >> .gitignore
echo '$*.precious' >> .gitignore
git add .gitignore
touch a.o
touch precious
mkdir mixed
touch mixed/precious mixed/b.o
(mkdir some-expendable && cd some-expendable
touch file.o file new && git add file
)
(mkdir some-precious && cd some-precious
touch file.precious file new && git add file
)
mkdir all-precious all-expendable all-precious-by-filematch all-expendable-by-filematch
touch all-precious/a all-precious/b all-expendable/c all-expendable/d
(cd all-precious-by-filematch
touch a.precious b.precious
)
(cd all-expendable-by-filematch
touch e.o f.o
)
git commit -m "init"
)
git init expendable-and-precious-nested-in-ignored-dir
(cd expendable-and-precious-nested-in-ignored-dir
echo 'ignored/' > .gitignore
git add .gitignore && git commit -m "init"
mkdir -p ignored/other
cp -R ../expendable-and-precious ignored/d
rm -Rf ignored/d/*-by-filematch ignored/d/some-*
mkdir -p other/ignored && >other/ignored/a
)
mkdir empty-and-untracked-dir
(cd empty-and-untracked-dir
mkdir empty untracked
>untracked/file
)
mkdir complex-empty
(cd complex-empty
mkdir empty-toplevel
mkdir -p only-dirs/sub/subsub only-dirs/other
mkdir -p dirs-and-files/sub dirs-and-files/dir
touch dirs-and-files/dir/file
)
git init type-mismatch
(cd type-mismatch
mkdir dir-is-file && >dir-is-file/a
>file-is-dir
git add .
rm -Rf dir-is-file
>dir-is-file
rm file-is-dir && mkdir file-is-dir && >file-is-dir/b
)
git init type-mismatch-icase
(cd type-mismatch-icase
mkdir dir-is-file && >dir-is-file/a
>file-is-dir
git add .
rm -Rf dir-is-file
>Dir-is-File
rm file-is-dir && mkdir File-is-Dir && >File-is-Dir/b
)
git init type-mismatch-icase-clash-dir-is-file
(cd type-mismatch-icase-clash-dir-is-file
empty_oid=$(git hash-object -w --stdin d
)
cp -R type-mismatch-icase-clash-dir-is-file type-mismatch-icase-clash-file-is-dir
(cd type-mismatch-icase-clash-file-is-dir
rm d
mkdir D && >D/a
)
mkdir empty
touch just-a-file
git init submodule
(cd submodule
touch empty && git add empty
git commit -m upstream
)
git clone submodule multiple-submodules
(cd multiple-submodules
git submodule add ../submodule submodule
git submodule add ../submodule a/b
git commit -m "add modules"
)
git clone submodule one-ignored-submodule
(cd one-ignored-submodule
git submodule add ../submodule submodule
echo '/submodule/' > .gitignore
echo '*' > submodule/.gitignore
git commit -m "add seemingly ignored submodule"
)
git init slash-in-root-and-negated
(cd slash-in-root-and-negated
cat <<'EOF' >.gitignore
/
!file
!*.md
!.github
!.github/**
EOF
touch file readme.md
mkdir .github
touch .github/workflow.yml
git add .github readme.md .gitignore
git commit -m "init"
)
git init star-in-root-and-negated
(cd star-in-root-and-negated
cat <<'EOF' >.gitignore
*
!file
!.gitignore
!*.md
!.github
!.github/**
EOF
touch file readme.md
mkdir .github
touch .github/workflow.yml
git add .github readme.md .gitignore
git commit -m "init"
)
git init slash-in-subdir-and-negated
(cd slash-in-subdir-and-negated
mkdir sub
(cd sub
cat <<'EOF' >.gitignore
/
!file
!*.md
!.github
!.github/**
EOF
touch file readme.md
mkdir .github
touch .github/workflow.yml
git add .github readme.md .gitignore
git commit -m "init"
)
)
git init star-in-subdir-and-negated
(cd star-in-subdir-and-negated
mkdir sub
(cd sub
cat <<'EOF' >.gitignore
*
!file
!.gitignore
!*.md
!.github
!.github/**
EOF
touch file readme.md
mkdir .github
touch .github/workflow.yml
git add .github readme.md .gitignore
git commit -m "init"
)
)
git init with-sub-repo
(cd with-sub-repo
echo '*' > .gitignore
git add -f .gitignore
git clone ../dir-with-file sub-repo
)
git clone dir-with-tracked-file in-repo-worktree
(cd in-repo-worktree
git worktree add worktree
git worktree add -b other-worktree dir/worktree
)
git clone dir-with-tracked-file in-repo-hidden-worktree
(cd in-repo-hidden-worktree
echo '/hidden/' > .gitignore
mkdir -p hidden/subdir
touch hidden/file
git worktree add -b worktree-branch hidden/subdir/worktree
)
gix-dir-0.11.0/tests/walk_utils/mod.rs 0000644 0000000 0000000 00000030165 10461020230 0015720 0 ustar 0000000 0000000 use bstr::BStr;
use gix_dir::{entry, walk, Entry};
use gix_testtools::scripted_fixture_read_only;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
pub fn fixture_in(filename: &str, name: &str) -> PathBuf {
let root = scripted_fixture_read_only(format!("{filename}.sh")).expect("script works");
root.join(name)
}
pub fn fixture(name: &str) -> PathBuf {
fixture_in("many", name)
}
/// Default options
pub fn options() -> walk::Options<'static> {
walk::Options::default()
}
/// Default options
pub fn options_emit_all() -> walk::Options<'static> {
walk::Options {
precompose_unicode: false,
ignore_case: false,
recurse_repositories: false,
for_deletion: None,
classify_untracked_bare_repositories: false,
emit_pruned: true,
emit_ignored: Some(walk::EmissionMode::Matching),
emit_tracked: true,
emit_untracked: walk::EmissionMode::Matching,
emit_empty_directories: true,
emit_collapsed: None,
symlinks_to_directories_are_ignored_like_directories: false,
worktree_relative_worktree_dirs: None,
}
}
pub fn entry(
rela_path: impl AsRef,
status: entry::Status,
disk_kind: entry::Kind,
) -> (Entry, Option) {
entryps(rela_path, status, disk_kind, entry::PathspecMatch::Always)
}
pub fn entry_nomatch(
rela_path: impl AsRef,
status: entry::Status,
disk_kind: entry::Kind,
) -> (Entry, Option) {
(
Entry {
rela_path: rela_path.as_ref().to_owned(),
status,
property: None,
disk_kind: Some(disk_kind),
index_kind: index_kind_from_status(status, disk_kind),
pathspec_match: None,
},
None,
)
}
pub fn entry_nokind(rela_path: impl AsRef, status: entry::Status) -> (Entry, Option) {
(
Entry {
rela_path: rela_path.as_ref().to_owned(),
status,
property: None,
disk_kind: None,
index_kind: None,
pathspec_match: None,
},
None,
)
}
pub fn entryps(
rela_path: impl AsRef,
status: entry::Status,
disk_kind: entry::Kind,
pathspec_match: entry::PathspecMatch,
) -> (Entry, Option) {
(
Entry {
rela_path: rela_path.as_ref().to_owned(),
status,
property: None,
disk_kind: Some(disk_kind),
index_kind: index_kind_from_status(status, disk_kind),
pathspec_match: Some(pathspec_match),
},
None,
)
}
pub fn entry_dirstat(
rela_path: impl AsRef,
status: entry::Status,
disk_kind: entry::Kind,
dir_status: entry::Status,
) -> (Entry, Option) {
(
Entry {
rela_path: rela_path.as_ref().to_owned(),
status,
property: None,
disk_kind: Some(disk_kind),
index_kind: index_kind_from_status(status, disk_kind),
pathspec_match: Some(entry::PathspecMatch::Always),
},
Some(dir_status),
)
}
/// These are entries that have been collapsed into a single directory.
pub fn entryps_dirstat(
rela_path: impl AsRef,
status: entry::Status,
disk_kind: entry::Kind,
pathspec_match: entry::PathspecMatch,
dir_status: entry::Status,
) -> (Entry, Option) {
(
Entry {
rela_path: rela_path.as_ref().to_owned(),
status,
property: None,
disk_kind: Some(disk_kind),
index_kind: index_kind_from_status(status, disk_kind),
pathspec_match: Some(pathspec_match),
},
Some(dir_status),
)
}
fn index_kind_from_status(status: entry::Status, disk_kind: entry::Kind) -> Option {
matches!(status, entry::Status::Tracked).then_some(disk_kind)
}
pub trait EntryExt {
fn with_index_kind(self, index_kind: entry::Kind) -> Self;
fn with_property(self, flags: entry::Property) -> Self;
fn with_match(self, m: entry::PathspecMatch) -> Self;
fn no_match(self) -> Self;
fn no_kind(self) -> Self;
fn no_index_kind(self) -> Self;
}
impl EntryExt for (Entry, Option) {
fn with_index_kind(mut self, index_kind: entry::Kind) -> Self {
self.0.index_kind = index_kind.into();
self
}
fn with_property(mut self, property: entry::Property) -> Self {
self.0.property = property.into();
self
}
fn with_match(mut self, m: entry::PathspecMatch) -> Self {
self.0.pathspec_match = Some(m);
self
}
fn no_match(mut self) -> Self {
self.0.pathspec_match = None;
self
}
fn no_kind(mut self) -> Self {
self.0.disk_kind = None;
self
}
fn no_index_kind(mut self) -> Self {
self.0.index_kind = None;
self
}
}
pub fn collect(
worktree_root: &Path,
root: Option<&Path>,
cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
) -> ((walk::Outcome, PathBuf), Entries) {
try_collect(worktree_root, root, cb).unwrap()
}
pub fn collect_filtered(
worktree_root: &Path,
root: Option<&Path>,
cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
patterns: impl IntoIterator- >,
) -> ((walk::Outcome, PathBuf), Entries) {
try_collect_filtered(worktree_root, root, cb, patterns).unwrap()
}
pub fn try_collect(
worktree_root: &Path,
root: Option<&Path>,
cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
) -> Result<((walk::Outcome, PathBuf), Entries), walk::Error> {
try_collect_filtered(worktree_root, root, cb, None::<&str>)
}
pub fn try_collect_filtered(
worktree_root: &Path,
root: Option<&Path>,
cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
patterns: impl IntoIterator
- >,
) -> Result<((walk::Outcome, PathBuf), Entries), walk::Error> {
try_collect_filtered_opts_collect(worktree_root, root, cb, patterns, Default::default())
}
pub fn try_collect_filtered_opts_collect(
worktree_root: &Path,
root: Option<&Path>,
cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
patterns: impl IntoIterator
- >,
options: Options<'_>,
) -> Result<((walk::Outcome, PathBuf), Entries), walk::Error> {
let mut dlg = gix_dir::walk::delegate::Collect::default();
let outcome = try_collect_filtered_opts(worktree_root, root, None, None, cb, patterns, &mut dlg, options)?;
Ok((outcome, dlg.into_entries_by_path()))
}
pub fn try_collect_filtered_opts_collect_with_root(
worktree_root: &Path,
root: Option<&Path>,
explicit_traversal_root: Option<&Path>,
cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
patterns: impl IntoIterator
- >,
options: Options<'_>,
) -> Result<((walk::Outcome, PathBuf), Entries), walk::Error> {
let mut dlg = gix_dir::walk::delegate::Collect::default();
let outcome = try_collect_filtered_opts(
worktree_root,
root,
explicit_traversal_root,
None,
cb,
patterns,
&mut dlg,
options,
)?;
Ok((outcome, dlg.into_entries_by_path()))
}
pub fn collect_filtered_with_cwd(
worktree_root: &Path,
root: Option<&Path>,
cwd_suffix: Option<&str>,
cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
patterns: impl IntoIterator
- >,
) -> ((walk::Outcome, PathBuf), Entries) {
let mut dlg = gix_dir::walk::delegate::Collect::default();
let outcome = try_collect_filtered_opts(
worktree_root,
root,
None,
cwd_suffix,
cb,
patterns,
&mut dlg,
Default::default(),
)
.expect("success");
(outcome, dlg.into_entries_by_path())
}
#[allow(clippy::too_many_arguments)]
pub fn try_collect_filtered_opts(
worktree_root: &Path,
root: Option<&Path>,
explicit_traversal_root: Option<&Path>,
append_to_cwd: Option<&str>,
cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
patterns: impl IntoIterator
- >,
delegate: &mut dyn gix_dir::walk::Delegate,
Options {
fresh_index,
git_dir,
should_interrupt,
}: Options<'_>,
) -> Result<(walk::Outcome, PathBuf), walk::Error> {
let git_dir = worktree_root.join(git_dir.unwrap_or(".git"));
let mut index = std::fs::read(git_dir.join("index")).ok().map_or_else(
|| gix_index::State::new(gix_index::hash::Kind::Sha1),
|bytes| {
gix_index::State::from_bytes(
&bytes,
std::time::UNIX_EPOCH.into(),
gix_index::hash::Kind::Sha1,
Default::default(),
)
.map(|t| t.0)
.expect("valid index")
},
);
if fresh_index {
index
.entries_mut()
.iter_mut()
.filter(|e| {
// relevant for partial checkouts, all related entries will have skip-worktree set,
// which also means they will never be up-to-date.
!e.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE)
})
.for_each(|e| {
// pretend that the index was refreshed beforehand so we know what's uptodate.
e.flags |= gix_index::entry::Flags::UPTODATE;
});
}
let mut search = gix_pathspec::Search::from_specs(
patterns.into_iter().map(|spec| {
gix_pathspec::parse(spec.as_ref(), gix_pathspec::Defaults::default()).expect("tests use valid pattern")
}),
root.map(|root| root.strip_prefix(worktree_root).expect("root is within worktree root"))
.or_else(|| append_to_cwd.map(Path::new)),
"we don't provide absolute pathspecs, thus need no worktree root".as_ref(),
)
.expect("search creation can't fail");
let mut stack = gix_worktree::Stack::from_state_and_ignore_case(
worktree_root,
false, /* ignore case */
gix_worktree::stack::State::IgnoreStack(gix_worktree::stack::state::Ignore::new(
Default::default(),
Default::default(),
None,
gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
)),
&index,
index.path_backing(),
);
let mut cwd = worktree_root.to_owned();
if let Some(suffix) = append_to_cwd {
assert!(
worktree_root.is_absolute(),
"BUG: need absolute worktree root for CWD checks to work"
);
cwd.push(suffix);
}
let git_dir_realpath = gix_path::realpath_opts(&git_dir, &cwd, gix_path::realpath::MAX_SYMLINKS).unwrap();
let lookup = index.prepare_icase_backing();
cb(
delegate,
walk::Context {
git_dir_realpath: &git_dir_realpath,
current_dir: &cwd,
index: &index,
ignore_case_index_lookup: Some(&lookup),
pathspec: &mut search,
pathspec_attributes: &mut |_, _, _, _| panic!("we do not use pathspecs that require attributes access."),
excludes: Some(&mut stack),
objects: &gix_object::find::Never,
explicit_traversal_root,
should_interrupt,
},
)
}
pub struct Options<'a> {
pub fresh_index: bool,
pub git_dir: Option<&'a str>,
pub should_interrupt: Option<&'a AtomicBool>,
}
impl<'a> Options<'a> {
pub fn git_dir(dir: &'a str) -> Self {
Options {
git_dir: Some(dir),
..Default::default()
}
}
}
impl Default for Options<'_> {
fn default() -> Self {
Options {
fresh_index: true,
git_dir: None,
should_interrupt: None,
}
}
}
type Entries = Vec<(Entry, Option)>;