Open .gitattributes export-ignore PRs at scale against PHP/Composer packages
that still ship dev files (tests/, phpunit.xml, .github/, …) in their
composer dist.
Designed to be polite, evidence-citing, and safe: each PR body references the
last .gitattributes change in the repo plus the commit/PR that introduced
every file we want excluded. Pre-flight checks skip repos where the maintainer
has already closed similar PRs, and drop entries whose paths no longer exist
upstream.
Many PHP libraries ship tests/, IDE configs and CI workflows in the composer
dist tarball, bloating every server's vendor/ directory with files that are
only useful for development. Adding export-ignore rules in .gitattributes
is the supported fix; this tool batches the PR-opening across many libraries.
- Rejection-history check: skip the repo if a non-author closed a similar
PR in the last 5 years (catches maintainers who said no before).
buildruns it too, so refusing repos are warned about on stderr and land in the emittedskipped:list instead oftargets:; move one back undertargets:by hand to override. - Upstream existence check: drop entries whose paths no longer exist in
upstream HEAD (catches stale-rename false positives like
phpunit.xml->phpunit.xml.dist). - Style detection: preserve the existing
.gitattributesstyle:- Alpha-sort if the existing file is sorted.
- Pad with spaces if
export-ignoreis aligned in a fixed column.
- CLA-org awareness: for
GoogleCloudPlatform/*,googleapis/*,google/*, replaceCo-Authored-Bytrailer with a plain-text attribution so the Google CLA bot doesn't block on the Anthropic co-author email. - HTTPS auth via
gh auth git-credential: no SSH key / yubikey taps mid campaign. - Idempotent: skip repos where a PR already exists on the same head branch.
- Skip list with reasons: track repos that maintainers won't accept.
- Broad dev-file detection: ~110 default candidates covering test runners,
static analysis, docs generators (
phpdoc.dist.xml,.phpdoc/), toolchain pinning (.tool-versions,.phive/,phive.xml,.mise.toml), CI services and dev-environment files. Files consumers legitimately read out ofvendor/(CHANGELOG.md,UPGRADE.md) are deliberately never proposed.
Requires Rust 1.75+ and the gh CLI (logged in via gh auth login).
cargo install --path .This produces a single vendor-cleanup binary on your $PATH.
gh auth login # if not already
# 1. Build a campaign YAML from a project's vendor/ folder.
# Scans every package, resolves the upstream repo via composer.json,
# drops anything already excluded by upstream's .gitattributes,
# drops paths that no longer exist upstream, and cites the last
# commit ref + date that touched each surviving entry.
vendor-cleanup build \
--vendor /path/to/your/project/vendor \
--user-login your-gh-handle \
--fork-dir /path/to/your/forks \
--output campaign.yaml
# Or build from an explicit repo list (uses the default candidate set).
vendor-cleanup build \
--repo schmittjoh/serializer \
--repo phar-io/manifest \
--user-login your-gh-handle \
--output campaign.yaml
# 2. Dry-run the campaign to review every diff.
vendor-cleanup run --config campaign.yaml
# 3. Fire when ready (random 10-20s sleep between PRs).
vendor-cleanup run --config campaign.yaml --go
vendor-cleanup run --config campaign.yaml --go --limit 2 # stop after 2
# Enrich a registry YAML with per-PR upstream byte sizes.
vendor-cleanup enrich-savings registry/williamdes-cleanup-prs.yamlDolibarr ships dependencies under
htdocs/includes/ with an irregular layout (not standard composer
install). For non-standard layouts, pass explicit --repo flags. After
manually identifying which embedded packages still ship dev files in
their upstream dist:
vendor-cleanup build \
--repo fruux/sabre-event \
--repo fruux/sabre-http \
--repo globalcitizen/php-iban \
--repo mike42/escpos-php \
--repo phayes/geoPHP \
--repo php-fig/simple-cache \
--repo serbanghita/Mobile-Detect \
--repo webklex/php-imap \
--user-login your-gh-handle \
--fork-dir /mnt/Dev/forks \
--output dolibarr-campaign.yaml
vendor-cleanup run --config dolibarr-campaign.yaml # dry-run
vendor-cleanup run --config dolibarr-campaign.yaml --go # fireIf an upstream's .gitattributes still has .travis.yml export-ignore
but the file no longer exists in upstream HEAD, build detects the
stale entry, proposes removing it, and (when .github/ exists upstream
and isn't already excluded) proposes adding /.github/ export-ignore
alongside. The resulting PR body is structured into two sections:
"Stale entries to remove" and "Entries to add", so reviewers see the
intent at a glance.
YAML file. See examples/ for a real sample.
defaults:
fork_dir: /home/me/forks
user_login: my-gh-handle
branch: gh-handle/gitattributes-export-ignore
pr_title: "Update .gitattributes to exclude dev files from composer dist"
targets:
- repo: schmittjoh/serializer
branch: master
create: false
last_gitattributes_ref: "cd24e3c (2023-01-06)"
entries:
- line: "/doc/ export-ignore"
ref: "e5baafe 2025-07-17 (#1604)"
- line: "/phpstan.neon.dist export-ignore"
ref: "3d937ad 2024-11-25"
- line: "/CONTRIBUTING.md export-ignore"
ref: "f674fba 2026-03-26"
- repo: shuchkin/simplexlsx
branch: master
create: true # no existing .gitattributes upstream
entries:
- line: "/examples/ export-ignore"
ref: "ce6559b 2022-02-21"
skipped:
- repo: maennchen/ZipStream-PHP
reason: |
Owner has repeatedly closed export-ignore PRs (#206, #285, #339, #422).
They want GitHub Source Download to include tests. Do not re-propose.This avoids shipping dev/tooling files in the composer dist that aren't
needed at runtime.
Last `.gitattributes` update was 2aa81ac (2022-10-19, #601). The files below
have been added or modified since, and are still included in the composer dist:
- `/phpstan.neon export-ignore` - last touched in 1e01c7b 2023-08-03 (#740)
- `/phpstan-baseline.neon export-ignore` - last touched in 1d1a873 2026-03-12 (#1103)
- `/.php-cs-fixer.php export-ignore` - last touched in 5a3f81b 2024-11-24 (#954)
- `/CONTRIBUTING.md export-ignore` - last touched in fbcd9bd 2024-01-23 (#839)
Background reading: https://blog.madewithlove.be/post/gitattributes/
A companion scanner that reads a project's composer.lock + vendor/
and emits a draft YAML config with last-touch commit refs per entry is
on the roadmap.
cargo build
cargo test # 77 tests (unit + integration)
cargo clippy --all-targets -- -D warningsMPL-2.0. See LICENSE.