patchwork 0.4.0
patchwork: ^0.4.0 copied to clipboard
Patch management for Dart projects, starting with reviewable dependency patches for pub packages.
Patchwork #
Patchwork keeps Dart pub dependency fixes in reviewable patch files without
editing your shared .pub-cache.
Use it when you need a local dependency fix that should survive fresh
checkouts, code review, and CI. Patchwork copies a resolved dependency into
.patchwork/<pkg>@<version>/, commits the edit as
patches/<pkg>@<version>.patch, and materializes the patch through generated
pub path overrides.
Why Patchwork #
Manual .pub-cache edits are fast, but they are local to one machine and easy
to lose. Patchwork keeps the durable part of the change in your project so the
same patch can be reviewed, committed, and applied by teammates or CI.
Patchwork is a good fit for small dependency fixes while you wait for an upstream release. Prefer a fork or vendored dependency when the change is large, long-lived, security-sensitive, or needs its own release process.
Install #
Add Patchwork as a dev dependency in the project that owns the patch files.
dart pub add dev:patchwork
For local repository development:
dev_dependencies:
patchwork:
path: ../path/to/patchwork/pub/patchwork
Run commands with dart run:
dart run patchwork --help
Global activation is optional for users who prefer a standalone executable.
Workflow #
Run Patchwork from the Dart package or workspace you want to patch.
dart pub get
dart run patchwork doctor
dart run patchwork patch collection
Edit the directory printed by patchwork patch, then commit it:
dart run patchwork commit collection
Apply committed patches:
dart run patchwork apply
dart pub get
dart run patchwork status
After a successful apply, Patchwork prints the dart pub get next step so pub
refreshes dependency resolution through the generated overrides.
Automatic Apply Hooks #
User projects can opt in to automatic apply with Dart build hooks. Add both Patchwork and the hooks API as dev dependencies:
dart pub add dev:patchwork dev:hooks
Then create hook/build.dart in the application or workspace member that owns
the patch workflow:
import 'package:hooks/hooks.dart';
import 'package:patchwork/hooks.dart' as patchwork;
Future<void> main(List<String> args) async {
await build(args, (input, output) async {
await patchwork.applyAll(input, output);
});
}
The first dart run, dart test, or build after a patch is committed applies
the generated output and runs dart pub get when pub resolution needs to be
refreshed. No-op hook runs do not call dart pub get.
To apply only one package:
await patchwork.apply(input, output, package: 'collection');
These helpers are for user-owned patches committed by the application or workspace. Dependency packages that publish their own patch contributions should use package-provided overlays instead.
Package-Provided Overlays #
Packages can also publish narrowly scoped patch contributions for their own dependencies. This is for package authors who know that their package needs a temporary fix in a dependency, while downstream applications should only depend on the package and run normally.
The provider package must depend on Patchwork as a regular dependency, not a dev dependency, so Patchwork's build hook is present in downstream dependency graphs:
dart pub add patchwork
Create and commit the dependency patch from the provider package:
dart run patchwork patch collection
# edit .patchwork/collection@<version>/
dart run patchwork commit collection
dart run patchwork overlay collection --reason "Fix parser crash used here."
patchwork overlay creates or updates patchwork.yaml:
overlays:
-
package: "collection"
version: "1.19.1"
sha256: "<source-tree-sha>"
patch: "patches/collection@1.19.1.patch"
reason: "Fix parser crash used here."
Commit these provider-owned files:
patchwork.yamlpatches/<pkg>@<version>.patchpatchwork.lock
When an application depends on the provider package, Patchwork's package hook
scans dependency packages for patchwork.yaml, selects overlays matching the
currently resolved package version and source sha256, and composes all
matching patch contributions into one generated output at
.dart_tool/patchwork/<pkg>@<version>/.
If multiple dependency packages provide overlays for the same target package, Patchwork applies provider overlays in deterministic order by provider package name and patch path. If the root application also owns a committed patch for that same target, the root patch is applied last. Conflicting patches fail the build with a deterministic diagnostic; Patchwork reports the conflict instead of trying to merge it.
Library API #
The CLI uses the same API that hooks or other Dart tooling can call:
import 'dart:io';
import 'package:patchwork/patchwork.dart';
Future<void> main() async {
final patchwork = await Patchwork.open(Directory.current);
await patchwork.patch('collection');
await patchwork.commit('collection');
await patchwork.apply('collection');
await patchwork.undo('collection');
final state = await patchwork.inspect();
stdout.writeln('${state.packages.length} patchwork packages');
}
Use PatchRef.version('1.19.0') with patch when carrying an older patch onto
a newer dependency source.
What To Commit #
Commit these files in projects that use Patchwork:
patchwork.lockpatches/*.patch
These files are the committed source of truth for your dependency patches.
State Model #
Patchwork uses four project-local locations:
.patchwork/<pkg>@<version>/is the editable work-in-progress copy.patches/<pkg>@<version>.patchis the committed reviewable patch file.patchwork.lockrecords the current source, current committed patch hashes, and applied-output safety state. It does not record historical patch files..dart_tool/patchwork/<pkg>@<version>/is generated bypatchwork applyand wired into pub throughpubspec_overrides.yaml.
The lockfile is intentionally a lockfile, not a historical patch manifest. A typical entry looks like this:
version: 2
packages:
collection:
version: "1.19.1"
source:
type: "hosted"
url: "https://pub.dev"
sha256: "<source-tree-sha>"
patch:
edit-sha256: "<edit-tree-sha>"
commit-sha256: "<patch-file-sha>"
applied:
patch-sha256: "<patch-file-sha>"
path: ".dart_tool/patchwork/collection@1.19.1"
source may describe hosted or custom hosted packages, path dependencies, or
git dependencies. Git sources record branch when pub resolves a ref and
commit when pub records the resolved commit.
Patchwork derives historical and stale patch inventory from safe
patches/<pkg>@<version>.patch filenames. Older patch files remain visible to
status and doctor until you carry them forward or remove them.
source:
type: "path"
path: "../packages/foo"
sha256: "<source-tree-sha>"
source:
type: "git"
url: "https://example.com/foo.git"
branch: "main"
commit: "<resolved-commit>"
path: "packages/foo"
sha256: "<source-tree-sha>"
What Stays Generated #
Do not commit Patchwork's generated integration state:
.patchwork/.dart_tool/patchwork/pubspec_overrides.yaml
patchwork apply never mutates the primary pubspec.yaml; it writes
pubspec_overrides.yaml so pub resolves patched packages through generated path
overrides. pubspec_overrides.yaml is shared with other tools and local
workflow, so Patchwork only manages its own patch override entries.
Commands #
| Command | Description |
|---|---|
patchwork patch <pkg> [--continue [version]] [--force] [--json] |
Create a source-based edit. |
patchwork commit [pkg] [--json] |
Commit open edits into patch files. |
patchwork overlay <pkg> [--reason <text>] [--json] |
Register a committed patch in patchwork.yaml. |
patchwork apply [pkg] [--json] |
Apply committed patches. |
patchwork undo <pkg> [--json] |
Remove one applied patch. |
patchwork status [--json] |
Show patch and override state. |
patchwork doctor [--json] |
Check local readiness. |
Packages are plain pub package names selected by the current pub resolution.
Patchwork rejects target syntax such as pub:collection, collection@1.19.1,
path:collection, git URLs, filesystem paths, the current project package, and
workspace member packages.
Use --json when a script, editor, or agent needs stable command output:
dart run patchwork status --json
JSON mode prints one JSON object on stdout and keeps the normal exit-code
rules. Patchwork and usage failures use an error object with a stable code.
Path fields use the same values the command would show in human output, such as
.patchwork/collection@1.19.1 and patches/collection@1.19.1.patch.
Migrating From Cache Patches #
If you currently use a cache patch tool or manual .pub-cache edits, restore a
clean dependency copy before creating a Patchwork edit. Patchwork should diff
from the original dependency source, not from a package that already has local
cache edits applied.
Patchwork does not import other patch formats yet. Recreate the dependency edit
with patchwork patch <package>, then commit it with
patchwork commit <package>.
Carrying Patches Across Upgrades #
When an upstream release may contain your fix, undo the generated override before upgrading so pub resolves the real dependency source:
dart run patchwork undo collection
dart pub get
dart pub upgrade collection
dart pub get
If the upstream release contains the fix, create a fresh edit from that source
and commit it unchanged. Patchwork removes now-unused lockfile safety state; the
older patch file remains in patches/ until you remove it intentionally:
dart run patchwork patch collection
dart run patchwork commit collection
If the upstream release does not contain the fix, explicitly continue from the older patch file:
dart run patchwork patch collection --continue 1.19.0
dart run patchwork commit collection
dart run patchwork apply collection
dart pub get
CI Check #
Run Patchwork in CI after dependencies are installed:
dart run patchwork apply
dart pub get
dart run patchwork status
dart test
Use patchwork doctor when CI should fail on missing, stale, or unapplied
patch state.
Example #
The repository contains a runnable example under examples/hello_patch. The
package-level example/README.md links to that walkthrough for pub.dev.