rgarciar1931/vendor-patches

Framework-agnostic CLI and Composer plugin to apply, revert and track the status of git patches against vendor/ dependencies in any PHP project.

Maintainers

Package info

github.com/rgarciar1931/vendor-patches

Type:composer-plugin

pkg:composer/rgarciar1931/vendor-patches

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v0.9.0 2026-07-17 08:47 UTC

This package is auto-updated.

Last update: 2026-07-17 11:31:35 UTC


README

A framework-agnostic Composer CLI and plugin to apply, revert, and track the status of git apply-based patches against vendor/ (or any other) files in a PHP project — including dependency ordering and a transactional apply command.

Why

Third-party dependencies sometimes need a local hotfix before an upstream release is available. Keeping those hotfixes as plain .patch files, applied by hand, doesn't scale: there's no record of what's applied, no ordering between patches that depend on each other, and no safe way to apply "all of them" without risking a half-patched tree if one fails partway through. vendor-patches formalizes that workflow with a small JSON manifest and three CLI commands.

Installation

composer require rgarciar1931/vendor-patches

This installs the vendor-patches binary at vendor/bin/vendor-patches and registers a Composer plugin that applies any pending patches automatically after composer install/update (see Auto-apply configuration to disable or tune this).

Quick start

  1. Create a manifest at patches/patches.json in your project root:

    {
      "patches": [
        {
          "id": "curl-100-continue-status",
          "name": "Fix cURL 100-continue real HTTP status",
          "description": "Corrects HTTP status parsing when a server replies with an interim 100-continue header.",
          "type": "fix",
          "sortOrder": 10,
          "dependencies": [],
          "file": "curl-100-continue-real-status.patch",
          "target": ".",
          "strip": 1
        }
      ]
    }
  2. Put the corresponding .patch file next to it (patches/curl-100-continue-real-status.patch), generated the normal way with git diff (or git diff --no-index) so it has standard a//b/ path prefixes.

  3. Check status, apply, and revert:

    vendor/bin/vendor-patches patch:status
    vendor/bin/vendor-patches patch:apply --all
    vendor/bin/vendor-patches patch:revert curl-100-continue-status

Commands

patch:apply [ids...] [--all] [--no-cascade] [--yes] [--manifest=] [--project-root=]

Applies one, several, or (--all) every patch, in dependency order.

  • If a requested patch depends on one that isn't applied yet, it's automatically pulled into the plan and applied first. Pass --no-cascade to instead fail fast, naming the missing dependency.
  • Transactional: if any patch in the plan fails (a real git apply failure, or a detected conflict), everything applied earlier in that same run is immediately reverted, in reverse order, before the command exits non-zero. A failed run never leaves the tree partially patched.
  • Prompts for confirmation with the full expanded plan before touching anything, unless --yes is passed or the session isn't interactive.

patch:revert [ids...] [--all] [--no-cascade] [--yes] [--manifest=] [--project-root=]

Reverts one, several, or (--all) every applied patch.

  • If a requested patch has applied dependents, they're automatically reverted first (cascade). Pass --no-cascade to instead fail fast, naming the blocking dependent.
  • Unlike patch:apply, a failure here is reported per-patch rather than triggering a rollback (reverting is already the "undo" operation) — but any patch that transitively depends on a failed one is skipped rather than reverted, to avoid leaving a dependent applied on top of a reverted prerequisite.

patch:status [--type=] [--manifest=] [--project-root=]

Shows every patch's live status (applied, not_applied, or conflict), computed fresh each run via git apply --check / --check --reverse — nothing is cached or persisted, so status is always accurate even if a patch was applied or reverted outside this tool. --type filters by the patch's type field.

Manifest schema

Default location: patches/patches.json in the project root (configurable, see below). See resources/manifest.schema.json for the full JSON Schema.

Field Type Required Default Meaning
id string yes Unique id, ^[a-z0-9][a-z0-9._-]*$. Used on the CLI and as the dependency-graph key.
name string yes Short human title.
description string no "" Longer free-text explanation.
type string no "fix" Free-form category (fix, security, performance, compatibility, ...).
sortOrder integer no 0 Tiebreaker within the dependency-respecting order.
dependencies string[] no [] Ids of other patches that must be applied first.
file string yes Path to the .patch file, relative to the manifest's own directory.
target string no "." Base directory (relative to the project root) used as the cwd for git apply.
strip integer no 1 The -p<N> level passed to git apply.

Auto-apply configuration

The Composer plugin runs patch:apply --all non-interactively after composer install/update. Configure it via environment variables (highest precedence) or extra.vendor-patches in your project's composer.json:

{
  "extra": {
    "vendor-patches": {
      "auto-apply": true,
      "manifest": "patches/patches.json",
      "strict": false
    }
  }
}
Setting Env var extra.vendor-patches key Default Meaning
Auto-apply VENDOR_PATCHES_AUTO_APPLY auto-apply true Whether the plugin runs at all.
Manifest path VENDOR_PATCHES_MANIFEST manifest patches/patches.json Manifest location, relative to the project root (or absolute).
Strict VENDOR_PATCHES_STRICT strict false Whether a failed auto-apply aborts composer install/update (non-zero exit) or just warns and continues.

Env vars accept 1/true/on/yes or 0/false/off/no (case-insensitive). Manual CLI usage always works regardless of these settings — they only gate the automatic post-install/update hook.

If no manifest file exists at the resolved path, the plugin silently does nothing.

Requirements and known limitations

  • Patches must be standard git diff/git format-patch-style unified diffs with a//b/ path prefixes — raw diff -u output without those prefixes won't apply even if content-equivalent.
  • target must be inside a git working tree; there's no fallback (e.g. a plain patch CLI driver) for non-git directories in this version.
  • Only tested on Linux/macOS; Windows should work via Symfony Process but isn't CI-verified yet.
  • Manifest and patch files are only ever read locally — this tool never fetches patch content from a remote source.

Development

composer install
composer test        # PHPUnit (unit + integration)
composer phpstan      # Static analysis (level 6)
composer cs-check     # PHP CS Fixer, dry-run

License

MIT, see LICENSE.