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.
- PHP >= 8.2
- Composer >= 2.0
composer require --dev bounteous-inc/composer-darnTo actually apply patches during composer install / composer update, you also need:
composer require cweagans/composer-patchescomposer 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.
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/packageAll commands support --apply to immediately apply the patch with cweagans/composer-patches after registering the patch.
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 |
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 --applyDownload 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/123Example:
composer darn:github https://github.com/drupal/drupal/pull/456 --applyRegister 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"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:fixList 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.
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:verifyRemove 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" --deleteDetect 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.
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/patchesSet GITHUB_TOKEN to authenticate with the GitHub API:
export GITHUB_TOKEN=ghp_yourtokenThis 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 }}See CONTRIBUTING.md for setup, development workflow, commit conventions, and the release process.
MIT — see LICENSE.