pax_global_header00006660000000000000000000000064142042200210014475gustar00rootroot0000000000000052 comment=4fda5369eee4fd253e81ae04cf03fe0cc48e15c4 precious-0.1.3/000077500000000000000000000000001420422002100133275ustar00rootroot00000000000000precious-0.1.3/.github/000077500000000000000000000000001420422002100146675ustar00rootroot00000000000000precious-0.1.3/.github/workflows/000077500000000000000000000000001420422002100167245ustar00rootroot00000000000000precious-0.1.3/.github/workflows/audit-nightly.yml000066400000000000000000000004021420422002100222250ustar00rootroot00000000000000name: Security audit on: schedule: - cron: "0 0 * * *" jobs: security_audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} precious-0.1.3/.github/workflows/audit-on-push.yml000066400000000000000000000004411420422002100221430ustar00rootroot00000000000000name: Security audit on: push: paths: - "**/Cargo.toml" - "**/Cargo.lock" jobs: security_audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} precious-0.1.3/.github/workflows/ci.yml000066400000000000000000000106721420422002100200500ustar00rootroot00000000000000name: Continuous integration on: [push, pull_request] env: CRATE_NAME: precious RUST_BACKTRACE: 1 jobs: test: name: Test - ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} runs-on: ${{ matrix.platform.os }} strategy: fail-fast: false matrix: platform: - os_name: Linux os: ubuntu-latest target: x86_64-unknown-linux-gnu - os_name: macOS os: macOS-latest target: x86_64-apple-darwin - os_name: Windows os: windows-latest target: x86_64-pc-windows-msvc toolchain: - stable - beta - nightly steps: - uses: actions/checkout@v2 - name: Cache cargo & target directories uses: Swatinem/rust-cache@v1 - name: Install toolchain uses: actions-rs/toolchain@v1 with: profile: default toolchain: ${{ matrix.toolchain }} override: true - name: Configure Git run: | git config --global user.email "jdoe@example.com" git config --global user.name "J. Doe" - name: Run cargo check uses: actions-rs/cargo@v1 with: command: check args: --target=${{ matrix.platform.target }} - name: Run cargo test uses: actions-rs/cargo@v1 with: command: test args: --target=${{ matrix.platform.target }} # There's not much value in running self-check on multiple # platforms. Either we're lint-clean or not. We do run on each rust # target to catch warnings coming from clippy in future rust versions. - name: Run cargo install uses: actions-rs/cargo@v1 with: command: install args: --target=${{ matrix.platform.target }} --path . if: matrix.platform.os_name == 'Linux' - uses: actions/setup-node@v2 if: matrix.platform.os_name == 'Linux' && matrix.toolchain == 'stable' - name: Run install-dev-tools.sh run: | set -e mkdir $HOME/bin ./dev/bin/install-dev-tools.sh if: matrix.platform.os_name == 'Linux' - name: Self-check run: PATH=$HOME/bin:$PATH $HOME/.cargo/bin/precious --debug lint --all if: matrix.platform.os_name == 'Linux' && matrix.toolchain == 'stable' # Copied from https://github.com/urbica/martin/blob/master/.github/workflows/ci.yml release: name: Release - ${{ matrix.platform.os_name }} if: startsWith( github.ref, 'refs/tags/v' ) needs: [test] strategy: matrix: platform: - os_name: Linux os: ubuntu-latest target: x86_64-unknown-linux-gnu bin: precious name: precious-Linux-x86_64.tar.gz - os_name: Windows os: windows-latest target: x86_64-pc-windows-msvc bin: precious.exe name: precious-Windows-x86_64.zip - os_name: macOS os: macOS-latest target: x86_64-apple-darwin bin: precious name: precious-Darwin-x86_64.tar.gz runs-on: ${{ matrix.platform.os }} steps: - name: Checkout uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true target: ${{ matrix.platform.target }} - name: Build binary uses: actions-rs/cargo@v1 with: command: build args: --release --target ${{ matrix.platform.target }} - name: Package as archive shell: bash run: | strip target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }} cd target/${{ matrix.platform.target }}/release if [[ "${{ matrix.platform.os }}" == "windows-latest" ]] then 7z a ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }} else tar czvf ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }} fi cd - - name: Generate SHA-256 if: matrix.platform.os == 'macOS-latest' run: shasum -a 256 ${{ matrix.platform.name }} - name: Publish GitHub release uses: softprops/action-gh-release@v1 with: draft: true files: "precious*" body_path: Changes.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} precious-0.1.3/.gitignore000066400000000000000000000000611420422002100153140ustar00rootroot00000000000000/bin /fatlib /**/*.rs.bk /target/**/* .\#* \#*\# precious-0.1.3/CODE_OF_CONDUCT.md000066400000000000000000000062301420422002100161270ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at autarch@urth.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org precious-0.1.3/Cargo.lock000066400000000000000000000414541420422002100152440ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ "winapi", ] [[package]] name = "anyhow" version = "1.0.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a99269dff3bc004caa411f38845c20303f1e393ca2bd6581576fa3a7f59577d" [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ "memchr", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "3.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62" dependencies = [ "atty", "bitflags", "indexmap", "os_str_bytes", "strsim", "termcolor", "textwrap", ] [[package]] name = "colored" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" dependencies = [ "atty", "lazy_static", "winapi", ] [[package]] name = "crossbeam-channel" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" dependencies = [ "cfg-if", "crossbeam-utils", "lazy_static", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-utils" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" dependencies = [ "cfg-if", "lazy_static", ] [[package]] name = "ctor" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" dependencies = [ "quote", "syn", ] [[package]] name = "diff" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "fastrand" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] [[package]] name = "fern" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9a4820f0ccc8a7afd67c39a0f1a0f4b07ca1725164271a64939d7aeb9af065" dependencies = [ "colored", "log", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "globset" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" dependencies = [ "aho-corasick", "bstr", "fnv", "log", "regex", ] [[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "ignore" version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" dependencies = [ "crossbeam-utils", "globset", "lazy_static", "log", "memchr", "regex", "same-file", "thread_local", "walkdir", "winapi-util", ] [[package]] name = "indexmap" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg", "hashbrown", "serde", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "itertools" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" dependencies = [ "either", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.119" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" [[package]] name = "lock_api" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ "cfg-if", ] [[package]] name = "md5" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "once_cell" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "os_str_bytes" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" dependencies = [ "memchr", ] [[package]] name = "output_vt100" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" dependencies = [ "winapi", ] [[package]] name = "parking_lot" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if", "instant", "libc", "redox_syscall", "smallvec", "winapi", ] [[package]] name = "path-clean" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" [[package]] name = "precious" version = "0.1.3" dependencies = [ "anyhow", "clap", "fern", "globset", "ignore", "indexmap", "itertools", "log", "md5", "path-clean", "pretty_assertions", "rayon", "serde", "serial_test", "tempfile", "thiserror", "toml", "which", ] [[package]] name = "pretty_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50" dependencies = [ "ansi_term", "ctor", "diff", "output_vt100", ] [[package]] name = "proc-macro2" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ "unicode-xid", ] [[package]] name = "quote" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" dependencies = [ "autocfg", "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", "lazy_static", "num_cpus", ] [[package]] name = "redox_syscall" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serial_test" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" dependencies = [ "lazy_static", "parking_lot", "serial_test_derive", ] [[package]] name = "serial_test_derive" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "smallvec" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] [[package]] name = "tempfile" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", "fastrand", "libc", "redox_syscall", "remove_dir_all", "winapi", ] [[package]] name = "termcolor" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" [[package]] name = "thiserror" version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thread_local" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" dependencies = [ "once_cell", ] [[package]] name = "toml" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ "serde", ] [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "walkdir" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", "winapi", "winapi-util", ] [[package]] name = "which" version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" dependencies = [ "either", "lazy_static", "libc", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" precious-0.1.3/Cargo.toml000066400000000000000000000015241420422002100152610ustar00rootroot00000000000000[package] name = "precious" version = "0.1.3" authors = ["Dave Rolsky "] description = "One code quality tool to rule them all" repository = "https://github.com/houseabsolute/precious" readme = "README.md" license = "MIT OR Apache-2.0" edition = "2018" [dependencies] anyhow = "1.0.53" clap = ">= 3.0.14, < 3.1.0" fern = { version = ">= 0.5.0, < 0.7.0", features = ["colored"] } globset = "0.4.8" ignore = "0.4.18" indexmap = { version = "1.8.0", features = ["serde"] } itertools = ">= 0.9.0, < 0.11.0" log = "0.4.14" md5 = "0.7.0" path-clean = "0.1.0" pretty_assertions = "1.1.0" rayon = "1.5.1" serde = { version = "1.0.136", features = ["derive"] } thiserror = "1.0.30" toml = "0.5.8" which = ">= 3.0.0, < 5.0.0" [dev-dependencies] serial_test = "0.5.1" tempfile = "3.3.0" [workspace.metadata.release] allow-branch = ["master"] precious-0.1.3/Changes.md000066400000000000000000000134101420422002100152200ustar00rootroot00000000000000## 0.1.3 - 2022-02-19 - Relaxed some dependencies for the benefit of packaging precious for Debian. Implemented by Jonas Smedegaard. - Added support for `.precious.toml` as a config name. Based on #21, by Olaf Alders. Fixes #13. ## 0.1.2 - 2021-10-14 - The order of commands in the config file is now preserved, and commands are executed in the order in which they appear in the config file. This addresses #12, requested by Olaf Alders. - Fixed the tests so that they set the default branch name when running `git init`, rather than setting this via `git config`. This lets anyone run the tests, whereas it was only safe to set this via `git config` in CI. This fixes #14, reported by Olaf Alders. ## 0.1.1 - 2021-07-12 - Fixed config handling of a global `exclude` key. The previous release did not handle a single string as that key's value, only an array. ## 0.1.0 - 2021-07-02 - The verbose and debugging level output now includes timing information on each linter and tidier that is run. This is helpful if you want to figure out why linting or tidying is slower than expected. - Fixed a bug in the debug output. It was not showing the correct cwd for commands where `chdir = true` was set. It always showed the project root directory instead of the directory where the command was run. It _was_ running these commands in the right directory. This was solely a bug in the debug output. ## 0.0.11 - 2021-02-20 - Fixed a bug in 0.0.10 where when _not_ running with `--debug`, precious would not honor the `expect_stderr = true` configuration, and would instead unconditionally treat stderr output as an error. ## 0.0.10 - 2021-02-20 - Errors are now printed out a bit differently, and in particular errors when trying to execute a command (not in the path, command fails unexpectedly, etc.) should be more readable now. - When running any commands, precious now explicitly checks to see if the executable is in your `PATH`. If it's not it prints a new error for this case, as opposed to when running the executable produces an error. This partially addresses #10. ## 0.0.9 - 2021-02-12 - Added a --jobs (-j) option for all subcommands. This lets you limit how many parallel threads are run. The default is to run one thread per available core. Requested by Shane Warden. GH #7. - Fixed a bug where running precious in "git staged mode" (`precious lint --staged`) would cause breakage with merge commits that were the result of resolving a merge conflict. Basically, you'd get the commit but git would no longer know it was merging a commit, because precious was running `git stash` under the hood to only check the staged files, then `git stash pop` to restore things back to their original state. But runnin`git stash` command. There's some discussion of this on [Stack Overflow](https://stackoverflow.com/questions/24637571/merge-status-lost-when-stashing) but apparently it's still an issue with git today. Reported by Carey Hoffman. GH #9. ## 0.0.8 - 2021-01-30 - Added a summary when there are problems linting or tidying files. Previously, when there were any errors, the last thing precious printed out would be something like `[precious::precious][ERROR] Error when linting files`. Now it also includes a summary of what filter failed on each path. This is primarily useful for linting, as tidy failures are typically failures to execute the tidy command. ## 0.0.7 - 2021-01-02 - Look for a `precious.toml` file in the current directory before trying to find one in the root of the current VCS checkout. - Use the current directory as the root for finding files, rather than the VCS checkout root. - When a filter command does not exist, the error output now shows the full command that was run, including any arguments. Fixes GH #6. ## 0.0.6 - 2020-08-01 - Precious can now be run outside of a VCS repo, as long as there is a `precious.toml` file in the current directory. There is probably more work to be done for precious to not expect to be run inside a VCS repo. - Fixed a bug where lint failures would still result in precious exiting with 0. I'm not sure when this bug was introduced. - Replaced deprecated failure and failure_derive crates with anyhow and thiserror. - Replaced the `on_dir` and `run_once` config flags with a single `run_mode` flag, which can be one of "files" (the default)", "dirs", or "root". If the mode is "root" then the command runs exactly once from the root of the project. - Added an `env` config key for filters. This allows you to define env vars that will be set when the filter's command is run. ## 0.0.5 - 2019-09-05 - Renamed the config key `lint_flag` to `lint_flags` so it can now be an array of strings as well as a single string. - Added a `tidy_flags` option as well. Now commands which are both must define either `lint_flags` or `tidy_flags` (or both). ## 0.0.4 - 2019-08-31 - Fixed a bug where `git stash` would be run multiple times in staged mode if you had more than one filter defined. As a bonus this also makes precious more efficient by not retrieving the list of files to check more than once. ## 0.0.3 - 2019-08-31 - Add a `run_once` flag for commands. This causes command to be run exactly once from the root directory of the project (when it applies). This lets you set your `include` and `exclude` rules based on files properly. Previously you would have to set `include = "."` and the command would run when any files changed, even files which shouldn't trigger a run. - Fixed a bug where a command with `on_dir` set to `true` would incorrectly be run when a file matched both an include _and_ exclude rule. Exclude rules should always win in these situations. ## 0.0.2 - 2019-08-13 - Documentation fixes ## 0.0.1 - 2019-08-13 - First release upon an unsuspecting world. precious-0.1.3/README.md000066400000000000000000000502371420422002100146150ustar00rootroot00000000000000# Precious - One Code Quality Tool to Rule Them All Who doesn't love linters and tidiers? I sure love them. I love them so much that in many of my projects I might easily have five or ten of them enabled! Wouldn't it be great if you could run all of them with just one command? Wouldn't it be great if that command just had one config file to define what tools to run on each part of your project? Wouldn't it be great if Sauron were our ruler? Now with Precious you can say "yes" to all of those questions. ## Why Precious? In all seriousness, managing code quality tools can be a bit of a pain. It becomes **much** more painful when you have a multi-language project. You may have multiple tools per language, each of which runs on some subset of your codebase. Then you need to hook these tools into your commit hooks and CI system. With Precious you can configure all of your code quality tool rules in one place and easily run `precious` from your commit hooks and in CI. ## Installation There are several ways to install this tool. ### Use ubi Install my [universal binary installer (ubi)](https://github.com/houseabsolute/ubi) tool and you can use it to download `precious` and many other tools. ``` $> ubi --project houseabsolute/precious --in ~/bin ``` ### Binary Releases You can grab a binary release from the [releases page](https://github.com/houseabsolute/precious/releases). Untar the tarball and put the executable it contains somewhere in your path and you're good to go. ### Cargo You can also install this via `cargo` by running `cargo install precious`. See [the cargo documentation](https://doc.rust-lang.org/cargo/commands/cargo-install.html) for the rules on where the binary is installed. ## Examples Check out this repo's [examples directory](examples) for example `precious.toml` config files for several languages. Contributions for other languages are welcome! Also check out [the example `install-dev-tools.sh`](examples/bin/install-dev-tools.sh) script. You can customize this as needed to install only the tools you need for your project. ## Configuration Precious is configured via a single `precious.toml` or `.precious.toml` file that lives in your project root. The file is in [TOML format](https://github.com/toml-lang/toml). There is just one key that can be set in the top level table of the config file: | Key | Type | Required? | Description | | --------- | ---------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `exclude` | array of strings | no | Each array member is a pattern that will be matched against potential files when `precious` is run. These patterns are matched in the same way patterns in a [gitignore file](https://git-scm.com/docs/gitignore). However, you cannot have a pattern starting with a `!` as you can in a gitignore file. | All other configuration is on a per-filter basis. A filter is something that either tidies (aka pretty prints or beautifies) or lints your code (or both). Currently all filters are defined as commands, external programs which precious will execute as needed. Each filter should be defined in a block named something like `[commands.filter-name]`. Each name after the `commands.` prefix must be unique. Note that you **can** have multiple filters defined for the same executable as long as each one has a unique name. Filters are run in the same order as they appear in the config file. The keys that are allowed for each command are as follows: | Key | Type | Required? | Applies To | Default | Description | | ------------------------- | ------------------------ | --------- | ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | strings | **yes** | all | | This must be either `lint`, `tidy`, or `both`. This defines what type of filter this is. Note that a filter which is `both` **must** define `lint_flags` or `tidy_flags` as well. | | `include` | array of strings | **yes** | all | | Each array member is a [gitignore file](https://git-scm.com/docs/gitignore) style pattern that tells `precious` what files this filter applies to. However, you cannot have a pattern starting with a `!` as you can in a gitignore file. | | `exclude` | array of strings | no | all | | Each array member is a [gitignore file](https://git-scm.com/docs/gitignore) style pattern that tells `precious` what files this filter should not be applied to. However, you cannot have a pattern starting with a `!` as you can in a gitignore file. | | `cmd` | array of strings | **yes** | all | | This is the executable to be run followed by any arguments that should always be passed. | | `env` | table of strings->string | no | all | | This key allows you to set one or more environment variables that will be set when the command is run. Both the keys and values of this table must be strings. | | `path_flag` | string | no | all | | By default, `precious` will pass each path being operated on to the command it executes as a final, positional, argument. However, if the command takes paths via a flag you need to specify that flag with this key. | | `lint_flags` | array of strings | no | combined linter & tidier | | If a command is both a linter and tidier than it may take extra flags to operate in linting mode. This is how you set that flag. | | `tidy_flags` | array of strings | no | combined linter & tidier | | If a command is both a linter and tidier than it may take extra flags to operate in tidying mode. This is how you set that flag. | | `run_mode` | "files", "dirs", "root" | no | all | "files" | This determines how the command is run. The default, "files", means that the command is run once per file that matches its include/exclude settings. If this is set to "dirs", then the command is run once per directory _containing_ files that matches its include/exclude settings. If it's set to "root", then it is run exactly once from the root of the project if it matches any files. | | `chdir` | boolean | no | all | false | If this is true, then the command will be run with a chdir to the relevant path. If the command operates on files, `precious` chdir's to the file's directory. If it operates on directories than it changes to each directory. Note that if `run_mode` is `dirs` and `chdir` is true then `precious` will not pass the path to the executable as an argument. | | `ok_exit_codes` | array of integers | **yes** | all | | Any exit code that **does not** indicate an abnormal exit should be here. For most commands this is just `0` but some commands may use other exit codes even for a normal exit. | | `lint_failure_exit_codes` | array of integers | no | linters | | If the command is a linter then these are the status codes that indicate a lint failure. These need to be specified so `precious` can distinguish an exit because of a lint failure versus an exit because of some unexpected issue. | | `expect_stderr` | boolean | all | false | | By default, `precious` assumes that when a command sends output to `stderr` that indicates a failure to lint or tidy. If this is not the case, set this to true. | ### Referencing the Project Root For tools that can be run from a subdirectory, you may need to specify config files in terms of the project root. You can do this by using the string `$PRECIOUS_ROOT` in any element of the `cmd` configuration key. So for example you might write something like this: ```toml cmd = ["some-tidier", "--config", "$PRECIOUS_ROOT/some-tidier.conf"] ``` The `$PRECIOUS_ROOT` string will be replaced by the absolute path to the project root. ## Running Precious To get help run `precious --help`. The root command takes the following options: | Flag | Description | | --------------------------- | ------------------------------------------------------------------- | | `-h`, `--help` | Prints help information | | `-q`, `--quiet` | Suppresses most output | | `-V`, `--version` | Prints version information | | `-v`, `--verbose` | Enable verbose output | | `-d`, `--debug` | Enable debugging output | | `-t`, `--trace` | Enable tracing output (maximum logging) | | `--ascii` | Replace super-fun Unicode symbols with terribly boring ASCII | | `-c`, `--config` `` | Path to config file | | `-j`, `--jobs` `` | Number of parallel jobs (threads) to run (defaults to one per core) | ### Subcommands The `precious` command has two subcommands, `lint` and `tidy`. You must always specify one of these. These subcommands take the same options, all of which are for selecting paths to operate on. ### Selecting Paths to Operate On When you run `precious` you must tell it what paths to operate on. Precious supports several ways of setting these via command line arguments: | Mode | Flag | Description | | ------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | All paths | `-a`, `--all` | Run on all paths in the project. | | Modified files according to git | `-g`, `--git` | Run on all files that git reports as having been modified. | | Staged files according to git | `-s`, `--staged` | Run on all files that git reports as having been staged. This will stash unstaged changes while it runs and pop the stash at the end. This ensures that filters only run against the staged version of your codebase. | | Paths given on CLI | | If you don't pass any of the above flags then `precious` will expect one or more paths to be passed on the command line after all other options. If any of these paths are directories then that entire directory tree will be included. | #### Default Exclusions When selecting paths `precious` _always_ respects your ignore files. Right now it only knows how this works for git, and it will respect all of the following ignore files: - Per-directory `.ignore` and `.gitignore` files. - The `.git/info/exclude` file. - Global gitignore globs, usually found in `$XDG_CONFIG_HOME/git/ignore`. This is implemented using the [rust `ignore` crate](https://crates.io/crates/ignore), so adding support for other VCS systems should be proposed there. In addition, you can specify excludes for all filters by setting a global `exclude` key. Finally, you can specify per-filter `include` and `exclude` keys. When `precious` runs it does the following to determine which filters apply to which paths. - The base paths are selected based on the command line option specified. - VCS ignore rules are applied to remove paths from this list. - Each filter is given either the files or directories from the list of paths, depending on the `run_mode` setting for that filter. - If the filter's `run_mode` is `root`, then it will get all of the files in all directories and will use those to determine whether to run or not. These filters are always run exactly once if any of the files match. - The filter will check its include and exclude rules. The path must match at least one include rule _and_ not match any exclude rules to be accepted. - If the filter is per-file, it matches each path against its rules as is. - If the filter is per-directory, it matches the files in the directory against its include and exclude rules. If _any_ of the files match the filter is run. If _none_ of the files match the filter is not run. ## Configuration Recommendations Here are some recommendations for how to get the best experience with precious. ### Choosing a Run Mode Some tools might work equally well with "root" or "dirs" as a the run mode. The right run mode to choose depends on how you are using precious. In general, if you either have a very small set of directories, _or_ you are running precious on most or all of the directories at once, then the "root" mode will be faster. However, if you have a larger set of directories and you only need to lint or tidy a small subset of these at once, then "dirs" mode will be faster. ### Quiet Flags Many tools will accept a "quiet" flag of some sort. In general, you probably _do not_ want to run tools in a quiet mode with precious. In the case of a successful tidy or lint command execution, precious already traps all stdout from the command that it runs. If the command fails somehow, precious will print out stdout (and stderr) output. By default, precious treats _any_ output to stderr as an error in the command (as opposed to a linting failure). If you set `expect_stderr = true`, then precious treats stderr just like stdout. In addition, you can see all stdout and stderr output when running precious in `--debug` mode. All of which is to say that in general there's no value to running a command in quiet mode with precious. All that does it potentially make it harder to debug issues with that command. ## Common Scenarios There are some configuration scenarios that you may need to handle. Here are some examples: ### Linter runs just once for the entire source tree Some linters, such as [rust-clippy](https://github.com/rust-lang/rust-clippy), expect to run just once across the entire source tree, rather than once per file or directory. In order to make that happen you should use the following config: ```toml include = "**/*.rs" run_mode = "root" ``` This combination of flags will cause `precious` to run the command exactly once in the project root. The above config will pass a path to the command, `.`. If the command does not need a path, set `chdir` to `true`: ```toml include = "**/*.rs" run_mode = "root" chdir = true ``` ### Linter runs in the same directory as the files it lints and does not accept path as arguments If you want to run the command without passing the path being operated on to the command, set `run_mode` to `dirs` and add the `chdir` flag: ```toml include = "**/*.rs" run_mode = "dirs" chdir = true ``` ### You want a command to exclude an entire directory (tree) except for one file There's no good way to do this with a single filter's `include` and `exclude`, as `excluding` a directory means that any attempt to `include` a file under that directory will be ignored. Instead, you can configure the same command twice: ```toml [commands.rustfmt-most] type = "both" include = "**/*.rs" exclude = "path/to/dir" cmd = ["rustfmt"] lint_flags = "--check" ok_exit_codes = [0] lint_failure_exit_codes = [1] [commands.rustfmt-that-file] type = "both" include = "path/to/dir/that.rs" cmd = ["rustfmt"] lint_flags = "--check" ok_exit_codes = [0] lint_failure_exit_codes = [1] ``` ### You want to run Precious as a commit hook Simply run `precious lint -s` in your hook. It will exit with a non-zero status if any of the lint filters indicate a linting problem. ### You want to run filters in a specific order As of version 0.1.2, filters are run in the same order as they appear in the config file. ## Build Status [![Build Status](https://travis-ci.com/houseabsolute/precious.svg?branch=master)](https://travis-ci.com/houseabsolute/precious) precious-0.1.3/dev/000077500000000000000000000000001420422002100141055ustar00rootroot00000000000000precious-0.1.3/dev/bin/000077500000000000000000000000001420422002100146555ustar00rootroot00000000000000precious-0.1.3/dev/bin/install-dev-tools.sh000077500000000000000000000015421420422002100205760ustar00rootroot00000000000000#!/bin/bash set -eo pipefail function run () { echo $1 eval $1 } function install_tools () { curl --silent --location \ https://raw.githubusercontent.com/houseabsolute/ubi/master/bootstrap/bootstrap-ubi.sh | sh run "rustup component add clippy" run "ubi --project houseabsolute/precious --in ~/bin" run "ubi --project houseabsolute/omegasort --in ~/bin" run "sudo npm install --global prettier" } if [ "$1" == "-v" ]; then set -x fi mkdir -p $HOME/bin set +e echo ":$PATH:" | grep --extended-regexp ":$HOME/bin:" >& /dev/null if [ "$?" -eq "0" ]; then path_has_home_bin=1 fi set -e if [ -z "$path_has_home_bin" ]; then PATH=$HOME/bin:$PATH fi install_tools echo "Tools were installed into $HOME/bin." if [ -z "$path_has_home_bin" ]; then echo "You should add $HOME/bin to your PATH." fi exit 0 precious-0.1.3/examples/000077500000000000000000000000001420422002100151455ustar00rootroot00000000000000precious-0.1.3/examples/bin/000077500000000000000000000000001420422002100157155ustar00rootroot00000000000000precious-0.1.3/examples/bin/install-dev-tools.sh000066400000000000000000000026661420422002100216430ustar00rootroot00000000000000#!/bin/bash set -eo pipefail function run () { echo $1 eval $1 } function install_any_project_tools () { curl --silent --location \ https://raw.githubusercontent.com/houseabsolute/ubi/master/bootstrap/bootstrap-ubi.sh | sh run "ubi --project houseabsolute/precious --in ~/bin" run "ubi --project houseabsolute/omegasort --in ~/bin" } function install_go_project_tools () { run "ubi --project golangci/golangci-lint --in ~/bin" # If we run this in the checkout dir it can mess with out go.mod and # go.sum. pushd /tmp # This will end up in $GOBIN, which defaults to $HOME/go/bin. run "go get golang.org/x/tools/cmd/goimports" popd } function install_rust_project_tools () { run "rustup component add clippy" } if [ "$1" == "-v" ]; then set -x fi mkdir -p $HOME/bin set +e echo ":$PATH:" | grep --extended-regexp ":$HOME/bin:" >& /dev/null if [ "$?" -eq "0" ]; then path_has_home_bin=1 fi set -e if [ -z "$path_has_home_bin" ]; then PATH=$HOME/bin:$PATH fi install_any_project_tools install_go_project_tools install_rust_project_tools echo "Tools were installed into $HOME/bin." if [ -z "$path_has_home_bin" ]; then echo "You should add $HOME/bin to your PATH." fi # For Perl, you would generally expect to have a cpanfile in the project root # that included the relevant develop prereqs, so developers could just run # `cpanm --installdeps --with-develop .` exit 0 precious-0.1.3/examples/golang/000077500000000000000000000000001420422002100164145ustar00rootroot00000000000000precious-0.1.3/examples/golang/helpers/000077500000000000000000000000001420422002100200565ustar00rootroot00000000000000precious-0.1.3/examples/golang/helpers/check-go-mod.sh000066400000000000000000000015151420422002100226510ustar00rootroot00000000000000#!/bin/bash set -e ROOT=$( git rev-parse --show-toplevel ) BEFORE_MOD=$( md5sum "$ROOT/go.mod" ) BEFORE_SUM=$( md5sum "$ROOT/go.sum" ) OUTPUT=$( go mod tidy -v 2>&1 ) AFTER_MOD=$( md5sum "$ROOT/go.mod" ) AFTER_SUM=$( md5sum "$ROOT/go.sum" ) red=$'\e[1;31m' end=$'\e[0m' if [ "$BEFORE_MOD" != "$AFTER_MOD" ]; then printf "${red}Running go mod tidy changed the contents of go.mod${end}\n" git diff "$ROOT/go.mod" changed=1 fi if [ "$BEFORE_SUM" != "$AFTER_SUM" ]; then printf "${red}Running go mod tidy changed the contents of go.sum${end}\n" git diff "$ROOT/go.sum" changed=1 fi if [ -n "$changed" ]; then if [ -n "$OUTPUT" ]; then printf "\nOutput from running go mod tidy -v:\n${OUTPUT}\n" else printf "\nThere was no output from running go mod tidy -v\n\n" fi exit 1 fi exit 0 precious-0.1.3/examples/golang/precious.toml000066400000000000000000000030121420422002100211360ustar00rootroot00000000000000# See examples/bin/install-dev-tools.sh for an example script to install all # of the tools needed by this config. You can copy it into your project and # modify it as needed to only install the tools you need. [commands.golangci-lint] type = "lint" include = "**/*.go" run_mode = "dirs" cmd = [ "golangci-lint", "run", "-c", "$PRECIOUS_ROOT/golangci-lint.yml", # Allowing golangci-lint to run in parallel reduces the effectiveness of # its cache when it has to parse the same code repeatedly. Depending on # the structure of your repo, you may get a better result by using the # `--allow-serial-runners` flag instead. However, you must use one of # these, as by default golangci-lint can simply timeout and fail when # multiple instances of the executable as invoked at the same time for the # same project. "--allow-parallel-runners", ] env = { "FAIL_ON_WARNINGS" = "1" } ok_exit_codes = [0] lint_failure_exit_codes = [1] [commands.goimports] type = "tidy" include = "**/*.go" cmd = ["goimports", "-w"] ok_exit_codes = [0] # See check-go-mod.sh in helpers dir [commands.check-go-mod] type = "lint" include = "**/*.go" run_mode = "root" chdir = true cmd = ["$PRECIOUS_ROOT/dev/bin/check-go-mod.sh"] expect_stderr = true ok_exit_codes = [0] lint_failure_exit_codes = [1] [commands.omegasort-gitignore] type = "both" include = "**/.gitignore" cmd = [ "omegasort", "--sort=path" ] lint_flags = "--check" tidy_flags = "--in-place" ok_exit_codes = 0 lint_failure_exit_codes = 1 expect_stderr = true precious-0.1.3/examples/perl/000077500000000000000000000000001420422002100161075ustar00rootroot00000000000000precious-0.1.3/examples/perl/precious.toml000066400000000000000000000043241420422002100206400ustar00rootroot00000000000000# See examples/bin/install-dev-tools.sh for an example script to install all # of the tools needed by this config. You can copy it into your project and # modify it as needed to only install the tools you need. # This list is appropriate for a Perl distribution that you upload to # CPAN. For Perl applications you develop locally, you can probably remove # most or all of these. exclude = [ # Used by Dist::Zilla ".build", # Replace with your distro name "DateTime-*", "blib", # All of these are generated by Dist::Zilla "t/00-*", "t/author-*", "t/release-*", "xt/author", "xt/release", ] [commands.omegasort-gitignore] type = "both" include = "**/.gitignore" cmd = [ "omegasort", "--sort=path" ] lint_flags = "--check" tidy_flags = "--in-place" ok_exit_codes = 0 lint_failure_exit_codes = 1 expect_stderr = true # If you have an external stopwords file for use with Test::Spelling [commands.omegasort-stopwords] type = "both" include = ".stopwords" cmd = [ "omegasort", "--sort=text", "--case-insensitive" ] lint_flags = "--check" tidy_flags = "--in-place" ok_exit_codes = 0 lint_failure_exit_codes = 1 expect_stderr = true # Add Perl::Critic as a develop phase prereq [commands.perlcritic] type = "lint" include = [ "**/*.{pl,pm,t,psgi}" ] cmd = [ "perlcritic", "--profile=$PRECIOUS_ROOT/perlcriticrc" ] ok_exit_codes = 0 lint_failure_exit_codes = 2 # Add Perl::Tidy as a develop phase prereq [commands.perltidy] type = "both" include = [ "**/*.{pl,pm,t,psgi}" ] cmd = [ "perltidy", "--profile=$PRECIOUS_ROOT/perltidyrc" ] lint_flags = [ "--assert-tidy", "--no-standard-output", "--outfile=/dev/null" ] tidy_flags = [ "--backup-and-modify-in-place", "--backup-file-extension=/" ] ok_exit_codes = 0 lint_failure_exit_codes = 2 expect_stderr = true # Add Pod::Checker as a develop phase prereq [commands.podchecker] type = "lint" include = [ "**/*.{pl,pm,pod}" ] cmd = [ "podchecker", "--warnings", "--warnings" ] ok_exit_codes = [ 0, 2 ] lint_failure_exit_codes = 1 expect_stderr = true # Add Pod::Tidy as a develop phase prereq [commands.podtidy] type = "tidy" include = [ "**/*.{pl,pm,pod}" ] cmd = [ "podtidy", "--columns", "80", "--inplace", "--nobackup" ] ok_exit_codes = 0 lint_failure_exit_codes = 1 precious-0.1.3/examples/rust/000077500000000000000000000000001420422002100161425ustar00rootroot00000000000000precious-0.1.3/examples/rust/precious.toml000066400000000000000000000025061420422002100206730ustar00rootroot00000000000000# See examples/bin/install-dev-tools.sh for an example script to install all # of the tools needed by this config. You can copy it into your project and # modify it as needed to only install the tools you need. exclude = [ "target", ] # If you install a nightly rustfmt from the project's GitHub releases # (https://github.com/rust-lang/rustfmt/releases) you can change the cmd to: # # cmd = [ "rustfmt", "--skip-children", "--unstable-features" ] # # This stops "rustfmt --check" from showing errors in main.rs or lib.rs # because of errors in files containing modules imported by main.rs/lib.rs. # # Try ubi (https://github.com/houseabsolute/ubi) for installing rustfmt and # other single-file executables. [commands.rustfmt] type = "both" include = "**/*.rs" cmd = "rustfmt" lint_flags = "--check" ok_exit_codes = 0 lint_failure_exit_codes = 1 [commands.clippy] type = "lint" include = "**/*.rs" run_mode = "root" chdir = true cmd = [ "cargo", "clippy", "--locked", "--all-targets", "--all-features", "--", "-D", "clippy::all" ] ok_exit_codes = 0 lint_failure_exit_codes = 101 expect_stderr = true [commands.omegasort-gitignore] type = "both" include = "**/.gitignore" cmd = [ "omegasort", "--sort=path" ] lint_flags = "--check" tidy_flags = "--in-place" ok_exit_codes = 0 lint_failure_exit_codes = 1 expect_stderr = true precious-0.1.3/git/000077500000000000000000000000001420422002100141125ustar00rootroot00000000000000precious-0.1.3/git/hooks/000077500000000000000000000000001420422002100152355ustar00rootroot00000000000000precious-0.1.3/git/hooks/pre-commit.sh000077500000000000000000000002701420422002100176470ustar00rootroot00000000000000#!/bin/bash status=0 PRECIOUS=$(which precious) if [[ -z $PRECIOUS ]]; then PRECIOUS=./bin/precious fi "$PRECIOUS" lint -s if (( $? != 0 )); then status+=1 fi exit $status precious-0.1.3/git/setup.pl000077500000000000000000000007021420422002100156110ustar00rootroot00000000000000#!/usr/bin/env perl use strict; use warnings; use Cwd qw( abs_path ); symlink_hook('pre-commit'); sub symlink_hook { my $hook = shift; my $dot = ".git/hooks/$hook"; my $file = "git/hooks/$hook.sh"; my $link = "../../$file"; if ( -e $dot ) { if ( -l $dot ) { return if readlink $dot eq $link; } warn "You already have a hook at $dot!\n"; return; } symlink $link, $dot; } precious-0.1.3/precious.toml000066400000000000000000000015551420422002100160630ustar00rootroot00000000000000exclude = [ "target", ] [commands.rustfmt] type = "both" include = "**/*.rs" cmd = [ "rustfmt", "--edition", "2021" ] lint_flags = "--check" ok_exit_codes = 0 lint_failure_exit_codes = 1 [commands.clippy] type = "lint" include = "**/*.rs" run_mode = "root" chdir = true cmd = [ "cargo", "clippy", "--locked", "--all-targets", "--all-features", "--", "-D", "clippy::all" ] ok_exit_codes = 0 lint_failure_exit_codes = 101 expect_stderr = true [commands.prettier] type = "both" include = [ "**/*.md", "**/*.yml" ] cmd = "prettier" lint_flags = "--check" tidy_flags = "--write" ok_exit_codes = 0 lint_failure_exit_codes = 1 [commands.omegasort-gitignore] type = "both" include = "**/.gitignore" cmd = [ "omegasort", "--sort=path" ] lint_flags = "--check" tidy_flags = "--in-place" ok_exit_codes = 0 lint_failure_exit_codes = 1 expect_stderr = true precious-0.1.3/src/000077500000000000000000000000001420422002100141165ustar00rootroot00000000000000precious-0.1.3/src/basepaths.rs000066400000000000000000000515011420422002100164400ustar00rootroot00000000000000use crate::command; use crate::path_matcher; use crate::vcs; use anyhow::Result; use itertools::Itertools; use log::{debug, error}; use path_clean::PathClean; use std::collections::HashMap; use std::fmt; use std::path::{Path, PathBuf}; use std::str; use thiserror::Error; #[derive(Clone, Copy, Debug, PartialEq)] pub enum Mode { FromCli, All, GitModified, GitStaged, } impl fmt::Display for Mode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Mode::FromCli => write!(f, "paths passed on the Cli (recursively)"), Mode::All => write!(f, "all files in the project"), Mode::GitModified => write!(f, "modified files according to git"), Mode::GitStaged => write!(f, "files staged for a git commit"), } } } #[derive(Debug)] pub struct BasePaths { mode: Mode, root: PathBuf, exclude_globs: Vec, stashed: bool, } #[derive(Clone, Debug, PartialEq)] pub struct Paths { pub dir: PathBuf, pub files: Vec, } #[derive(Debug, Error, PartialEq)] pub enum BasePathsError { #[error("You cannot pass an explicit list of files when looking for {mode:}")] GotPathsFromCliWithWrongMode { mode: Mode }, #[error("Found some paths when looking for {mode:} but they were all excluded")] AllPathsWereExcluded { mode: Mode }, #[error("Found a path on the Cli which does not exist: {path:}")] NonExistentPathOnCli { path: String }, #[error("Could not determine the repo root by running \"git rev-parse --show-toplevel\"")] CouldNotDetermineRepoRoot, } impl BasePaths { pub fn new(mode: Mode, root: PathBuf, exclude_globs: Vec) -> Result { Ok(BasePaths { mode, root, exclude_globs, stashed: false, }) } pub fn paths(&mut self, cli_paths: Vec) -> Result>> { match self.mode { Mode::FromCli => (), _ => { if !cli_paths.is_empty() { return Err( BasePathsError::GotPathsFromCliWithWrongMode { mode: self.mode }.into(), ); } } }; let files = match self.mode { Mode::All => self.all_files()?, Mode::FromCli => self.files_from_cli(cli_paths)?, Mode::GitModified => self.git_modified_files()?, Mode::GitStaged => self.git_staged_files()?, }; if files.is_none() { return Ok(None); } self.maybe_git_stash()?; self.files_to_paths(files.unwrap()) } fn maybe_git_stash(&mut self) -> Result<()> { if self.mode != Mode::GitStaged { return Ok(()); } let res = command::run_command( String::from("git"), ["rev-parse", "--show-toplevel"] .iter() .map(|a| (*a).to_string()) .collect(), &HashMap::new(), &[0], false, Some(&self.root), )?; let stdout = res .stdout .ok_or(BasePathsError::CouldNotDetermineRepoRoot)?; let repo_root = stdout.trim(); let mut mm = PathBuf::from(repo_root); mm.push(".git"); mm.push("MERGE_MODE"); if !mm.exists() { command::run_command( String::from("git"), ["stash", "--keep-index"] .iter() .map(|a| (*a).to_string()) .collect(), &HashMap::new(), &[0], false, Some(&self.root), )?; self.stashed = true; } Ok(()) } fn all_files(&self) -> Result>> { debug!("Getting all files under {}", self.root.to_string_lossy()); match self.walkdir_files(self.root.as_path())? { Some(all) => Ok(Some(self.relative_files(all)?)), None => Ok(None), } } fn files_from_cli(&self, cli_paths: Vec) -> Result>> { debug!("Using the list of files passed from the command line"); let excluder = self.excluder()?; let mut files: Vec = vec![]; for rel in self.relative_files(cli_paths)? { let full = self.root.clone().join(rel.clone()); if !full.exists() { return Err(BasePathsError::NonExistentPathOnCli { path: rel.to_string_lossy().to_string(), } .into()); } if excluder.path_matches(&rel) { continue; } if full.is_dir() { files.append(self.walkdir_files(&full)?.unwrap().as_mut()); } else { files.push(rel); } } Ok(Some(files)) } fn git_modified_files(&self) -> Result>> { debug!("Getting modified files according to git"); self.files_from_git(&["diff", "--name-only", "--diff-filter=ACM"]) } fn git_staged_files(&self) -> Result>> { debug!("Getting staged files according to git"); self.files_from_git(&["diff", "--cached", "--name-only", "--diff-filter=ACM"]) } fn walkdir_files(&self, root: &Path) -> Result>> { let mut excludes = ignore::overrides::OverrideBuilder::new(root); for e in &self.exclude_globs { excludes.add(&format!("!{}", e))?; } for d in vcs::dirs() { excludes.add(&format!("!{}/**/*", d))?; } let mut files: Vec = vec![]; for result in ignore::WalkBuilder::new(root) .hidden(false) .overrides(excludes.build()?) .build() { match result { Ok(ent) => { if ent.path().is_dir() { continue; } files.push(ent.into_path()); } Err(e) => return Err(e.into()), }; } Ok(Some(self.relative_files(files)?)) } fn files_from_git(&self, args: &[&str]) -> Result>> { let result = command::run_command( String::from("git"), args.iter().map(|a| String::from(*a)).collect(), &HashMap::new(), &[0], false, Some(&self.root), )?; let excluder = self.excluder()?; match result.stdout { Some(s) => Ok(Some( self.relative_files( s.lines() .filter_map(|rel| { if excluder.path_matches(&PathBuf::from(rel)) { return None; } let mut f = self.root.clone(); f.push(rel); Some(f) }) .collect(), )?, )), None => Ok(None), } } fn excluder(&self) -> Result { let mut globs = self.exclude_globs.clone(); let mut v = vcs::dirs(); globs.append(&mut v); path_matcher::Matcher::new(globs.as_ref()) } fn files_to_paths(&self, files: Vec) -> Result>> { let mut entries: HashMap> = HashMap::new(); for f in files { let dir = f.parent().unwrap().to_path_buf(); entries .entry(dir) .and_modify(|e| e.push(f.clone())) .or_insert_with(|| vec![f.clone()]); } if entries.is_empty() { return Err(BasePathsError::AllPathsWereExcluded { mode: self.mode }.into()); } Ok(Some( entries .keys() .sorted() .map(|k| { let mut files = entries.get(k).unwrap().to_vec(); files.sort(); Paths { dir: k.to_path_buf().clean(), files, } }) .collect(), )) } // We want to make all files relative. This lets us consistently produce // path names starting at the root dir (without "./"). fn relative_files(&self, files: Vec) -> Result> { let mut relative: Vec = vec![]; for mut f in files { if !f.is_absolute() { f = self.root.clone().join(f); } // If the directory given is just "." then the first clean() // removes that and we then strip the prefix, leaving an empty // string. The second clean turns that back into ".". relative.push(f.clean().strip_prefix(&self.root)?.to_path_buf().clean()); } Ok(relative) } } impl Drop for BasePaths { fn drop(&mut self) { if !self.stashed { return; } let res = command::run_command( String::from("git"), ["stash", "pop"].iter().map(|a| (*a).to_string()).collect(), &HashMap::new(), &[0], false, Some(&self.root), ); if res.is_ok() { return; } error!("Error popping stash: {}", res.unwrap_err()); } } #[cfg(test)] mod tests { use super::*; use crate::testhelper; use anyhow::Result; use pretty_assertions::assert_eq; use std::fs; fn new_basepaths(mode: Mode, root: PathBuf) -> Result { new_basepaths_with_excludes(mode, root, vec![]) } fn new_basepaths_with_excludes( mode: Mode, root: PathBuf, exclude: Vec, ) -> Result { BasePaths::new(mode, root, exclude) } #[test] fn files_to_paths() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let bp = new_basepaths(Mode::All, helper.root())?; let paths = bp.files_to_paths(helper.all_files())?.unwrap(); assert_eq!(paths.len(), 3, "got three paths entries"); assert_eq!( paths[0], Paths { dir: PathBuf::from("."), files: ["README.md", "can_ignore.x", "merge-conflict-file"] .iter() .map(PathBuf::from) .collect(), } ); assert_eq!( paths[1], Paths { dir: PathBuf::from("src"), files: [ "src/bar.rs", "src/can_ignore.rs", "src/main.rs", "src/module.rs", ] .iter() .map(PathBuf::from) .collect(), } ); assert_eq!( paths[2], Paths { dir: PathBuf::from("tests/data"), files: [ "tests/data/bar.txt", "tests/data/foo.txt", "tests/data/generated.txt", ] .iter() .map(PathBuf::from) .collect(), } ); Ok(()) } #[test] fn all_mode() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let mut bp = new_basepaths(Mode::All, helper.root())?; assert_eq!(bp.paths(vec![])?, bp.files_to_paths(helper.all_files())?); Ok(()) } #[test] fn all_mode_with_gitignore() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let mut gitignores = helper.add_gitignore_files()?; let mut expect = testhelper::TestHelper::non_ignored_files(); expect.append(&mut gitignores); let mut bp = new_basepaths(Mode::All, helper.root())?; assert_eq!(bp.paths(vec![])?, bp.files_to_paths(expect)?); Ok(()) } #[test] fn git_modified_mode_empty() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let mut bp = new_basepaths(Mode::GitModified, helper.root())?; let res = bp.paths(vec![]); assert!(res.is_ok()); assert!(res.unwrap().is_none()); Ok(()) } #[test] fn git_modified_mode_with_changes() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let modified = helper.modify_files()?; let mut bp = new_basepaths(Mode::GitModified, helper.root())?; let expect = bp.files_to_paths( modified .iter() .sorted_by(|a, b| a.cmp(b)) .map(PathBuf::from) .collect::>(), )?; assert_eq!(bp.paths(vec![])?, expect); Ok(()) } #[test] fn git_modified_mode_with_excluded_files() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; helper.write_file(&PathBuf::from("vendor/foo/bar.txt"), "initial content")?; helper.stage_all()?; helper.commit_all()?; let modified = helper.modify_files()?; helper.write_file(&PathBuf::from("vendor/foo/bar.txt"), "new content")?; let mut bp = new_basepaths_with_excludes( Mode::GitModified, helper.root(), vec!["vendor/**/*".to_string()], )?; let expect = bp.files_to_paths( modified .iter() .sorted_by(|a, b| a.cmp(b)) .map(PathBuf::from) .collect::>(), )?; assert_eq!(bp.paths(vec![])?, expect); Ok(()) } #[test] fn git_staged_mode_empty() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let mut bp = new_basepaths(Mode::GitStaged, helper.root())?; let res = bp.paths(vec![]); assert!(res.is_ok()); assert!(res.unwrap().is_none()); Ok(()) } #[test] fn git_staged_mode_with_changes() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let modified = helper.modify_files()?; { let mut bp = new_basepaths(Mode::GitStaged, helper.root())?; let res = bp.paths(vec![]); assert!(res.is_ok()); assert!(res.unwrap().is_none()); } { let mut bp = new_basepaths(Mode::GitStaged, helper.root())?; helper.stage_all()?; let expect = bp.files_to_paths( modified .iter() .sorted_by(|a, b| a.cmp(b)) .map(PathBuf::from) .collect::>(), )?; assert_eq!(bp.paths(vec![])?, expect); } Ok(()) } #[test] fn git_staged_mode_with_excluded_files() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let modified = helper.modify_files()?; helper.write_file(&PathBuf::from("vendor/foo/bar.txt"), "initial content")?; helper.stage_all()?; let mut bp = new_basepaths_with_excludes( Mode::GitStaged, helper.root(), vec!["vendor/**/*".to_string()], )?; let expect = bp.files_to_paths( modified .iter() .sorted_by(|a, b| a.cmp(b)) .map(PathBuf::from) .collect::>(), )?; assert_eq!(bp.paths(vec![])?, expect); Ok(()) } #[test] fn git_staged_mode_stashes_unindexed() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let modified = helper.modify_files()?; helper.stage_all()?; let unstaged = "tests/data/bar.txt"; helper.write_file(&PathBuf::from(unstaged), "new content")?; { let mut bp = new_basepaths(Mode::GitStaged, helper.root())?; let expect = bp.files_to_paths( modified .iter() .sorted_by(|a, b| a.cmp(b)) .map(PathBuf::from) .collect::>(), )?; assert_eq!(bp.paths(vec![])?, expect); assert_eq!( String::from_utf8(fs::read(helper.root().join(unstaged))?)?, String::from("some content"), ); } assert_eq!( String::from_utf8(fs::read(helper.root().join(unstaged))?)?, String::from("new content"), ); Ok(()) } // This tests the issue reported in // https://github.com/houseabsolute/precious/issues/9. I had tried to test // for this earlier, but I thought it was a non-issue because I couldn't // replicate the issue. Later, I realized that this only happens if a // merge commit leads to a conflict. Otherwise, `git diff --cached` won't // report any files at all for the commit. But if you've had a conflict // and resolved it, any files that had a conflict will be reported as // having a diff. #[test] fn git_staged_mode_merge_stash() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let file = Path::new("merge-conflict-here"); helper.write_file(file, "line 1\nline 2\n")?; helper.stage_all()?; helper.commit_all()?; helper.switch_to_branch("new-branch", false)?; helper.write_file(file, "line 1\nline 1.5\nline 2\n")?; helper.commit_all()?; helper.switch_to_branch("master", true)?; helper.write_file(file, "line 1\nline 1.6\nline 2\n")?; helper.commit_all()?; helper.switch_to_branch("new-branch", true)?; helper.merge_master(true)?; helper.write_file(file, "line 1\nline 1.7\nline 2\n")?; helper.stage_all()?; let mut bp = new_basepaths(Mode::GitStaged, helper.root())?; let expect = bp.files_to_paths(vec![PathBuf::from("merge-conflict-here")])?; assert_eq!(bp.paths(vec![])?, expect); assert!(!bp.stashed); Ok(()) } #[test] fn cli_mode() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let mut bp = new_basepaths(Mode::FromCli, helper.root())?; let expect = bp.files_to_paths( helper .all_files() .iter() .filter(|p| p.starts_with("tests/")) .sorted_by(|a, b| a.cmp(b)) .map(PathBuf::from) .collect::>(), )?; assert_eq!(bp.paths(vec![PathBuf::from("tests")])?, expect); Ok(()) } #[test] fn cli_mode_given_dir_with_excluded_files() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; helper.write_file(&PathBuf::from("vendor/foo/bar.txt"), "initial content")?; let mut bp = new_basepaths_with_excludes( Mode::FromCli, helper.root(), vec!["vendor/**/*".to_string()], )?; let expect = bp.files_to_paths( helper .all_files() .iter() .sorted_by(|a, b| a.cmp(b)) .map(PathBuf::from) .collect::>(), )?; assert_eq!(bp.paths(vec![PathBuf::from(".")])?, expect); Ok(()) } #[test] fn cli_mode_given_files_with_excluded_files() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; helper.write_file(&PathBuf::from("vendor/foo/bar.txt"), "initial content")?; let mut bp = new_basepaths_with_excludes( Mode::FromCli, helper.root(), vec!["vendor/**/*".to_string()], )?; let expect = bp.files_to_paths(vec![helper.all_files()[0].clone()])?; let cli_paths = vec![ helper.all_files()[0].clone(), PathBuf::from("vendor/foo/bar.txt"), ]; assert_eq!(bp.paths(cli_paths)?, expect); Ok(()) } #[test] fn cli_mode_given_files_with_nonexistent_path() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let mut bp = new_basepaths(Mode::FromCli, helper.root())?; let cli_paths = vec![ helper.all_files()[0].clone(), PathBuf::from("does/not/exist"), ]; let res = bp.paths(cli_paths); assert!(res.is_err()); assert_eq!( std::mem::discriminant(res.unwrap_err().downcast_ref().unwrap(),), std::mem::discriminant(&BasePathsError::NonExistentPathOnCli { path: String::from("does/not/exist"), }), ); Ok(()) } } precious-0.1.3/src/chars.rs000066400000000000000000000013131420422002100155620ustar00rootroot00000000000000#[derive(Debug, PartialEq)] pub struct Chars { pub ring: &'static str, pub tidied: &'static str, pub unchanged: &'static str, pub lint_free: &'static str, pub lint_dirty: &'static str, pub empty: &'static str, pub bullet: &'static str, pub execution_error: &'static str, } pub const FUN_CHARS: Chars = Chars { ring: "💍", tidied: "💧", unchanged: "✨", lint_free: "💯", lint_dirty: "💩", empty: "⚫", bullet: "▶", execution_error: "💥", }; pub const BORING_CHARS: Chars = Chars { ring: ":", tidied: "*", unchanged: "|", lint_free: "|", lint_dirty: "*", empty: "_", bullet: "*", execution_error: "!", }; precious-0.1.3/src/command.rs000066400000000000000000000230531420422002100161050ustar00rootroot00000000000000use anyhow::{Context, Result}; use log::Level::Debug; use log::{debug, error, log_enabled}; use std::collections::HashMap; use std::env; use std::fs; use std::path::Path; use std::process; use thiserror::Error; use which::which; #[cfg(target_family = "unix")] use std::os::unix::prelude::*; #[derive(Debug, Error)] pub enum CommandError { #[error(r#"Could not find "{exe:}" in your path ({path:}"#)] ExecutableNotInPath { exe: String, path: String }, #[error("Got unexpected exit code {code:} from `{cmd:}`")] UnexpectedExitCode { cmd: String, code: i32 }, #[error("Got unexpected exit code {code:} from `{cmd:}`. Stderr was {stderr:}")] UnexpectedExitCodeWithStderr { cmd: String, code: i32, stderr: String, }, #[error("Ran `{cmd:}` and it was killed by signal {signal:}")] ProcessKilledBySignal { cmd: String, signal: i32 }, #[error("Got unexpected stderr output from `{cmd:}`:\n{stderr:}")] UnexpectedStderr { cmd: String, stderr: String }, } #[derive(Debug)] pub struct CommandResult { pub exit_code: i32, pub stdout: Option, pub stderr: Option, } pub fn run_command( cmd: String, args: Vec, env: &HashMap, ok_exit_codes: &[i32], expect_stderr: bool, in_dir: Option<&Path>, ) -> Result { if which(&cmd).is_err() { let path = match env::var("PATH") { Ok(p) => p, Err(e) => format!("", e), }; return Err(CommandError::ExecutableNotInPath { exe: cmd, path }.into()); } let mut c = process::Command::new(&cmd); for a in args.iter() { c.arg(a); } // We are canonicalizing this primarily for the benefit of our debugging // output, because otherwise we might see the current dir as just `.`, // which is not helpful. let cwd = if let Some(id) = in_dir { fs::canonicalize(id)? } else { fs::canonicalize(env::current_dir()?)? }; c.current_dir(cwd.clone()); c.envs(env); if log_enabled!(Debug) { let cstr = command_string(&cmd, &args); debug!( "Running command [{}] with cwd = {}", cstr, cwd.to_string_lossy(), ); } let output = output_from_command(c, ok_exit_codes, &cmd, &args).with_context(|| { format!( r#"Failed to execute command `{}`"#, command_string(&cmd, &args) ) })?; if log_enabled!(Debug) && !output.stdout.is_empty() { debug!("Stdout was:\n{}", String::from_utf8(output.stdout.clone())?); } if !output.stderr.is_empty() { if log_enabled!(Debug) { debug!("Stderr was:\n{}", String::from_utf8(output.stderr.clone())?); } if !expect_stderr { return Err(CommandError::UnexpectedStderr { cmd: command_string(&cmd, &args), stderr: String::from_utf8(output.stderr)?, } .into()); } } let code = output.status.code().unwrap_or(-1); Ok(CommandResult { exit_code: code, stdout: to_option_string(output.stdout), stderr: to_option_string(output.stderr), }) } fn output_from_command( mut c: process::Command, ok_exit_codes: &[i32], cmd: &str, args: &[String], ) -> Result { let output = c.output()?; match output.status.code() { Some(code) => { let cstr = command_string(cmd, args); debug!("Ran {} and got exit code of {}", cstr, code); if !ok_exit_codes.contains(&code) { if output.stderr.is_empty() { return Err(CommandError::UnexpectedExitCode { cmd: cstr, code }.into()); } else { return Err(CommandError::UnexpectedExitCodeWithStderr { cmd: cstr, code, stderr: String::from_utf8(output.stderr)?, } .into()); } } } None => { let cstr = command_string(cmd, args); if output.status.success() { error!("Ran {} successfully but it had no exit code", cstr); } else { let signal = signal_from_status(output.status); debug!("Ran {} which exited because of signal {}", cstr, signal); return Err(CommandError::ProcessKilledBySignal { cmd: cstr, signal }.into()); } } } Ok(output) } fn command_string(cmd: &str, args: &[String]) -> String { let mut cstr = cmd.to_string(); if !args.is_empty() { cstr.push(' '); cstr.push_str(args.join(" ").as_str()); } cstr } fn to_option_string(v: Vec) -> Option { if v.is_empty() { None } else { Some(String::from_utf8_lossy(&v).into_owned()) } } #[cfg(target_family = "unix")] fn signal_from_status(status: process::ExitStatus) -> i32 { status.signal().unwrap_or(0) } #[cfg(target_family = "windows")] fn signal_from_status(_: process::ExitStatus) -> i32 { 0 } #[cfg(test)] mod tests { use crate::testhelper; use anyhow::Result; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::env; use tempfile::tempdir; #[test] fn command_string() { assert_eq!( super::command_string(&String::from("foo"), &[]), String::from("foo"), "command without args", ); assert_eq!( super::command_string(&String::from("foo"), &[String::from("bar")],), String::from("foo bar"), "command with one arg" ); assert_eq!( super::command_string( &String::from("foo"), &[String::from("--bar"), String::from("baz")], ), String::from("foo --bar baz"), "command with multiple args", ); } #[test] fn run_command() -> Result<()> { let res = super::run_command( String::from("echo"), vec![String::from("foo")], &HashMap::new(), &[0], false, None, )?; assert_eq!(res.exit_code, 0, "command exits 0"); let env_key = "PRECIOUS_ENV_TEST"; let mut env = HashMap::new(); env.insert(String::from(env_key), String::from("foo")); let res = super::run_command( String::from("sh"), vec![String::from("-c"), format!("echo ${}", env_key)], &env, &[0], false, None, )?; assert_eq!(res.exit_code, 0, "command exits 0"); assert!(res.stdout.is_some(), "command has stdout output"); assert_eq!( res.stdout.unwrap(), String::from("foo\n"), "{} env var was set when command was run", env_key, ); let val = env::var(env_key); assert_eq!( val.err().unwrap(), std::env::VarError::NotPresent, "{} env var is not set after command was run", env_key, ); let res = super::run_command( String::from("sh"), vec![String::from("-c"), String::from("exit 32")], &HashMap::new(), &[0], false, None, ); assert!(res.is_err(), "command exits non-zero"); match res { Ok(_) => panic!("did not get an error in the returned Result"), Err(e) => { let r = e.downcast_ref::(); match r { Some(c) => match c { super::CommandError::UnexpectedExitCode { cmd: _, code } => { assert_eq!(code, &32, "command unexpectedly exits 32"); } _ => panic!("expected a CommandError::UnexpectedExitCode "), }, None => panic!("expected an error, not a None"), } } } Ok(()) } #[test] fn run_command_in_dir() -> Result<()> { // On windows the path we get from `pwd` is a Windows path (C:\...) // but `td.path()` contains a Unix path (/tmp/...). Very confusing. if cfg!(windows) { return Ok(()); } let td = tempdir()?; let td_path = testhelper::maybe_canonicalize(td.path())?; let res = super::run_command( String::from("pwd"), vec![], &HashMap::new(), &[0], false, Some(td_path.as_ref()), )?; assert_eq!(res.exit_code, 0, "command exits 0"); assert!(res.stdout.is_some(), "command produced stdout output"); let stdout = res.stdout.unwrap(); let stdout_trimmed = stdout.trim_end(); assert_eq!( stdout_trimmed, td_path.to_string_lossy(), "command runs in another dir", ); Ok(()) } #[test] fn executable_does_not_exist() { let exe = "I hope this binary does not exist on any system!"; let args = vec![String::from("--arg"), String::from("42")]; let res = super::run_command(String::from(exe), args, &HashMap::new(), &[0], false, None); assert!(res.is_err()); if let Err(e) = res { assert!(e.to_string().contains( r#"Could not find "I hope this binary does not exist on any system!" in your path"#, )); } } } precious-0.1.3/src/config.rs000066400000000000000000000307051420422002100157360ustar00rootroot00000000000000use crate::filter; use anyhow::Result; use indexmap::IndexMap; use serde::de; use serde::de::Deserializer; use serde::Deserialize; use std::collections::HashMap; use std::fmt; use std::fs; use std::marker::PhantomData; use std::path::Path; use thiserror::Error; #[derive(Debug, Deserialize)] pub struct FilterCore { #[serde(rename = "type")] typ: filter::FilterType, #[serde(deserialize_with = "string_or_seq_string")] include: Vec, #[serde(default)] #[serde(deserialize_with = "string_or_seq_string")] exclude: Vec, #[serde(default = "default_run_mode")] run_mode: filter::RunMode, #[serde(deserialize_with = "string_or_seq_string")] cmd: Vec, #[serde(default)] env: HashMap, } #[derive(Debug, Deserialize)] pub struct Command { #[serde(flatten)] core: FilterCore, #[serde(default)] chdir: bool, #[serde(default)] #[serde(deserialize_with = "string_or_seq_string")] lint_flags: Vec, #[serde(default)] #[serde(deserialize_with = "string_or_seq_string")] tidy_flags: Vec, #[serde(default = "empty_string")] path_flag: String, #[serde(deserialize_with = "u8_or_seq_u8")] ok_exit_codes: Vec, #[serde(default)] #[serde(deserialize_with = "u8_or_seq_u8")] lint_failure_exit_codes: Vec, #[serde(default)] expect_stderr: bool, } fn default_run_mode() -> filter::RunMode { filter::RunMode::Files } fn empty_string() -> String { String::new() } #[derive(Debug, Deserialize)] pub struct Config { #[serde(default)] #[serde(deserialize_with = "string_or_seq_string")] pub exclude: Vec, commands: IndexMap, } #[derive(Debug, Error)] pub enum ConfigError { #[error("File at {file:} cannot be read: {error:}")] FileCannotBeRead { file: String, error: std::io::Error }, } // Copied from https://stackoverflow.com/a/43627388 - CC-BY-SA 3.0 fn string_or_seq_string<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { struct StringOrVec(PhantomData>); impl<'de> de::Visitor<'de> for StringOrVec { type Value = Vec; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("string or list of strings") } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(vec![value.to_owned()]) } fn visit_seq(self, visitor: S) -> Result where S: de::SeqAccess<'de>, { Deserialize::deserialize(de::value::SeqAccessDeserializer::new(visitor)) } } deserializer.deserialize_any(StringOrVec(PhantomData)) } fn u8_or_seq_u8<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { struct U8OrVec(PhantomData>); impl<'de> de::Visitor<'de> for U8OrVec { type Value = Vec; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("integer or list of integers") } fn visit_i8(self, value: i8) -> Result where E: de::Error, { if value < 0 { return Err(de::Error::invalid_type( de::Unexpected::Signed(value as i64), &"an integer from 0-255", )); } Ok(vec![value as u8]) } fn visit_i16(self, value: i16) -> Result where E: de::Error, { if value < 0 || value > std::u8::MAX as i16 { return Err(de::Error::invalid_type( de::Unexpected::Signed(value as i64), &"an integer from 0-255", )); } Ok(vec![value as u8]) } fn visit_i32(self, value: i32) -> Result where E: de::Error, { if value < 0 || value > std::u8::MAX as i32 { return Err(de::Error::invalid_type( de::Unexpected::Signed(value as i64), &"an integer from 0-255", )); } Ok(vec![value as u8]) } fn visit_i64(self, value: i64) -> Result where E: de::Error, { if value < 0 || value > std::u8::MAX as i64 { return Err(de::Error::invalid_type( de::Unexpected::Signed(value as i64), &"an integer from 0-255", )); } Ok(vec![value as u8]) } fn visit_u8(self, value: u8) -> Result where E: de::Error, { Ok(vec![value]) } fn visit_u16(self, value: u16) -> Result where E: de::Error, { if value > std::u8::MAX as u16 { return Err(de::Error::invalid_type( de::Unexpected::Unsigned(value as u64), &"an integer from 0-255", )); } Ok(vec![value as u8]) } fn visit_u32(self, value: u32) -> Result where E: de::Error, { if value > std::u8::MAX as u32 { return Err(de::Error::invalid_type( de::Unexpected::Unsigned(value as u64), &"an integer from 0-255", )); } Ok(vec![value as u8]) } fn visit_u64(self, value: u64) -> Result where E: de::Error, { if value > std::u8::MAX as u64 { return Err(de::Error::invalid_type( de::Unexpected::Unsigned(value as u64), &"an integer from 0-255", )); } Ok(vec![value as u8]) } fn visit_seq(self, visitor: S) -> Result where S: de::SeqAccess<'de>, { Deserialize::deserialize(de::value::SeqAccessDeserializer::new(visitor)) } } deserializer.deserialize_any(U8OrVec(PhantomData)) } impl Config { pub fn new(file: &Path) -> Result { match fs::read(file) { Err(e) => { return Err(ConfigError::FileCannotBeRead { file: file.to_string_lossy().to_string(), error: e, } .into()); } Ok(bytes) => Ok(toml::from_slice(&bytes)?), } } pub fn tidy_filters(&self, root: &Path) -> Result> { let mut tidiers: Vec = vec![]; for (name, c) in self.commands.iter() { if let filter::FilterType::Lint = c.core.typ { continue; } tidiers.push(self.make_command(root, name, c)?); } Ok(tidiers) } pub fn lint_filters(&self, root: &Path) -> Result> { let mut linters: Vec = vec![]; for (name, c) in self.commands.iter() { if let filter::FilterType::Tidy = c.core.typ { continue; } linters.push(self.make_command(root, name, c)?); } Ok(linters) } fn make_command(&self, root: &Path, name: &str, command: &Command) -> Result { let n = filter::Command::build(filter::CommandParams { root: root.to_owned(), name: name.to_owned(), typ: command.core.typ, include: command.core.include.clone(), exclude: command.core.exclude.clone(), run_mode: command.core.run_mode, chdir: command.chdir, cmd: command.core.cmd.clone(), env: command.core.env.clone(), lint_flags: command.lint_flags.clone(), tidy_flags: command.tidy_flags.clone(), path_flag: command.path_flag.clone(), ok_exit_codes: command.ok_exit_codes.clone(), lint_failure_exit_codes: command.lint_failure_exit_codes.clone(), expect_stderr: command.expect_stderr, })?; Ok(n) } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn filter_order_is_preserved1() -> Result<()> { let toml_text = r#" [commands.rustfmt] type = "both" include = "**/*.rs" cmd = [ "rustfmt", "--skip-children", "--unstable-features" ] lint_flags = "--check" ok_exit_codes = 0 lint_failure_exit_codes = 1 [commands.clippy] type = "lint" include = "**/*.rs" run_mode = "root" chdir = true cmd = "$PRECIOUS_ROOT/dev/bin/force-clippy.sh" ok_exit_codes = 0 lint_failure_exit_codes = 101 expect_stderr = true [commands.omegasort-gitignore] type = "both" include = "**/.gitignore" cmd = [ "omegasort", "--sort=path" ] lint_flags = "--check" tidy_flags = "--in-place" ok_exit_codes = 0 lint_failure_exit_codes = 1 expect_stderr = true "#; let config: Config = toml::from_str(toml_text)?; let keys = config .commands .keys() .map(|k| k.as_str()) .collect::>(); let expect: Vec<&str> = vec!["rustfmt", "clippy", "omegasort-gitignore"]; assert_eq!(keys, expect); Ok(()) } #[test] fn filter_order_is_preserved2() -> Result<()> { let toml_text = r#" [commands.clippy] type = "lint" include = "**/*.rs" run_mode = "root" chdir = true cmd = "$PRECIOUS_ROOT/dev/bin/force-clippy.sh" ok_exit_codes = 0 lint_failure_exit_codes = 101 expect_stderr = true [commands.rustfmt] type = "both" include = "**/*.rs" cmd = [ "rustfmt", "--skip-children", "--unstable-features" ] lint_flags = "--check" ok_exit_codes = 0 lint_failure_exit_codes = 1 [commands.omegasort-gitignore] type = "both" include = "**/.gitignore" cmd = [ "omegasort", "--sort=path" ] lint_flags = "--check" tidy_flags = "--in-place" ok_exit_codes = 0 lint_failure_exit_codes = 1 expect_stderr = true "#; let config: Config = toml::from_str(toml_text)?; let keys = config .commands .keys() .map(|k| k.as_str()) .collect::>(); let expect: Vec<&str> = vec!["clippy", "rustfmt", "omegasort-gitignore"]; assert_eq!(keys, expect); Ok(()) } #[test] fn filter_order_is_preserved3() -> Result<()> { let toml_text = r#" [commands.omegasort-gitignore] type = "both" include = "**/.gitignore" cmd = [ "omegasort", "--sort=path" ] lint_flags = "--check" tidy_flags = "--in-place" ok_exit_codes = 0 lint_failure_exit_codes = 1 expect_stderr = true [commands.clippy] type = "lint" include = "**/*.rs" run_mode = "root" chdir = true cmd = "$PRECIOUS_ROOT/dev/bin/force-clippy.sh" ok_exit_codes = 0 lint_failure_exit_codes = 101 expect_stderr = true [commands.rustfmt] type = "both" include = "**/*.rs" cmd = [ "rustfmt", "--skip-children", "--unstable-features" ] lint_flags = "--check" ok_exit_codes = 0 lint_failure_exit_codes = 1 "#; let config: Config = toml::from_str(toml_text)?; let keys = config .commands .keys() .map(|k| k.as_str()) .collect::>(); let expect: Vec<&str> = vec!["omegasort-gitignore", "clippy", "rustfmt"]; assert_eq!(keys, expect); Ok(()) } } precious-0.1.3/src/filter.rs000066400000000000000000000703651420422002100157640ustar00rootroot00000000000000use crate::command; use crate::path_matcher; use anyhow::Result; use log::{debug, info}; use serde::Deserialize; use std::collections::{HashMap, HashSet}; use std::fmt; use std::fs; use std::path::{Path, PathBuf}; use std::time::SystemTime; use thiserror::Error; #[derive(Clone, Copy, Debug, Deserialize)] pub enum FilterType { #[serde(rename = "lint")] Lint, #[serde(rename = "tidy")] Tidy, #[serde(rename = "both")] Both, } impl FilterType { fn what(&self) -> &'static str { match self { FilterType::Lint => "lint", FilterType::Tidy => "tidier", FilterType::Both => "linter/tidier", } } } #[derive(Clone, Copy, Debug, Deserialize)] pub enum RunMode { #[serde(rename = "files")] Files, #[serde(rename = "dirs")] Dirs, #[serde(rename = "root")] Root, } #[derive(Debug, Error)] enum FilterError { #[error( "You cannot create a Command which lints and tidies without lint_flags and/or tidy_flags" )] CommandWhichIsBothRequiresLintOrTidyFlags, #[error( "You can only pass paths to files to the {method:} method for this filter, you passed {path:}" )] CanOnlyOperateOnFiles { method: &'static str, path: String }, #[error( "You can only pass paths to directories to the {method:} method for this filter, you passed {path:}" )] CanOnlyOperateOnDirectories { method: &'static str, path: String }, #[error("")] CannotX { what: &'static str, typ: &'static str, method: &'static str, }, #[error( "Cannot compare previous state of {path:} to its current state because we did not record its previous state!" )] CannotComparePaths { path: String }, } pub struct Filter { root: PathBuf, pub name: String, typ: FilterType, includer: path_matcher::Matcher, excluder: path_matcher::Matcher, pub run_mode: RunMode, implementation: Box, } // This should be safe because we never mutate the Filter struct in any of its // methods. unsafe impl Sync for Filter {} impl fmt::Debug for Filter { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // I'm not sure how to get any useful info for the implementation // field so we'll just leave it out for now. write!( f, "{{ root: {:?}, name: {:?}, typ: {:?}, includer: {:?}, excluder: {:?}, run_mode: {:?} }}", self.root, self.name, self.typ, self.includer, self.excluder, self.run_mode, ) } } #[derive(Debug)] pub struct LintResult { pub ok: bool, pub stdout: Option, pub stderr: Option, } pub trait FilterImplementation { fn tidy(&self, name: &str, path: &Path) -> Result<()>; fn lint(&self, name: &str, path: &Path) -> Result; fn filter_key(&self) -> &str; } #[derive(Debug)] struct PathInfo { mtime: SystemTime, size: u64, hash: md5::Digest, } fn run_mode_is(mode1: &RunMode, mode2: &RunMode) -> bool { std::mem::discriminant(mode1) == std::mem::discriminant(mode2) } impl Filter { pub fn tidy(&self, path: &Path, files: &[PathBuf]) -> Result> { self.require_is_not_filter_type(FilterType::Lint)?; let mut full = self.root.clone(); full.push(path); self.require_path_type("tidy", &full)?; if !self.should_process_path(path, files) { return Ok(None); } let info = Self::path_info_map_for(&full)?; self.implementation.tidy(&self.name, path)?; Ok(Some(Self::path_was_changed(&full, &info)?)) } pub fn lint(&self, path: &Path, files: &[PathBuf]) -> Result> { self.require_is_not_filter_type(FilterType::Tidy)?; let mut full = self.root.clone(); full.push(path); self.require_path_type("lint", &full)?; if !self.should_process_path(path, files) { return Ok(None); } let r = self.implementation.lint(&self.name, path)?; Ok(Some(r)) } fn require_is_not_filter_type(&self, not_allowed: FilterType) -> Result<()> { if std::mem::discriminant(¬_allowed) == std::mem::discriminant(&self.typ) { return Err(FilterError::CannotX { what: "command", typ: self.typ.what(), method: "tidy", } .into()); } Ok(()) } fn require_path_type(&self, method: &'static str, path: &Path) -> Result<()> { if self.run_mode_is(RunMode::Root) { return Ok(()); } let is_dir = fs::metadata(path)?.is_dir(); if self.run_mode_is(RunMode::Dirs) && !is_dir { return Err(FilterError::CanOnlyOperateOnDirectories { method, path: path.to_string_lossy().to_string(), } .into()); } else if self.run_mode_is(RunMode::Files) && is_dir { return Err(FilterError::CanOnlyOperateOnFiles { method, path: path.to_string_lossy().to_string(), } .into()); } Ok(()) } pub fn run_mode_is(&self, mode: RunMode) -> bool { run_mode_is(&self.run_mode, &mode) } fn should_process_path(&self, path: &Path, files: &[PathBuf]) -> bool { if self.excluder.path_matches(path) { debug!( "Path {} is excluded for the {} filter", path.to_string_lossy(), self.name, ); return false; } if self.includer.path_matches(path) { debug!( "Path {} is included in the {} filter", path.to_string_lossy(), self.name ); return true; } if !self.run_mode_is(RunMode::Files) { for f in files { if self.excluder.path_matches(f) { continue; } if self.includer.path_matches(f) { debug!( "Directory {} is included in the {} filter because it contains {} which is included", path.to_string_lossy(), self.name, f.to_string_lossy(), ); return true; } } debug!( "Directory {} is not included in the {} filter because neither it nor its files are included", path.to_string_lossy(), self.name ); return false; } debug!( "Path {} is not included in the {} filter", path.to_string_lossy(), self.name ); false } fn path_was_changed(path: &Path, prev: &HashMap) -> Result { let meta = fs::metadata(path)?; if meta.is_file() { if !prev.contains_key(path) { return Err(FilterError::CannotComparePaths { path: path.to_string_lossy().to_string(), } .into()); } let prev_info = prev.get(path).unwrap(); // If the mtime is unchanged we don't need to compare anything // else. Unfortunately there's no guarantee a filter won't modify // the mtime even if it doesn't change the file's contents. For // example, Perl::Tidy does this :( if prev_info.mtime == meta.modified()? { return Ok(false); } // If the size changed we know the contents changed. if prev_info.size != meta.len() { return Ok(true); } // Otherwise we need to compare the content hash. return Ok(prev_info.hash != md5::compute(fs::read(path)?)); } for entry in path.read_dir()? { if let Err(e) = entry { return Err(e.into()); } let e = entry.unwrap(); if e.metadata()?.is_dir() { continue; } if prev.contains_key(&e.path()) && Self::path_was_changed(&e.path(), prev)? { return Ok(true); } // We can only assume that when an entry is not found in the // previous hash that the filter must have added a new file. return Ok(true); } Ok(false) } fn path_info_map_for(path: &Path) -> Result> { let meta = fs::metadata(path)?; if meta.is_dir() { let mut info = HashMap::new(); for entry in path.read_dir()? { match entry { Ok(e) => { // We do not recurse into subdirs. Our assumption is // that filters which operate on a dir do not recurse // either (thinking of things like golint, etc.). if !e.metadata()?.is_dir() { for (k, v) in Self::path_info_map_for(&e.path())?.drain() { info.insert(k.clone(), v); } } } Err(e) => return Err(e.into()), } } return Ok(info); } let mut info = HashMap::new(); info.insert( path.to_owned(), PathInfo { mtime: meta.modified()?, size: meta.len(), hash: md5::compute(fs::read(path)?), }, ); Ok(info) } pub fn config_key(&self) -> String { format!( "{}.{}", self.implementation.filter_key(), Self::maybe_toml_quote(&self.name), ) } fn maybe_toml_quote(name: &str) -> String { if name.contains(' ') { return format!(r#""{}""#, name); } name.to_string() } } #[derive(Debug)] pub struct Command { cmd: Vec, env: HashMap, chdir: bool, lint_flags: Option>, tidy_flags: Option>, path_flag: Option, ok_exit_codes: HashSet, lint_failure_exit_codes: HashSet, run_mode: RunMode, expect_stderr: bool, } pub struct CommandParams { pub root: PathBuf, pub name: String, pub typ: FilterType, pub include: Vec, pub exclude: Vec, pub run_mode: RunMode, pub chdir: bool, pub cmd: Vec, pub env: HashMap, pub lint_flags: Vec, pub tidy_flags: Vec, pub path_flag: String, pub ok_exit_codes: Vec, pub lint_failure_exit_codes: Vec, pub expect_stderr: bool, } impl Command { pub fn build(params: CommandParams) -> Result { if let FilterType::Both = params.typ { if params.lint_flags.is_empty() && params.tidy_flags.is_empty() { return Err(FilterError::CommandWhichIsBothRequiresLintOrTidyFlags.into()); } } let cmd = replace_root(params.cmd, ¶ms.root); Ok(Filter { root: params.root, name: params.name, typ: params.typ, includer: path_matcher::Matcher::new(¶ms.include)?, excluder: path_matcher::Matcher::new(¶ms.exclude)?, run_mode: params.run_mode, implementation: Box::new(Command { cmd, env: params.env, chdir: params.chdir, lint_flags: if params.lint_flags.is_empty() { None } else { Some(params.lint_flags) }, tidy_flags: if params.tidy_flags.is_empty() { None } else { Some(params.tidy_flags) }, path_flag: if params.path_flag.is_empty() { None } else { Some(params.path_flag) }, ok_exit_codes: Self::exit_codes_hashset( ¶ms.ok_exit_codes, Some(¶ms.lint_failure_exit_codes), ), lint_failure_exit_codes: Self::exit_codes_hashset( ¶ms.lint_failure_exit_codes, None, ), run_mode: params.run_mode, expect_stderr: params.expect_stderr, }), }) } fn exit_codes_hashset( ok_exit_codes: &[u8], lint_failure_exit_codes: Option<&[u8]>, ) -> HashSet { let mut len = ok_exit_codes.len(); if let Some(lfec) = lint_failure_exit_codes { len += lfec.len(); } let mut hash: HashSet = HashSet::with_capacity(len); for c in ok_exit_codes { hash.insert(i32::from(*c)); } if let Some(lfec) = lint_failure_exit_codes { for c in lfec { hash.insert(i32::from(*c)); } } hash } fn in_dir<'a>(&self, path: &'a Path) -> Option<&'a Path> { if !self.chdir { return None; } if path.is_dir() { return Some(path); } Some(path.parent().unwrap()) } fn run_mode_is(&self, mode: RunMode) -> bool { run_mode_is(&self.run_mode, &mode) } fn command_for_path(&self, path: &Path, flags: &Option>) -> Vec { let mut cmd = self.cmd.clone(); if let Some(flags) = flags { for f in flags { cmd.push(f.clone()); } } if self.run_mode_is(RunMode::Files) || !self.chdir { if let Some(pf) = &self.path_flag { cmd.push(pf.clone()); } cmd.push(path.to_string_lossy().to_string()); } cmd } } impl FilterImplementation for Command { fn tidy(&self, name: &str, path: &Path) -> Result<()> { let mut cmd = self.command_for_path(path, &self.tidy_flags); info!( "Tidying {} with {} command: {}", path.to_string_lossy(), name, cmd.join(" "), ); let ok_exit_codes: Vec = self.ok_exit_codes.iter().cloned().collect(); match command::run_command( cmd.remove(0), cmd, &self.env, &ok_exit_codes, self.expect_stderr, self.in_dir(path), ) { Ok(_) => Ok(()), Err(e) => Err(e), } } fn lint(&self, name: &str, path: &Path) -> Result { let mut cmd = self.command_for_path(path, &self.lint_flags); info!( "Linting {} with {} command: {}", path.to_string_lossy(), name, cmd.join(" "), ); let ok_exit_codes: Vec = self.ok_exit_codes.iter().cloned().collect(); match command::run_command( cmd.remove(0), cmd, &self.env, &ok_exit_codes, self.expect_stderr, self.in_dir(path), ) { Ok(result) => Ok(LintResult { ok: !self.lint_failure_exit_codes.contains(&result.exit_code), stdout: result.stdout, stderr: result.stderr, }), Err(e) => Err(e), } } fn filter_key(&self) -> &str { "commands" } } // #[derive(Debug)] // pub struct Server { // name: String, // typ: FilterType, // include: GlobSet, // excluder: path_matcher::Matcher, // cmd: Vec, // run_mode: RunMode, // port: u16, // } fn replace_root(cmd: Vec, root: &Path) -> Vec { cmd.iter() .map(|c| { c.replace( "$PRECIOUS_ROOT", root.to_string_lossy().into_owned().as_str(), ) }) .collect() } #[cfg(test)] mod tests { use super::*; use crate::path_matcher; use crate::testhelper; use anyhow::Result; use pretty_assertions::assert_eq; type Mock = i8; impl FilterImplementation for Mock { fn tidy(&self, _: &str, _: &Path) -> Result<()> { Ok(()) } fn lint(&self, _: &str, _: &Path) -> Result { Ok(LintResult { ok: true, stdout: None, stderr: None, }) } fn filter_key(&self) -> &str { "commands" } } fn mock_filter() -> Box { Box::new(1) } fn matcher(globs: &[&str]) -> Result { path_matcher::Matcher::new( &globs .iter() .map(|g| String::from(*g)) .collect::>(), ) } #[test] fn require_path_type_dir() -> Result<()> { let filter = Filter { root: PathBuf::from("/foo/bar"), name: String::from("Test"), typ: FilterType::Lint, includer: matcher(&[])?, excluder: matcher(&[])?, run_mode: RunMode::Dirs, implementation: mock_filter(), }; let helper = testhelper::TestHelper::new()?.with_git_repo()?; assert!(filter.require_path_type("tidy", &helper.root()).is_ok()); let mut file = helper.root(); file.push(helper.all_files()[0].clone()); let res = filter.require_path_type("tidy", &file); assert!(res.is_err()); assert_eq!( std::mem::discriminant(res.unwrap_err().downcast_ref().unwrap(),), std::mem::discriminant(&FilterError::CanOnlyOperateOnDirectories { method: "tidy", path: file.to_string_lossy().to_string(), }), ); Ok(()) } #[test] fn require_path_type_file() -> Result<()> { let filter = Filter { root: PathBuf::from("/foo/bar"), name: String::from("Test"), typ: FilterType::Lint, includer: matcher(&[])?, excluder: matcher(&[])?, run_mode: RunMode::Files, implementation: mock_filter(), }; let helper = testhelper::TestHelper::new()?.with_git_repo()?; let res = filter.require_path_type("tidy", &helper.root()); assert!(res.is_err()); assert_eq!( std::mem::discriminant(res.unwrap_err().downcast_ref().unwrap(),), std::mem::discriminant(&FilterError::CanOnlyOperateOnFiles { method: "tidy", path: helper.root().to_string_lossy().to_string(), }), ); let mut file = helper.root(); file.push(helper.all_files()[0].clone()); assert!(filter.require_path_type("tidy", &file).is_ok()); Ok(()) } #[test] fn should_process_path() -> Result<()> { let filter = Filter { root: PathBuf::from("/foo/bar"), name: String::from("Test"), typ: FilterType::Lint, includer: matcher(&["**/*.go"])?, excluder: matcher(&["foo/**/*", "baz/bar/**/quux/*"])?, run_mode: RunMode::Files, implementation: mock_filter(), }; let include = &["something.go", "dir/foo.go", ".foo.go", "bar/foo/x.go"]; for i in include.iter().map(PathBuf::from) { let name = i.clone(); assert!( filter.should_process_path(&i.clone(), &[i]), "{}", name.to_string_lossy(), ); } let exclude = &[ "something.pl", "dir/foo.pl", "foo/bar.go", "baz/bar/anything/here/quux/file.go", ]; for e in exclude.iter().map(PathBuf::from) { let name = e.clone(); assert!( !filter.should_process_path(&e.clone(), &[e]), "{}", name.to_string_lossy(), ); } Ok(()) } #[test] fn should_process_path_run_mode_dirs() -> Result<()> { let filter = Filter { root: PathBuf::from("/foo/bar"), name: String::from("Test"), typ: FilterType::Lint, includer: matcher(&["**/*.go"])?, excluder: matcher(&["foo/**/*", "baz/bar/**/quux/*"])?, run_mode: RunMode::Dirs, implementation: mock_filter(), }; let include = &[ &[".", "foo.go", "README.md"], &["dir/foo", "dir/foo/foo.pl", "dir/foo/file.go"], ]; for i in include.iter() { let dir = PathBuf::from(i[0]); let files = i[1..].iter().map(PathBuf::from).collect::>(); let name = dir.clone(); assert!( filter.should_process_path(&dir, &files), "{}", name.to_string_lossy(), ); } let exclude = &[ &["foo", "foo/bar.go", "foo/baz.go"], &[ "baz/bar/foo/quux", "baz/bar/foo/quux/file.go", "baz/bar/foo/quux/other.go", ], &["dir", "dir/foo.pl", "dir/file.txt"], ]; for e in exclude.iter() { let dir = PathBuf::from(e[0]); let files = e[1..].iter().map(PathBuf::from).collect::>(); let name = dir.clone(); assert!( !filter.should_process_path(&dir, &files), "{}", name.to_string_lossy(), ); } Ok(()) } #[test] fn should_process_path_run_mode_root() -> Result<()> { let filter = Filter { root: PathBuf::from("/foo/bar"), name: String::from("Test"), typ: FilterType::Lint, includer: matcher(&["**/*.go"])?, excluder: matcher(&["foo/**/*", "baz/bar/**/quux/*"])?, run_mode: RunMode::Root, implementation: mock_filter(), }; let include = &[ &[".", "foo.go", "README.md"], &["dir/foo", "dir/foo/foo.pl", "dir/foo/file.go"], ]; for i in include.iter() { let dir = PathBuf::from(i[0]); let files = i[1..].iter().map(PathBuf::from).collect::>(); let name = dir.clone(); assert!( filter.should_process_path(&dir, &files), "{}", name.to_string_lossy(), ); } let exclude = &[ &["foo", "foo/bar.go", "foo/baz.go"], &[ "baz/bar/foo/quux", "baz/bar/foo/quux/file.go", "baz/bar/foo/quux/other.go", ], &["dir", "dir/foo.pl", "dir/file.txt"], ]; for e in exclude.iter() { let dir = PathBuf::from(e[0]); let files = e[1..].iter().map(PathBuf::from).collect::>(); let name = dir.clone(); assert!( !filter.should_process_path(&dir, &files), "{}", name.to_string_lossy(), ); } Ok(()) } #[test] fn command_for_path() { { let command = Command { cmd: vec!["test".to_string()], env: HashMap::new(), chdir: false, lint_flags: None, tidy_flags: None, path_flag: None, ok_exit_codes: HashSet::new(), lint_failure_exit_codes: HashSet::new(), run_mode: RunMode::Root, expect_stderr: false, }; assert_eq!( command.command_for_path(Path::new("foo.go"), &None), vec!["test".to_string(), "foo.go".to_string()], "root mode, no chdir", ); } { let command = Command { cmd: vec!["test".to_string()], env: HashMap::new(), chdir: false, lint_flags: None, tidy_flags: None, path_flag: None, ok_exit_codes: HashSet::new(), lint_failure_exit_codes: HashSet::new(), run_mode: RunMode::Root, expect_stderr: false, }; assert_eq!( command.command_for_path(Path::new("foo.go"), &Some(vec!["--flag".to_string()])), vec![ "test".to_string(), "--flag".to_string(), "foo.go".to_string(), ], "root mode, no chdir with flags", ); } { let command = Command { cmd: vec!["test".to_string()], env: HashMap::new(), chdir: true, lint_flags: None, tidy_flags: None, path_flag: None, ok_exit_codes: HashSet::new(), lint_failure_exit_codes: HashSet::new(), run_mode: RunMode::Root, expect_stderr: false, }; assert_eq!( command.command_for_path(Path::new("foo.go"), &None), vec!["test".to_string()], "root mode, with chdir", ); } { let command = Command { cmd: vec!["test".to_string()], env: HashMap::new(), chdir: true, lint_flags: None, tidy_flags: None, path_flag: None, ok_exit_codes: HashSet::new(), lint_failure_exit_codes: HashSet::new(), run_mode: RunMode::Files, expect_stderr: false, }; assert_eq!( command.command_for_path(Path::new("foo.go"), &None), vec!["test".to_string(), "foo.go".to_string()], "files mode, with chdir", ); } { let command = Command { cmd: vec!["test".to_string()], env: HashMap::new(), chdir: false, lint_flags: None, tidy_flags: None, path_flag: None, ok_exit_codes: HashSet::new(), lint_failure_exit_codes: HashSet::new(), run_mode: RunMode::Files, expect_stderr: false, }; assert_eq!( command.command_for_path(Path::new("foo.go"), &None), vec!["test".to_string(), "foo.go".to_string()], "files mode, no chdir", ); } { let command = Command { cmd: vec!["test".to_string()], env: HashMap::new(), chdir: false, lint_flags: None, tidy_flags: None, path_flag: Some("--file".to_string()), ok_exit_codes: HashSet::new(), lint_failure_exit_codes: HashSet::new(), run_mode: RunMode::Files, expect_stderr: false, }; assert_eq!( command.command_for_path(Path::new("foo.go"), &None), vec![ "test".to_string(), "--file".to_string(), "foo.go".to_string(), ], "files mode, no chdir, with path flag" ); } { let command = Command { cmd: vec!["test".to_string()], env: HashMap::new(), chdir: true, lint_flags: None, tidy_flags: None, path_flag: Some("--file".to_string()), ok_exit_codes: HashSet::new(), lint_failure_exit_codes: HashSet::new(), run_mode: RunMode::Files, expect_stderr: false, }; assert_eq!( command.command_for_path(Path::new("foo.go"), &None), vec![ "test".to_string(), "--file".to_string(), "foo.go".to_string(), ], "files mode, with chdir, with path flag", ); } } } precious-0.1.3/src/main.rs000066400000000000000000000011651420422002100154130ustar00rootroot00000000000000#![recursion_limit = "1024"] #[cfg(test)] mod testhelper; mod basepaths; mod chars; mod command; mod config; mod filter; mod path_matcher; mod precious; mod vcs; use log::error; fn main() { let matches = precious::app().get_matches(); let res = precious::init_logger(&matches); if let Err(e) = res { eprintln!("Error creating logger: {}", e); std::process::exit(1); } let p = precious::Precious::new(&matches); let status = match p { Ok(mut p) => p.run(), Err(e) => { error!("{}", e); 1 } }; std::process::exit(status as i32); } precious-0.1.3/src/path_matcher.rs000066400000000000000000000041541420422002100171270ustar00rootroot00000000000000use anyhow::Result; use globset::{Glob, GlobSet, GlobSetBuilder}; use std::path::Path; #[derive(Debug)] pub struct Matcher { globs: GlobSet, } impl Matcher { pub fn new(globs: &[String]) -> Result { let mut builder = GlobSetBuilder::new(); for g in globs { builder.add(Glob::new(g.as_str())?); } Ok(Matcher { globs: builder.build()?, }) } pub fn path_matches(&self, path: &Path) -> bool { self.globs.is_match(path) } } #[cfg(test)] mod tests { use super::*; use anyhow::Result; use std::path::PathBuf; struct TestSet { globs: Vec, yes: &'static [&'static str], no: &'static [&'static str], } #[test] fn path_matches() -> Result<()> { let tests = vec![ TestSet { globs: vec![String::from("*.foo")], yes: &["file.foo", "./file.foo"], no: &["file.bar", "./file.bar"], }, TestSet { globs: vec![String::from("*.foo"), String::from("**/foo/*")], yes: &[ "file.foo", "/baz/bar/file.foo", "/contains/foo/any.txt", "./file.foo", "./baz/bar/file.foo", "./contains/foo/any.txt", ], no: &[ "file.bar", "/baz/bar/file.bar", "./file.bar", "./baz/bar/file.bar", ], }, TestSet { globs: vec![String::from("/foo/**/*")], yes: &["/foo/file.go", "/foo/bar/baz/file.go"], no: &["/bar/file.go"], }, ]; for t in tests { let m = Matcher::new(&t.globs)?; for y in t.yes { assert!(m.path_matches(&PathBuf::from(y)), "{} matches", y); } for n in t.no { assert!(!m.path_matches(&PathBuf::from(n)), "{} matches", n); } } Ok(()) } } precious-0.1.3/src/precious.rs000066400000000000000000001024361420422002100163230ustar00rootroot00000000000000use crate::basepaths; use crate::chars; use crate::config; use crate::filter; use crate::vcs; use anyhow::{Error, Result}; use clap::{App, Arg, ArgGroup, ArgMatches}; use fern::colors::{Color, ColoredLevelConfig}; use fern::Dispatch; use log::{debug, error, info}; use rayon::{prelude::*, ThreadPool, ThreadPoolBuilder}; use std::collections::HashMap; use std::env; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use thiserror::Error; #[derive(Debug, Error)] enum PreciousError { #[error("No subcommand (lint or tidy) was given in the command line args")] NoSubcommandInCliArgs, #[error("No mode or paths were provided in the command line args")] NoModeOrPathsInCliArgs, #[error(r#"Could not parse {arg:} argument, "{val:}", as an integer"#)] InvalidIntegerArgument { arg: String, val: String }, #[error("Could not find a VCS checkout root starting from {cwd:}")] CannotFindRoot { cwd: String }, #[error("No {what:} filters defined in your config")] NoFilters { what: String }, } #[derive(Debug)] struct Exit { status: i8, message: Option, error: Option, } impl From for Exit { fn from(err: Error) -> Exit { Exit { status: 1, message: None, error: Some(err.to_string()), } } } #[derive(Debug)] struct ActionError { error: String, config_key: String, path: PathBuf, } #[derive(Debug)] pub struct Precious<'a> { matches: &'a ArgMatches, mode: basepaths::Mode, root: PathBuf, cwd: PathBuf, config: config::Config, chars: chars::Chars, quiet: bool, thread_pool: ThreadPool, } const CONFIG_FILE_NAMES: &[&str] = &["precious.toml", ".precious.toml"]; pub fn app<'a>() -> App<'a> { App::new("precious") .version(env!("CARGO_PKG_VERSION")) .author("Dave Rolsky ") .about("One code quality tool to rule them all") .arg( Arg::new("config") .short('c') .long("config") .takes_value(true) .help("Path to config file"), ) .arg( Arg::new("jobs") .short('j') .long("jobs") .takes_value(true) .help("Number of parallel jobs (threads) to run (defaults to one per core)"), ) .arg( Arg::new("ascii") .long("ascii") .help("Replace super-fun Unicode symbols with terribly boring ASCII"), ) .arg( Arg::new("verbose") .short('v') .long("verbose") .help("Enable verbose output"), ) .arg( Arg::new("debug") .short('d') .long("debug") .help("Enable debugging output"), ) .arg( Arg::new("trace") .short('t') .long("trace") .help("Enable tracing output (maximum logging)"), ) .arg( Arg::new("quiet") .short('q') .long("quiet") .help("Suppresses most output"), ) .group(ArgGroup::new("log-level").args(&["verbose", "debug", "trace", "quiet"])) .subcommand(common_subcommand( "tidy", "Tidies the specified files and/or directories", )) .subcommand(common_subcommand( "lint", "Lints the specified files and/or directories", )) } fn common_subcommand<'a>(name: &'a str, about: &'a str) -> App<'a> { App::new(name) .about(about) .arg( Arg::new("all") .short('a') .long("all") .help("Run against all files in the current directory and below"), ) .arg( Arg::new("git") .short('g') .long("git") .help("Run against files that have been modified according to git"), ) .arg( Arg::new("staged") .short('s') .long("staged") .help("Run against file content that is staged for a git commit"), ) .arg( Arg::new("paths") .multiple_occurrences(true) .takes_value(true) .help("A list of paths on which to operate"), ) .group( ArgGroup::new("operate-on") .args(&["all", "git", "staged", "paths"]) .required(true), ) } pub fn init_logger(matches: &ArgMatches) -> Result<(), log::SetLoggerError> { let line_colors = ColoredLevelConfig::new() .error(Color::Red) .warn(Color::Yellow) .info(Color::BrightBlack) .debug(Color::BrightBlack) .trace(Color::BrightBlack); let level = if matches.is_present("trace") { log::LevelFilter::Trace } else if matches.is_present("debug") { log::LevelFilter::Debug } else if matches.is_present("verbose") { log::LevelFilter::Info } else { log::LevelFilter::Warn }; let level_colors = line_colors.info(Color::Green).debug(Color::Black); Dispatch::new() .format(move |out, message, record| { out.finish(format_args!( "{color_line}[{target}][{level}{color_line}] {message}\x1B[0m", color_line = format_args!( "\x1B[{}m", line_colors.get_color(&record.level()).to_fg_str() ), target = record.target(), level = level_colors.color(record.level()), message = message, )); }) .level(level) .chain(std::io::stderr()) .apply() } impl<'a> Precious<'a> { pub fn new(matches: &'a ArgMatches) -> Result> { if log::log_enabled!(log::Level::Debug) { if let Some(path) = env::var_os("PATH") { debug!("PATH = {}", path.to_string_lossy()); } } let c = if matches.is_present("ascii") { chars::BORING_CHARS } else { chars::FUN_CHARS }; let cwd = env::current_dir()?; let root = Self::root(&cwd)?; let (config, _) = Self::config(matches, &root)?; Ok(Precious { matches, mode: Self::mode(matches)?, config, root, cwd, chars: c, quiet: matches.is_present("quiet"), thread_pool: ThreadPoolBuilder::new() .num_threads(Self::jobs(matches)?) .build()?, }) } fn mode(matches: &'a ArgMatches) -> Result { match matches.subcommand() { Some((_, subc_matches)) => { if subc_matches.is_present("all") { return Ok(basepaths::Mode::All); } else if subc_matches.is_present("git") { return Ok(basepaths::Mode::GitModified); } else if subc_matches.is_present("staged") { return Ok(basepaths::Mode::GitStaged); } if !subc_matches.is_present("paths") { return Err(PreciousError::NoModeOrPathsInCliArgs.into()); } Ok(basepaths::Mode::FromCli) } None => Err(PreciousError::NoSubcommandInCliArgs.into()), } } fn jobs(matches: &'a ArgMatches) -> Result { match matches.value_of("jobs") { Some(j) => match j.parse::() { Ok(u) => Ok(u), Err(_) => Err(PreciousError::InvalidIntegerArgument { arg: "--jobs".to_string(), val: j.to_string(), } .into()), }, None => Ok(0), } } fn root(cwd: &Path) -> Result { if Self::has_config_file(cwd) { return Ok(cwd.into()); } let mut root = PathBuf::new(); for anc in cwd.ancestors() { if Self::is_checkout_root(anc) { root.push(anc); return Ok(root); } } Err(PreciousError::CannotFindRoot { cwd: cwd.to_string_lossy().to_string(), } .into()) } fn config(matches: &'a ArgMatches, root: &Path) -> Result<(config::Config, PathBuf)> { let file = if matches.is_present("config") { let conf_file = matches.value_of("config").unwrap(); debug!("Loading config from {} (set via flag)", conf_file); PathBuf::from(conf_file) } else { let default = Self::default_config_file(root); debug!( "Loading config from {} (default location)", default.to_string_lossy() ); default }; Ok((config::Config::new(file.as_path())?, file)) } fn default_config_file(root: &Path) -> PathBuf { let root_path = root.to_path_buf(); // It'd be nicer to use the version of this provided by itertools, but // that requires itertools 0.10.1, and we want to keep the version at // 0.9.0 for the benefit of Debian. Self::find_or_first( CONFIG_FILE_NAMES.iter().map(|n| { let mut path = root_path.clone(); path.push(n); path }), |p| p.exists(), ) } fn find_or_first(mut iter: I, pred: P) -> PathBuf where I: Iterator, P: Fn(&Path) -> bool, { let first = iter.next().unwrap(); if pred(&first) { return first; } iter.find(|i| pred(i)).unwrap_or(first) } pub fn run(&mut self) -> i8 { match self.run_subcommand() { Ok(e) => { debug!("{:?}", e); if let Some(err) = e.error { print!("{}", err); } if let Some(msg) = e.message { println!("{} {}", self.chars.empty, msg); } e.status } Err(e) => { error!("Failed to run precious: {}", e); 1 } } } fn run_subcommand(&mut self) -> Result { if self.matches.subcommand_matches("tidy").is_some() { return self.tidy(); } else if self.matches.subcommand_matches("lint").is_some() { return self.lint(); } Ok(Exit { status: 1, message: None, error: Some(String::from( "You must run either the tidy or lint subcommand", )), }) } fn tidy(&mut self) -> Result { println!("{} Tidying {}", self.chars.ring, self.mode); let tidiers = self.config.tidy_filters(&self.root)?; self.run_all_filters("tidying", tidiers, |s, p, t| s.run_one_tidier(p, t)) } fn lint(&mut self) -> Result { println!("{} Linting {}", self.chars.ring, self.mode); let linters = self.config.lint_filters(&self.root)?; self.run_all_filters("linting", linters, |s, p, l| s.run_one_linter(p, l)) } fn run_all_filters( &mut self, action: &str, filters: Vec, run_filter: R, ) -> Result where R: Fn(&mut Self, Vec, &filter::Filter) -> Option>, { if filters.is_empty() { return Err(PreciousError::NoFilters { what: action.into(), } .into()); } let cli_paths = match self.mode { basepaths::Mode::FromCli => self.paths_from_args(), _ => vec![], }; match self.basepaths()?.paths(cli_paths)? { None => Ok(self.no_files_exit()), Some(paths) => { let mut all_errors: Vec = vec![]; for f in filters { if let Some(mut errors) = run_filter(self, paths.clone(), &f) { all_errors.append(&mut errors); } } Ok(self.make_exit(all_errors, action)) } } } fn make_exit(&self, errors: Vec, action: &str) -> Exit { let (status, error) = if errors.is_empty() { (0, None) } else { let red = format!("\x1B[{}m", Color::Red.to_fg_str()); let ansi_off = "\x1B[0m"; let plural = if errors.len() > 1 { 's' } else { '\0' }; let error = format!( "{}Error{} when {} files:{}\n{}", red, plural, action, ansi_off, errors .iter() .map(|ae| format!( " {} {} [{}]\n {}\n", self.chars.bullet, ae.path.to_string_lossy(), ae.config_key, ae.error, )) .collect::>() .join("") ); (1, Some(error)) }; Exit { status, message: None, error, } } fn run_one_tidier( &mut self, all_paths: Vec, t: &filter::Filter, ) -> Option> { let runner = |s: &Self, p: &Path, paths: &basepaths::Paths| -> Option> { match t.tidy(p, &paths.files) { Ok(Some(true)) => { if !s.quiet { println!( "{} Tidied by {}: {}", s.chars.tidied, t.name, p.to_string_lossy() ); } Some(Ok(())) } Ok(Some(false)) => { if !s.quiet { println!( "{} Unchanged by {}: {}", s.chars.unchanged, t.name, p.to_string_lossy() ); } Some(Ok(())) } Ok(None) => None, Err(e) => { println!( "{} error {}: {}", s.chars.execution_error, t.name, p.to_string_lossy() ); Some(Err(ActionError { error: format!("{:#}", e), config_key: t.config_key(), path: p.to_owned(), })) } } }; self.run_parallel("Tidying", all_paths, t, runner) } fn run_one_linter( &mut self, all_paths: Vec, l: &filter::Filter, ) -> Option> { let runner = |s: &Self, p: &Path, paths: &basepaths::Paths| -> Option> { match l.lint(p, &paths.files) { Ok(Some(r)) => { if r.ok { if !s.quiet { println!( "{} Passed {}: {}", s.chars.lint_free, l.name, p.to_string_lossy() ); } Some(Ok(())) } else { println!( "{} Failed {}: {}", s.chars.lint_dirty, l.name, p.to_string_lossy() ); if let Some(s) = r.stdout { println!("{}", s); } if let Some(s) = r.stderr { println!("{}", s); } Some(Err(ActionError { error: "linting failed".into(), config_key: l.config_key(), path: p.to_owned(), })) } } Ok(None) => None, Err(e) => { println!( "{} error {}: {}", s.chars.execution_error, l.name, p.to_string_lossy() ); Some(Err(ActionError { error: format!("{:#}", e), config_key: l.config_key(), path: p.to_owned(), })) } } }; self.run_parallel("Linting", all_paths, l, runner) } fn run_parallel( &mut self, what: &str, all_paths: Vec, f: &filter::Filter, runner: R, ) -> Option> where R: Fn(&Self, &Path, &basepaths::Paths) -> Option> + Sync, { let map = self.path_map(all_paths, f); let start = Instant::now(); let mut results: Vec> = vec![]; self.thread_pool.install(|| { results.append( &mut map .par_iter() .filter_map(|(p, paths)| runner(self, p, paths)) .collect::>>(), ); }); if !results.is_empty() { info!( "{} with {} on {} path{}, elapsed time = {}", what, f.name, results.len(), if results.len() > 1 { "s" } else { "" }, format_duration(&start.elapsed()) ); } let errors = results .into_iter() .filter_map(|r| match r { Ok(_) => None, Err(e) => Some(e), }) .collect::>(); if errors.is_empty() { None } else { Some(errors) } } fn no_files_exit(&self) -> Exit { Exit { status: 0, message: Some(String::from("No files found")), error: None, } } fn path_map( &mut self, all_paths: Vec, f: &filter::Filter, ) -> HashMap { if f.run_mode_is(filter::RunMode::Root) { return self.root_as_paths(all_paths); } else if f.run_mode_is(filter::RunMode::Dirs) { return self.dirs(all_paths); } self.files(all_paths) } fn root_as_paths( &mut self, mut all_paths: Vec, ) -> HashMap { let mut root_map = HashMap::new(); let mut all: Vec = vec![]; for p in all_paths.iter_mut() { all.append(&mut p.files); } let root_paths = basepaths::Paths { dir: PathBuf::from("."), files: all, }; root_map.insert(PathBuf::from("."), root_paths); root_map } fn dirs(&mut self, all_paths: Vec) -> HashMap { let mut map = HashMap::new(); for p in all_paths { map.insert(p.dir.clone(), p); } map } fn files(&mut self, all_paths: Vec) -> HashMap { let mut map = HashMap::new(); for p in all_paths { for f in p.files.iter() { map.insert(f.clone(), p.clone()); } } map } fn basepaths(&mut self) -> Result { basepaths::BasePaths::new(self.mode, self.cwd.clone(), self.config.exclude.clone()) } fn paths_from_args(&self) -> Vec { let subc_matches = self.matched_subcommand(); subc_matches .values_of("paths") .unwrap() .map(PathBuf::from) .collect::>() } fn matched_subcommand(&self) -> &ArgMatches { match self.matches.subcommand() { Some(("tidy", m)) => m, Some(("lint", m)) => m, _ => panic!("Somehow none of our subcommands matched and clap did not return an error"), } } fn is_checkout_root(path: &Path) -> bool { for dir in vcs::dirs() { let mut poss = PathBuf::from(path); poss.push(dir); if poss.exists() { return true; } } false } fn has_config_file(path: &Path) -> bool { Self::default_config_file(path).exists() } } // I tried the humantime crate but it doesn't do what I want. It formats each // element separately ("1s 243ms 179us 984ns"), which is _way_ more detail // than I want for this. This algorithm will format to the most appropriate of: // // Xm Y.YYs // X.XXs // X.XXms // X.XXus // X.XXns fn format_duration(d: &Duration) -> String { let s = (d.as_secs_f64() * 100.0).round() / 100.0; if s >= 60.0 { let minutes = (s / 60.0).floor() as u64; let secs = s - (minutes as f64 * 60.0); return format!("{}m {:.2}s", minutes, secs); } else if s >= 0.01 { return format!("{:.2}s", s); } let n = d.as_nanos(); if n > 1_000_000 { return format!("{:.2}ms", n as f64 / 1_000_000.0); } else if n > 1_000 { return format!("{:.2}us", n as f64 / 1_000.0); } format!("{}ns", n) } #[cfg(test)] mod tests { use super::*; use crate::testhelper; use itertools::Itertools; use pretty_assertions::assert_eq; // Anything that does pushd must be run serially or else chaos ensues. use serial_test::serial; use std::path::PathBuf; #[cfg(not(target_os = "windows"))] use std::str::FromStr; #[cfg(not(target_os = "windows"))] use which::which; const SIMPLE_CONFIG: &str = r#" [commands.rustfmt] type = "both" include = "**/*.rs" cmd = ["rustfmt"] lint_flags = "--check" ok_exit_codes = [0] lint_failure_exit_codes = [1] "#; const DEFAULT_CONFIG_FILE_NAME: &str = super::CONFIG_FILE_NAMES[0]; #[test] #[serial] fn new() -> Result<()> { for name in super::CONFIG_FILE_NAMES { let helper = testhelper::TestHelper::new()?.with_config_file(name, SIMPLE_CONFIG)?; let _pushd = helper.pushd_to_root()?; let app = app(); let matches = app.try_get_matches_from(&["precious", "tidy", "--all"])?; let p = Precious::new(&matches)?; assert_eq!(p.chars, chars::FUN_CHARS); assert!(!p.quiet); let (_, config_file) = Precious::config(&matches, &p.root)?; let mut expect_config_file = p.root; expect_config_file.push(name); assert_eq!(config_file, expect_config_file); } Ok(()) } #[test] #[serial] fn new_with_ascii_flag() -> Result<()> { let helper = testhelper::TestHelper::new()? .with_config_file(DEFAULT_CONFIG_FILE_NAME, SIMPLE_CONFIG)?; let _pushd = helper.pushd_to_root()?; let app = app(); let matches = app.try_get_matches_from(&["precious", "--ascii", "tidy", "--all"])?; let p = Precious::new(&matches)?; assert_eq!(p.chars, chars::BORING_CHARS); Ok(()) } #[test] #[serial] fn new_with_config_path() -> Result<()> { let helper = testhelper::TestHelper::new()? .with_config_file(DEFAULT_CONFIG_FILE_NAME, SIMPLE_CONFIG)?; let _pushd = helper.pushd_to_root()?; let app = app(); let matches = app.try_get_matches_from(&[ "precious", "--config", helper .config_file(DEFAULT_CONFIG_FILE_NAME) .to_str() .unwrap(), "tidy", "--all", ])?; let p = Precious::new(&matches)?; let (_, config_file) = Precious::config(&matches, &p.root)?; let mut expect_config_file = p.root; expect_config_file.push(DEFAULT_CONFIG_FILE_NAME); assert_eq!(config_file, expect_config_file); Ok(()) } #[test] #[serial] fn set_root_prefers_config_file() -> Result<()> { let helper = testhelper::TestHelper::new()?.with_git_repo()?; let mut src_dir = helper.root(); src_dir.push("src"); let mut subdir_config = src_dir.clone(); subdir_config.push(DEFAULT_CONFIG_FILE_NAME); helper.write_file(&subdir_config, SIMPLE_CONFIG)?; let _pushd = testhelper::Pushd::new(src_dir.clone())?; let app = app(); let matches = app.try_get_matches_from(&["precious", "--quiet", "tidy", "--all"])?; let p = Precious::new(&matches)?; assert_eq!(p.root, src_dir); Ok(()) } #[test] #[serial] fn basepaths_uses_cwd() -> Result<()> { let helper = testhelper::TestHelper::new()? .with_config_file(DEFAULT_CONFIG_FILE_NAME, SIMPLE_CONFIG)? .with_git_repo()?; let mut src_dir = helper.root(); src_dir.push("src"); let _pushd = testhelper::Pushd::new(src_dir)?; let app = app(); let matches = app.try_get_matches_from(&["precious", "--quiet", "tidy", "--all"])?; let mut p = Precious::new(&matches)?; let mut paths = p.basepaths()?; let expect = vec![basepaths::Paths { dir: PathBuf::from("."), files: ["bar.rs", "can_ignore.rs", "main.rs", "module.rs"] .iter() .map(PathBuf::from) .collect(), }]; assert_eq!(paths.paths(vec![])?, Some(expect)); Ok(()) } #[test] #[serial] fn tidy_succeeds() -> Result<()> { let config = r#" [commands.true] type = "tidy" include = "**/*" cmd = ["true"] ok_exit_codes = [0] "#; let helper = testhelper::TestHelper::new()?.with_config_file(DEFAULT_CONFIG_FILE_NAME, config)?; let _pushd = helper.pushd_to_root()?; let app = app(); let matches = app.try_get_matches_from(&["precious", "--quiet", "tidy", "--all"])?; let mut p = Precious::new(&matches)?; let status = p.run(); assert_eq!(status, 0); Ok(()) } #[test] #[serial] fn tidy_fails() -> Result<()> { let config = r#" [commands.false] type = "tidy" include = "**/*" cmd = ["false"] ok_exit_codes = [0] "#; let helper = testhelper::TestHelper::new()?.with_config_file(DEFAULT_CONFIG_FILE_NAME, config)?; let _pushd = helper.pushd_to_root()?; let app = app(); let matches = app.try_get_matches_from(&["precious", "--quiet", "tidy", "--all"])?; let mut p = Precious::new(&matches)?; let status = p.run(); assert_eq!(status, 1); Ok(()) } #[test] #[serial] fn lint_succeeds() -> Result<()> { let config = r#" [commands.true] type = "lint" include = "**/*" cmd = ["true"] ok_exit_codes = [0] lint_failure_exit_codes = [1] "#; let helper = testhelper::TestHelper::new()?.with_config_file(DEFAULT_CONFIG_FILE_NAME, config)?; let _pushd = helper.pushd_to_root()?; let app = app(); let matches = app.try_get_matches_from(&["precious", "--quiet", "lint", "--all"])?; let mut p = Precious::new(&matches)?; let status = p.run(); assert_eq!(status, 0); Ok(()) } #[test] #[serial] fn lint_fails() -> Result<()> { let config = r#" [commands.false] type = "lint" include = "**/*" cmd = ["false"] ok_exit_codes = [0] lint_failure_exit_codes = [1] "#; let helper = testhelper::TestHelper::new()?.with_config_file(DEFAULT_CONFIG_FILE_NAME, config)?; let _pushd = helper.pushd_to_root()?; let app = app(); let matches = app.try_get_matches_from(&["precious", "--quiet", "lint", "--all"])?; let mut p = Precious::new(&matches)?; let status = p.run(); assert_eq!(status, 1); Ok(()) } #[test] #[serial] // This fails in CI on Windows with a confusing error - "Cannot complete // in-place edit of test.replace: Work file is missing - did you change // directory?" I don't know what this means, and it's not really important // to run this test on every OS. #[cfg(not(target_os = "windows"))] fn command_order_is_preserved_when_running() -> Result<()> { if which("perl").is_err() { println!("Skipping test since perl is not in path"); return Ok(()); } let config = r#" [commands.perl-replace-a-with-b] type = "tidy" include = "test.replace" cmd = ["perl", "-pi", "-e", "s/a/b/i"] ok_exit_codes = [0] [commands.perl-replace-a-with-c] type = "tidy" include = "test.replace" cmd = ["perl", "-pi", "-e", "s/a/c/i"] ok_exit_codes = [0] lint_failure_exit_codes = [1] [commands.perl-replace-a-with-d] type = "tidy" include = "test.replace" cmd = ["perl", "-pi", "-e", "s/a/d/i"] ok_exit_codes = [0] lint_failure_exit_codes = [1] "#; let helper = testhelper::TestHelper::new()?.with_config_file(DEFAULT_CONFIG_FILE_NAME, config)?; let test_replace = PathBuf::from_str("test.replace")?; helper.write_file(test_replace.as_ref(), "The letter A")?; let _pushd = helper.pushd_to_root()?; let app = app(); let matches = app.try_get_matches_from(&["precious", "--quiet", "tidy", "-a"])?; let mut p = Precious::new(&matches)?; let status = p.run(); assert_eq!(status, 0); let content = helper.read_file(test_replace.as_ref())?; assert_eq!(content, "The letter b".to_string()); Ok(()) } #[test] fn format_duration_output() { let mut tests: HashMap = HashMap::new(); tests.insert(Duration::new(0, 24), "24ns"); tests.insert(Duration::new(0, 124), "124ns"); tests.insert(Duration::new(0, 1_243), "1.24us"); tests.insert(Duration::new(0, 12_443), "12.44us"); tests.insert(Duration::new(0, 124_439), "124.44us"); tests.insert(Duration::new(0, 1_244_392), "1.24ms"); tests.insert(Duration::new(0, 12_443_924), "0.01s"); tests.insert(Duration::new(0, 124_439_246), "0.12s"); tests.insert(Duration::new(1, 1), "1.00s"); tests.insert(Duration::new(1, 12), "1.00s"); tests.insert(Duration::new(1, 124), "1.00s"); tests.insert(Duration::new(1, 1_243), "1.00s"); tests.insert(Duration::new(1, 12_443), "1.00s"); tests.insert(Duration::new(1, 124_439), "1.00s"); tests.insert(Duration::new(1, 1_244_392), "1.00s"); tests.insert(Duration::new(1, 12_443_926), "1.01s"); tests.insert(Duration::new(1, 124_439_267), "1.12s"); tests.insert(Duration::new(59, 1), "59.00s"); tests.insert(Duration::new(59, 1_000_000), "59.00s"); tests.insert(Duration::new(59, 10_000_000), "59.01s"); tests.insert(Duration::new(59, 90_000_000), "59.09s"); tests.insert(Duration::new(59, 99_999_999), "59.10s"); tests.insert(Duration::new(59, 100_000_000), "59.10s"); tests.insert(Duration::new(59, 900_000_000), "59.90s"); tests.insert(Duration::new(59, 990_000_000), "59.99s"); tests.insert(Duration::new(59, 999_000_000), "1m 0.00s"); tests.insert(Duration::new(59, 999_999_999), "1m 0.00s"); tests.insert(Duration::new(60, 0), "1m 0.00s"); tests.insert(Duration::new(60, 10_000_000), "1m 0.01s"); tests.insert(Duration::new(60, 100_000_000), "1m 0.10s"); tests.insert(Duration::new(60, 110_000_000), "1m 0.11s"); tests.insert(Duration::new(60, 990_000_000), "1m 0.99s"); tests.insert(Duration::new(60, 999_000_000), "1m 1.00s"); tests.insert(Duration::new(61, 10_000_000), "1m 1.01s"); tests.insert(Duration::new(61, 100_000_000), "1m 1.10s"); tests.insert(Duration::new(61, 120_000_000), "1m 1.12s"); tests.insert(Duration::new(61, 990_000_000), "1m 1.99s"); tests.insert(Duration::new(61, 999_000_000), "1m 2.00s"); tests.insert(Duration::new(120, 99_000_000), "2m 0.10s"); tests.insert(Duration::new(120, 530_000_000), "2m 0.53s"); tests.insert(Duration::new(120, 990_000_000), "2m 0.99s"); tests.insert(Duration::new(152, 240_123_456), "2m 32.24s"); for k in tests.keys().sorted() { let f = format_duration(k); let e = tests.get(k).unwrap().to_string(); assert_eq!(f, e, "{}s {}ns", k.as_secs(), k.as_nanos()); } } } precious-0.1.3/src/testhelper.rs000066400000000000000000000166511420422002100166540ustar00rootroot00000000000000#[cfg(test)] use crate::command; use anyhow::{Context, Result}; use std::collections::HashMap; use std::env; use std::fs; use std::io::prelude::*; use std::path::{Path, PathBuf}; use tempfile::{tempdir, TempDir}; pub struct TestHelper { // While we never access this field we need to hold onto the tempdir or // else the directory it references will be deleted. _tempdir: TempDir, root: PathBuf, paths: Vec, root_gitignore_file: PathBuf, tests_data_gitignore_file: PathBuf, } impl TestHelper { const PATHS: &'static [&'static str] = &[ "README.md", "can_ignore.x", "src/can_ignore.rs", "src/bar.rs", "src/main.rs", "src/module.rs", "merge-conflict-file", "tests/data/foo.txt", "tests/data/bar.txt", "tests/data/generated.txt", ]; pub fn new() -> Result { let temp = tempdir()?; let root = maybe_canonicalize(temp.path())?; let helper = TestHelper { _tempdir: temp, root, paths: Self::PATHS.iter().map(PathBuf::from).collect(), root_gitignore_file: PathBuf::from(".gitignore"), tests_data_gitignore_file: PathBuf::from("tests/data/.gitignore"), }; Ok(helper) } pub fn with_git_repo(self) -> Result { self.create_git_repo()?; Ok(self) } pub fn with_config_file(self, file_name: &str, content: &str) -> Result { if cfg!(windows) { self.write_file(&self.config_file(file_name), &content.replace("\n", "\r\n"))?; } else { self.write_file(&self.config_file(file_name), content)?; } Ok(self) } pub fn pushd_to_root(&self) -> Result { Pushd::new(self.root.clone()) } fn create_git_repo(&self) -> Result<()> { for p in self.paths.iter() { self.write_file(p, "some content")?; } self.run_git(&["init", "--initial-branch", "master"])?; // If the tests are run in a totally clean environment they will blow // up if this isnt't set. This fixes // https://github.com/houseabsolute/precious/issues/15. self.run_git(&["config", "user.email", "precious@example.com"])?; // With this on I get line ending warnings from git on Windows if I // don't write out files with CRLF. Disabling this simplifies things // greatly. self.run_git(&["config", "core.autocrlf", "false"])?; self.stage_all()?; self.run_git(&["commit", "-m", "initial commit"])?; Ok(()) } pub fn root(&self) -> PathBuf { self.root.clone() } pub fn config_file(&self, file_name: &str) -> PathBuf { let mut path = self.root.clone(); path.push(file_name); path } pub fn all_files(&self) -> Vec { self.paths.to_vec() } pub fn stage_all(&self) -> Result<()> { self.run_git(&["add", "."]) } pub fn commit_all(&self) -> Result<()> { self.run_git(&["commit", "-a", "-m", "committed"]) } const ROOT_GITIGNORE: &'static str = " /**/bar.* can_ignore.* "; const TESTS_DATA_GITIGNORE: &'static str = " generated.* "; pub fn non_ignored_files() -> Vec { Self::PATHS .iter() .filter_map(|&p| { if p.contains("can_ignore") || p.contains("bar.") || p.contains("generated.txt") { None } else { Some(PathBuf::from(p)) } }) .collect() } pub fn switch_to_branch(&self, branch: &str, exists: bool) -> Result<()> { let mut args: Vec<&str> = vec!["checkout", "--quiet"]; if !exists { args.push("-b"); } args.push(branch); command::run_command( "git".to_string(), args.iter().map(|a| a.to_string()).collect(), &HashMap::new(), &[0], false, Some(&self.root()), )?; Ok(()) } pub fn merge_master(&self, expect_fail: bool) -> Result<()> { let mut expect_codes = [0].to_vec(); if expect_fail { expect_codes.push(1); } command::run_command( "git".to_string(), ["merge", "--quiet", "--no-ff", "--no-commit", "master"] .iter() .map(|a| a.to_string()) .collect(), &HashMap::new(), &expect_codes, true, Some(&self.root()), )?; Ok(()) } pub fn add_gitignore_files(&self) -> Result> { self.write_file(&self.root_gitignore_file, Self::ROOT_GITIGNORE)?; self.write_file(&self.tests_data_gitignore_file, Self::TESTS_DATA_GITIGNORE)?; Ok(vec![ self.root_gitignore_file.clone(), self.tests_data_gitignore_file.clone(), ]) } fn run_git(&self, args: &[&str]) -> Result<()> { command::run_command( "git".to_string(), args.iter().map(|a| a.to_string()).collect(), &HashMap::new(), &[0], false, Some(&self.root), )?; Ok(()) } const TO_MODIFY: &'static [&'static str] = &["src/module.rs", "tests/data/foo.txt"]; pub fn modify_files(&self) -> Result> { let mut paths: Vec = vec![]; for p in Self::TO_MODIFY.iter().map(PathBuf::from) { self.write_file(&p, "new content")?; paths.push(p.clone()); } Ok(paths) } pub fn write_file(&self, rel: &Path, content: &str) -> Result<()> { let mut full = self.root.clone(); full.push(rel); fs::create_dir_all(full.parent().unwrap()).with_context(|| { format!( "Creating dir at {}", full.parent().unwrap().to_string_lossy(), ) })?; let mut file = fs::File::create(full.clone()) .context(format!("Creating file at {}", full.to_string_lossy()))?; file.write_all(content.as_bytes()) .context(format!("Writing to file at {}", full.to_string_lossy()))?; Ok(()) } #[cfg(not(target_os = "windows"))] pub fn read_file(&self, rel: &Path) -> Result { let mut full = self.root.clone(); full.push(rel); let content = fs::read_to_string(full.clone()) .context(format!("Reading file at {}", full.to_string_lossy()))?; Ok(content) } } pub struct Pushd(PathBuf); impl Pushd { pub fn new(path: PathBuf) -> Result { let cwd = env::current_dir()?; env::set_current_dir(path)?; Ok(Pushd(cwd)) } } impl Drop for Pushd { fn drop(&mut self) { // If the original path was a tempdir it may be gone now. if !self.0.exists() { return; } let res = env::set_current_dir(&self.0); if let Err(e) = res { panic!( "Could not return to original dir, {}: {}", self.0.to_string_lossy(), e, ); } } } // The temp directory on macOS in GitHub Actions appears to be a symlink, but // canonicalizing on Windows breaks tests for some reason. pub fn maybe_canonicalize(path: &Path) -> Result { if cfg!(windows) { return Ok(path.to_owned()); } Ok(fs::canonicalize(path)?) } precious-0.1.3/src/vcs.rs000066400000000000000000000002031420422002100152520ustar00rootroot00000000000000pub fn dirs() -> Vec { [".git", ".hg", ".svn"] .iter() .map(|&s| String::from(s)) .collect() }