bounteous-inc / composer-darn
A Composer plugin to easily apply patches to your project.
Package info
github.com/Bounteous-Inc/composer-darn
Type:composer-plugin
pkg:composer/bounteous-inc/composer-darn
Requires
- php: >=8.2
- composer-plugin-api: ^2.0
- guzzlehttp/guzzle: ^7.0
Requires (Dev)
- composer/composer: ^2.0
- cweagans/composer-patches: ^2.0
- ergebnis/composer-normalize: ^2.49
- laravel/pint: ^1.27
- pestphp/pest: ^3.8
- php-parallel-lint/php-parallel-lint: ^1.4
- phpro/grumphp: ^2.19
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- yieldstudio/grumphp-laravel-pint: ^1.0
Suggests
- cweagans/composer-patches: Required to apply the downloaded patches.
README
A Composer plugin for fetching, registering, and managing patches in your project. Supports Drupal.org issues, GitHub pull requests, and direct .patch/.diff URLs or local files. Fully compatible with cweagans/composer-patches.
Requirements
- PHP >= 8.2
- Composer >= 2.0
Installation
composer require --dev bounteous-inc/composer-darn
To actually apply patches during composer install / composer update, you also need:
composer require cweagans/composer-patches
composer darn will offer to install it for you when needed if it is missing.
Installing into a project with existing patches? If your
composer.jsonalready has entries underextra.patcheswhenbounteous-inc/composer-darnis first installed, it will prompt you to rundarn:fixautomatically. This normalises your existing entries to the standard format with self-documenting descriptions,issue-tracker-urlfields, and locally-stored patch files.
Quick Start
The darn command is a smart dispatcher — give it any supported URL or file path and it routes to the right sub-command automatically:
# From a Drupal.org issue composer darn https://www.drupal.org/project/drupal/issues/3151000 # From a GitHub pull request composer darn https://github.com/owner/repo/pull/123 # From a direct patch URL composer darn https://example.com/my-fix.patch --package=vendor/package # From a local file composer darn ./my-fix.patch --package=vendor/package
All commands support --apply to immediately apply the patch with cweagans/composer-patches after registering the patch.
Commands
darn — Smart Dispatcher
Detects the source type and delegates to the appropriate sub-command.
composer darn <source> [options]
| Argument / Option | Description |
|---|---|
source |
Drupal.org issue URL, GitHub PR URL, direct .patch/.diff URL, or local file path |
--package |
Package name — required for direct URLs/files when auto-detection is unavailable |
--dir |
Override patches directory (default: patches) |
--apply, -a |
Apply the patch via cweagans/composer-patches after registering |
--depth, -p |
Strip-prefix depth passed to git apply -p<n> |
--ticket |
Internal ticket reference (e.g. JIRA-123) stored in composer.json |
--description |
Patch description — skips the interactive prompt |
darn:drupal.org — Drupal.org Issues
Download and register a patch from a Drupal.org issue.
composer darn:drupal.org [<issue_id>] [options]
Without an issue_id, the command prompts for one interactively.
What it does:
- Queries the Drupal.org REST API for the issue.
- Resolves the Composer package name from the project machine name.
- Lists all available patches: GitLab Merge Request diffs and
.patchfile attachments (newest first). - Prompts you to select a patch.
- Downloads the patch to
patches/<package>/. - Cleans up previously downloaded patches for the same issue if found.
- Registers the patch in
composer.jsonunderextra.patches.
Filename format:
- Merge Request:
{issueId}-mr-{iid}-{sha8}.patch - File attachment:
{issueId}-{commentIndex}-{filename}
Example:
composer darn:drupal.org 3151000 --apply
darn:github — GitHub Pull Requests
Download and register a patch from a GitHub pull request.
composer darn:github <url> [<package>] [options]
For PR URLs (github.com/owner/repo/pull/N), the package name is auto-detected from the repository's composer.json. Provide <package> explicitly if detection fails.
What it does:
- Auto-detects the package name and generates a self-documenting filename (
{owner}-{repo}-pr-{number}.patch). - Downloads the patch to
patches/<package>/. - Fetches the PR title from the GitHub API and uses it as the default description (
PR #N: {title} (owner/repo)). - Registers the patch in
composer.json.
Authentication:
Set GITHUB_TOKEN to avoid rate limits and access private repositories:
export GITHUB_TOKEN=ghp_yourtoken
composer darn:github https://github.com/owner/repo/pull/123
Example:
composer darn:github https://github.com/drupal/drupal/pull/456 --apply
darn:patch — Direct URL or Local File
Register a patch from any direct URL or local .patch/.diff file.
composer darn:patch <source> --package=<vendor/name> [options]
--package is required because there is no API to auto-detect it.
What it does:
- For URLs: downloads the file to
patches/<package>/. - For local files: copies the file to the patches directory (unless it is already there).
- Prompts for a description (defaults to the filename without extension).
- Registers the patch in
composer.json.
Example:
composer darn:patch https://example.com/fix.patch --package=drupal/core --description="Fix broken layout"
darn:fix — Normalize Existing Patch Entries
Re-fetch metadata for all remote patches in composer.json and rewrite their entries to the standard format: self-documenting description, issue-tracker-url, and a locally-stored patch file.
composer darn:fix [--dry-run] [--dir=patches]
What it does:
- Scans every entry under
extra.patches. - For each Drupal.org URL: fetches the issue and file metadata, generates the canonical description (
Issue #N: {title} ({file})), downloads the patch topatches/<package>/, and updates the entry. - For each GitHub PR URL: fetches the PR title, downloads the
.diff, and updates the entry. - Skips local files, GitLab URLs, and any unrecognized remote URLs.
| Option | Description |
|---|---|
--dry-run |
Show what would change without downloading or writing anything |
--dir |
Override the patches directory (default: patches) |
Example:
# Preview changes first composer darn:fix --dry-run # Apply normalization composer darn:fix
darn:list — List Registered Patches
List all patches in composer.json, grouped by package.
composer darn:list [<package>] [--dir=patches]
Each entry shows whether the patch file exists on disk:
drupal/core
[✓] Issue #3151000: Fix render pipeline (patches/drupal-core/3151000-1-fix.patch)
[✗] Issue #3200000: Missing layout (patches/drupal-core/3200000-1-layout.patch)
2 patch(es) registered for 1 package(s). 1 missing.
Always exits 0 — use darn:verify for CI checks.
darn:verify — Verify Patches Exist
Verify that every patch file referenced in composer.json exists on disk.
composer darn:verify [--prune] [--dir=patches]
Also detects:
- Duplicate patch URLs registered under different entries
- Patches directory listed in
.gitignore(patches should be committed)
Exit codes: 0 = all present, 1 = any missing or malformed.
With --prune: delegates to darn:prune for interactive cleanup.
CI/CD example:
# GitHub Actions - name: Verify patches run: composer darn:verify
darn:remove — Remove a Patch Entry
Remove one or more patches from composer.json, optionally deleting the files from disk.
composer darn:remove [<package>] [<description>] [--delete] [--dir=patches]
Modes:
- Both arguments supplied: removes the specified entry non-interactively (safe for scripts).
- Package only: shows a numbered list of patches for that package; prompts for selection.
- No arguments: first selects a package interactively, then shows the patch list.
Use --delete to also remove the patch file(s) from disk without an extra prompt.
Example:
# Non-interactive removal (e.g. in a script) composer darn:remove drupal/core "Issue #3151000: Fix render pipeline" --delete
darn:prune — Remove Orphaned Files and Stale Entries
Detect and clean up two categories of staleness:
composer darn:prune [<package>] [--clean-config] [--dir=patches]
| Category | Description | Cleaned up by |
|---|---|---|
| Orphaned files | .patch/.diff files on disk with no matching composer.json entry |
Always offered (prompts to delete) |
| Missing entries | composer.json entries whose file no longer exists |
Requires --clean-config |
Scope to a single package with the optional package argument.
Configuration
Patches Directory
Override the default patches/ directory globally via composer.json:
{
"extra": {
"composer-darn": {
"patches-dir": "custom/patches"
}
}
}
Or per-command with --dir:
composer darn:list --dir=custom/patches
GitHub Token
Set GITHUB_TOKEN to authenticate with the GitHub API:
export GITHUB_TOKEN=ghp_yourtoken
This avoids rate limits (60 req/hr unauthenticated vs. 5,000 req/hr authenticated) and is required for private repositories. In GitHub Actions, GITHUB_TOKEN is provided automatically:
- name: Register patch run: composer darn https://github.com/owner/repo/pull/123 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Contributing
See CONTRIBUTING.md for setup, development workflow, commit conventions, and the release process.
License
MIT — see LICENSE.