Patch a project¶
Dfetch has a first-class patch workflow. When you need to fix a bug or apply a customisation to a vendored dependency, you can track that change as a patch file that is automatically re-applied on every dfetch update. When the fix is ready to share, Format patch converts it into a contributor-ready unified diff that upstream maintainers can apply directly.
Note
New to patching with Dfetch? Start with My first patch in the getting-started tutorial, which walks through creating and applying your first patch step by step.
The full lifecycle looks like this:
Capturing local changes — capture local edits as a
.patchfile withdfetch diffAdding the patch to the manifest — reference the patch from the manifest so it is applied on every fetch
Refreshing a patch — refresh the patch as your edits evolve with
dfetch update-patchUpgrading the upstream version — re-apply your patch when you move to a new upstream version
Contributing the patch upstream — reformat the patch for upstream use with
dfetch format-patch
Before you begin¶
Dfetch calculates the diff for a project by comparing the working tree
against the revision recorded in the project’s .dfetch_data.yaml metadata
file. For that comparison to be meaningful, the fetched files should already
be committed to your superproject’s VCS — they become the baseline that the
patch is measured against.
After fetching, commit before editing:
$ dfetch update some-project
$ git add some-project/
$ git commit -m "vendor: add some-project v1.2.3"
$ dfetch update some-project
$ svn add some-project/
$ svn commit some-project/ -m "vendor: add some-project v1.2.3"
You can then make edits to some-project/ and capture them with
dfetch diff. Both committed and uncommitted edits are included in the
generated patch, so you do not need to commit every intermediate step — only
the clean upstream baseline matters.
Capturing local changes¶
After fetching a project with dfetch update, make your edits directly in the vendored source tree. Once you are happy with the changes, run:
$ dfetch diff some-project
Dfetch compares the working tree against the revision recorded in the
metadata file and writes a patch file named some-project.patch.
What goes into the patch
The diff captures all tracked modifications and any new untracked files in the
vendored directory. Files ignored by your superproject’s VCS (via
.gitignore or svn:ignore) and the dfetch metadata file itself are
always excluded.
Controlling which revisions are compared
By default, Dfetch uses the revision stored in the project’s metadata as the base. You can override this:
Single base revision:
dfetch diff some-project --revs 23864ef2Explicit range:
dfetch diff some-project --revs 23864ef2:4a9cb18
See Diff in the command reference for all options.
Example: A patch file is generated
Scenario: A patch file is generated
Given "SomeProject/README.md" in MyProject is changed and committed with
"""
An important sentence for the README!
"""
When I run "dfetch diff SomeProject"
Then the patch file 'MyProject/SomeProject.patch' is generated
"""
diff --git a/README.md b/README.md
index 1e65bd6..faa3b21 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
Generated file for SomeProject.git
+An important sentence for the README!
"""
Example: New files are part of the patch
Scenario: New files are part of the patch
Given files as '*.tmp' are ignored in git in MyProject
And "SomeProject/NEWFILE.md" in MyProject is created and committed with
"""
A completely new tracked file.
"""
And "SomeProject/NEW_UNCOMMITTED_FILE.md" in MyProject is created
And "SomeProject/IGNORE_ME.tmp" in MyProject is created
When I run "dfetch diff SomeProject"
Then the patch file 'MyProject/SomeProject.patch' is generated
"""
diff --git a/NEWFILE.md b/NEWFILE.md
new file mode 100644
index 0000000..a2d8605
--- /dev/null
+++ b/NEWFILE.md
@@ -0,0 +1 @@
+A completely new tracked file.
diff --git a/NEW_UNCOMMITTED_FILE.md b/NEW_UNCOMMITTED_FILE.md
new file mode 100644
index 0000000..0ee3895
--- /dev/null
+++ NEW_UNCOMMITTED_FILE.md
@@ -0,0 +1,1 @@
+Some content
"""
Example: No change is present
Scenario: No change is present
When I run "dfetch diff SomeProject"
Then the output shows
"""
Dfetch (0.14.3)
SomeProject:
> No diffs found since 59efb91396fd369eb113b43382783294dc8ed6d2
"""
Example: Diff is generated on uncommitted changes
Scenario: Diff is generated on uncommitted changes
Given "SomeProject/README.md" in MyProject is changed with
"""
An important sentence for the README!
"""
When I run "dfetch diff SomeProject"
Then the patch file 'MyProject/SomeProject.patch' is generated
"""
diff --git a/README.md b/README.md
index 1e65bd6..faa3b21 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
Generated file for SomeProject.git
+An important sentence for the README!
"""
Example: Metadata is not part of diff
Scenario: Metadata is not part of diff
Given the metadata file ".dfetch_data.yaml" of "MyProject/SomeProject" is changed
When I run "dfetch diff SomeProject"
Then the output shows
"""
Dfetch (0.14.3)
SomeProject:
> No diffs found since 59efb91396fd369eb113b43382783294dc8ed6d2
"""
Example: A patch file is generated
Scenario: A patch file is generated
Given "SomeProject/README.md" in MySvnProject is changed, added and committed with
"""
An important sentence for the README!
"""
When I run "dfetch diff SomeProject" in MySvnProject
Then the patch file 'MySvnProject/SomeProject.patch' is generated
"""
Index: README.md
===================================================================
--- README.md
+++ README.md
@@ -1,1 +1,2 @@
Generated file for SomeProject
+An important sentence for the README!
"""
Example: New files are part of the patch
Scenario: New files are part of the patch
Given files as '*.tmp' are ignored in 'MySvnProject/SomeProject' in svn
And "SomeProject/NEWFILE.md" in MySvnProject is changed, added and committed with
"""
A completely new tracked file.
"""
And "SomeProject/NEW_UNCOMMITTED_FILE.md" in MySvnProject is created
And "SomeProject/IGNORE_ME.tmp" in MySvnProject is created
When I run "dfetch diff SomeProject" in MySvnProject
Then the patch file 'MySvnProject/SomeProject.patch' is generated
"""
Index: NEWFILE.md
===================================================================
--- NEWFILE.md
+++ NEWFILE.md
@@ -0,0 +1,1 @@
+A completely new tracked file.
Index: NEW_UNCOMMITTED_FILE.md
===================================================================
--- /dev/null
+++ NEW_UNCOMMITTED_FILE.md
@@ -0,0 +1,1 @@
+Some content
"""
Example: No change is present
Scenario: No change is present
When I run "dfetch diff SomeProject" in MySvnProject
Then the output shows
"""
Dfetch (0.14.3)
SomeProject:
> No diffs found since 1
"""
Example: A patch file is generated on uncommitted changes
Scenario: A patch file is generated on uncommitted changes
Given "SomeProject/README.md" in MySvnProject is changed with
"""
An important sentence for the README!
"""
When I run "dfetch diff SomeProject" in MySvnProject
Then the patch file 'MySvnProject/SomeProject.patch' is generated
"""
Index: README.md
===================================================================
--- README.md
+++ README.md
@@ -1,1 +1,2 @@
Generated file for SomeProject
+An important sentence for the README!
"""
Example: Metadata is not part of diff
Scenario: Metadata is not part of diff
Given the metadata file ".dfetch_data.yaml" of "MySvnProject/SomeProject" is changed
When I run "dfetch diff SomeProject" in MySvnProject
Then the output shows
"""
Dfetch (0.14.3)
SomeProject:
> No diffs found since 1
"""
Adding the patch to the manifest¶
Once you have a patch file, commit it to your repository and reference it from
the project entry in dfetch.yaml using the Patch attribute:
manifest:
version: '0.0'
projects:
- name: some-project
url: https://github.com/example/some-project
tag: v1.2.3
patch: some-project.patch
From this point on, every dfetch update will fetch the upstream source and
re-apply the patch on top. You can test the round-trip immediately:
$ dfetch update --force some-project
The --force flag overwrites the working tree with the freshly fetched and
patched version. Confirm the result looks right, then commit the manifest
change and the patch file together.
Organizing patch files
Keep patch files alongside dfetch.yaml or in a dedicated subdirectory such
as patches/. Dfetch resolves patch paths relative to the manifest file,
so as long as the path in the manifest matches the location on disk you have
full flexibility. Committing the patch files to VCS ensures every team member
and every CI run gets the same result.
Multiple patches
You can split your changes into separate, focused patch files and list them in order:
patch:
- 001-fix-null-dereference.patch
- 002-add-missing-header.patch
Patches are applied in the order listed. A good convention is to prefix each
file name with a three-digit, zero-padded number (001-, 002-, …) so
they sort correctly and their purpose is clear at a glance. The dfetch update-patch
command always updates the last patch in the list, so the earlier patches represent
stable, settled changes and the final one accumulates ongoing work.
See Patch in the manifest reference for the full attribute syntax.
Refreshing a patch¶
As your local edits evolve, the existing patch file may become stale. Instead of manually regenerating it, run:
$ dfetch update-patch some-project
This command:
Re-fetches the upstream revision (without applying any patches).
Computes the diff between that clean baseline and your current working tree.
Overwrites the last patch in the manifest list with the new diff.
Re-fetches the project and applies all patches so the working tree is left in the patched state.
It is safe to run repeatedly as you iterate on a fix. The upstream revision stays unchanged — only the patch file is updated.
Note
update-patch requires the project directory to have no uncommitted
changes in the superproject. Commit your work first (Git users can also
git stash), then run the command.
See Update patch in the command reference for all options.
Example: Patch is updated with new local changes
Scenario: Patch is updated with new local changes
Given "SomeProject/README.md" in MyProject is changed and committed with
"""
Update to patched file for SomeProject.git
"""
When I run "dfetch update-patch SomeProject" in MyProject
Then the patch file 'MyProject/patches/SomeProject.patch' is updated
"""
diff --git a/README.md b/README.md
index 1e65bd6..925b8c4 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
-Generated file for SomeProject.git
+Patched file for SomeProject.git
+Update to patched file for SomeProject.git
"""
And the output shows
"""
Dfetch (0.14.3)
SomeProject:
> Fetched master - f9b88b8259d9a7fb48327bf23beabe40c150d474
> Updating patch "patches/SomeProject.patch"
> Fetched master - f9b88b8259d9a7fb48327bf23beabe40c150d474
> Applying patch "patches/SomeProject.patch"
successfully patched 1/1: b'README.md'
"""
Example: Patch is updated with new but not ignored files
Scenario: Patch is updated with new but not ignored files
Given files as '*.tmp' are ignored in git in MyProject
And "SomeProject/IGNORE_ME.tmp" in MyProject is created
And "SomeProject/NEWFILE.md" in MyProject is created
And all files in MyProject are committed
When I run "dfetch update-patch SomeProject" in MyProject
Then the patch file 'MyProject/patches/SomeProject.patch' is updated
"""
diff --git a/NEWFILE.md b/NEWFILE.md
new file mode 100644
index 0000000..0ee3895
--- /dev/null
+++ b/NEWFILE.md
@@ -0,0 +1 @@
+Some content
diff --git a/README.md b/README.md
index 1e65bd6..38c1a65 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-Generated file for SomeProject.git
+Patched file for SomeProject.git
"""
Example: Patch is updated with new local changes
Scenario: Patch is updated with new local changes
Given "SomeProject/README.md" in MySvnProject is changed, added and committed with
"""
Update to patched file for SomeProject
"""
When I run "dfetch update-patch SomeProject" in MySvnProject
Then the patch file 'MySvnProject/patches/SomeProject.patch' is updated
"""
Index: README.md
===================================================================
--- README.md
+++ README.md
@@ -1,1 +1,2 @@
-Generated file for SomeProject
+Patched file for SomeProject
+Update to patched file for SomeProject
"""
And the output shows
"""
Dfetch (0.14.3)
Update patch is only fully supported in git superprojects!
SomeProject:
> Fetched trunk - 1
> Updating patch "patches/SomeProject.patch"
> Fetched trunk - 1
> Applying patch "patches/SomeProject.patch"
successfully patched 1/1: b'README.md'
"""
Example: Patch is updated with new but not ignored files
Scenario: Patch is updated with new but not ignored files
Given files as '*.tmp' are ignored in 'MySvnProject/SomeProject' in svn
And "SomeProject/IGNORE_ME.tmp" in MySvnProject is created
And all files in MySvnProject are added and committed
When I run "dfetch update-patch SomeProject" in MySvnProject
Then the patch file 'MySvnProject/patches/SomeProject.patch' is updated
"""
Index: README.md
===================================================================
--- README.md
+++ README.md
@@ -1,1 +1,1 @@
-Generated file for SomeProject
+Patched file for SomeProject
"""
Upgrading the upstream version¶
When you want to move to a new upstream release, update the tag,
branch, or revision in dfetch.yaml and then run dfetch update.
Dfetch fetches the new version and attempts to re-apply the patch using fuzzy
matching, so patches often survive minor context changes automatically.
$ # 1. Edit dfetch.yaml: change tag v1.2.3 → v1.3.0
$ dfetch update some-project
Three outcomes are possible:
Patch applies cleanly — you are done. Review the result, commit the updated manifest and the updated vendored files.
Patch applies with fuzz warnings — the patch applied but the context lines
shifted slightly. The files are in the correct state. Run
dfetch update-patch some-project to refresh the patch against the new
baseline so it stays clean for future upgrades:
$ git add some-project/
$ git commit -m "vendor: update some-project to v1.3.0"
$ dfetch update-patch some-project
$ git add some-project.patch
$ git commit -m "patches: refresh some-project.patch for v1.3.0"
$ svn commit some-project/ -m "vendor: update some-project to v1.3.0"
$ dfetch update-patch some-project
$ svn commit some-project.patch -m "patches: refresh some-project.patch for v1.3.0"
Patch fails to apply — the upstream changes conflict with the local edits
tracked in the patch. Resolve the conflict manually by editing the vendored
files, then use dfetch update-patch to record the resolved state:
$ # Manually resolve conflicts in some-project/
$ git add some-project/
$ git commit -m "vendor: update some-project to v1.3.0 with resolved conflicts"
$ dfetch update-patch some-project
$ git add some-project.patch
$ git commit -m "patches: update some-project.patch for v1.3.0"
$ # Manually resolve conflicts in some-project/
$ svn commit some-project/ -m "vendor: update some-project to v1.3.0 with resolved conflicts"
$ dfetch update-patch some-project
$ svn commit some-project.patch -m "patches: update some-project.patch for v1.3.0"
Contributing the patch upstream¶
Patches generated by dfetch diff are relative to the project’s vendored
directory inside your repository. Most upstream projects expect patches to be
relative to their own root, which is a different path. To reformat all patches
for a project:
$ dfetch format-patch some-project
This writes a formatted-some-project.patch file (or one file per patch if
there are several) in the current directory. Use --output-directory to
place the formatted files in a specific location:
$ dfetch format-patch some-project --output-directory patches/upstream
Before sending a patch, do a dry-run check to confirm it applies cleanly to a local clone of the upstream repository:
$ git apply --check formatted-some-project.patch
Example: All patch files are formatted
Scenario: All patch files are formatted
Given the manifest 'dfetch.yaml'
"""
manifest:
version: '0.0'
remotes:
- name: github-com-dfetch-org
url-base: https://github.com/dfetch-org/test-repo
projects:
- name: ext/test-repo-tag
tag: v2.0
dst: ext/test-repo-tag
patch:
- 001-diff.patch
- 002-diff.patch
- 003-new-file.patch
"""
And the patch file '001-diff.patch'
"""
diff --git a/README.md b/README.md
index 32d9fad..62248b7 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
# Test-repo
-A test repo for testing dfetch.
+A test repo for testing patch.
"""
And the patch file '002-diff.patch'
"""
diff --git a/README.md b/README.md
index 32d9fad..62248b7 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
# Test-repo
-A test repo for testing patch.
+A test repo for testing formatting patches.
"""
And the patch file '003-new-file.patch'
"""
diff --git a/NEWFILE.md b/NEWFILE.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/NEWFILE.md
@@ -0,0 +1 @@
+This is a new file.
"""
And all projects are updated
When I run "dfetch format-patch ext/test-repo-tag --output-directory patches"
Then the patch file 'patches/001-diff.patch' is generated
"""
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: John Doe <john@dfetch.io>
Date: Mon, 02 Feb 2026 21:02:42 +0000
Subject: [PATCH 1/3] Patch for ext/test-repo-tag
Patch for ext/test-repo-tag
diff --git a/README.md b/README.md
index 32d9fad..62248b7 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
# Test-repo
-A test repo for testing dfetch.
+A test repo for testing patch.
"""
And the patch file 'patches/002-diff.patch' is generated
"""
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: John Doe <john@dfetch.io>
Date: Mon, 02 Feb 2026 21:02:42 +0000
Subject: [PATCH 2/3] Patch for ext/test-repo-tag
Patch for ext/test-repo-tag
diff --git a/README.md b/README.md
index 32d9fad..62248b7 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
# Test-repo
-A test repo for testing patch.
+A test repo for testing formatting patches.
"""
And the patch file 'patches/003-new-file.patch' is generated
"""
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: John Doe <john@dfetch.io>
Date: Mon, 02 Feb 2026 21:02:42 +0000
Subject: [PATCH 3/3] Patch for ext/test-repo-tag
Patch for ext/test-repo-tag
diff --git a/NEWFILE.md b/NEWFILE.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/NEWFILE.md
@@ -0,0 +1,1 @@
+This is a new file.
"""
Example: Svn subproject in Git superproject gives a svn patch
Scenario: Svn subproject in Git superproject gives a svn patch
Given a svn-server "SomeProject" with the files
| path |
| SomeFolder/SomeSubFolder/README.md |
And the patch file 'MyProject/patches/001-diff.patch'
"""
diff --git a/README.md b/README.md
index 32d9fad..62248b7 100644
--- a/README.md
+++ b/README.md
@@ -1,1 +1,1 @@
-Generated file for SomeProject
+Patched file for SomeProject
"""
And a fetched and committed git-repo "MyProject" with the manifest:
"""
manifest:
version: 0.0
projects:
- name: SomeProject
url: some-remote-server/SomeProject
src: SomeFolder/SomeSubFolder
patch:
- patches/001-diff.patch
vcs: svn
"""
When I run "dfetch format-patch SomeProject --output-directory MyProject/patches"
Then the patch file 'MyProject/patches/001-diff.patch' is generated
"""
Index: SomeFolder/SomeSubFolder/README.md
===================================================================
--- SomeFolder/SomeSubFolder/README.md
+++ SomeFolder/SomeSubFolder/README.md
@@ -1,1 +1,1 @@
-Generated file for SomeProject
+Patched file for SomeProject
"""
Once confirmed, hand the file off to the upstream project. Upstream maintainers can apply it with git am:
$ git am formatted-some-project.patch
Each patch file results in a separate commit.
$ svn patch formatted-some-project.patch
Example: All patch files are formatted
Scenario: All patch files are formatted
Given a svn-server "SomeProject" with the files
| path |
| SomeFolder/SomeSubFolder/README.md |
And the patch file 'MySvnProject/patches/001-diff.patch'
"""
Index: README.md
===================================================================
--- README.md
+++ README.md
@@ -1,1 +1,1 @@
-Generated file for SomeProject
+Patched file for SomeProject
"""
And the patch file 'MySvnProject/patches/002-diff.patch'
"""
Index: README.md
===================================================================
--- README.md
+++ README.md
@@ -1,1 +1,1 @@
-Generated file for SomeProject
+Patched file for formatted patch of SomeProject
"""
And a fetched and committed MySvnProject with the manifest
"""
manifest:
version: 0.0
projects:
- name: SomeProject
url: some-remote-server/SomeProject
src: SomeFolder/SomeSubFolder
patch:
- patches/001-diff.patch
- patches/002-diff.patch
vcs: svn
"""
And all projects are updated
When I run "dfetch format-patch SomeProject --output-directory formatted-patches" in MySvnProject
Then the patch file 'MySvnProject/formatted-patches/001-diff.patch' is generated
"""
Index: SomeFolder/SomeSubFolder/README.md
===================================================================
--- SomeFolder/SomeSubFolder/README.md
+++ SomeFolder/SomeSubFolder/README.md
@@ -1,1 +1,1 @@
-Generated file for SomeProject
+Patched file for SomeProject
"""
And the patch file 'MySvnProject/formatted-patches/002-diff.patch' is generated
"""
Index: SomeFolder/SomeSubFolder/README.md
===================================================================
--- SomeFolder/SomeSubFolder/README.md
+++ SomeFolder/SomeSubFolder/README.md
@@ -1,1 +1,1 @@
-Generated file for SomeProject
+Patched file for formatted patch of SomeProject
"""
Example: Git subproject in Svn superproject gives a git patch
Scenario: Git subproject in Svn superproject gives a git patch
Given a git repository "SomeProject.git"
And the patch file 'MySvnProject/patches/001-diff.patch'
"""
Index: README.md
===================================================================
--- README.md
+++ README.md
@@ -1,1 +1,1 @@
-Generated file for SomeProject
+Patched file for SomeProject
"""
And a fetched and committed MySvnProject with the manifest
"""
manifest:
version: 0.0
projects:
- name: some-subproject
url: some-remote-server/SomeProject.git
patch:
- patches/001-diff.patch
"""
When I run "dfetch format-patch some-subproject --output-directory MySvnProject/patches"
Then the patch file 'MySvnProject/patches/001-diff.patch' is generated
"""
From ce0f26a0ef7924942debe7285af89337bac26ddf Mon Sep 17 00:00:00 2001
From: ben <ben@example.com>
Date: Sat, 07 Feb 2026 16:23:34 +0000
Subject: [PATCH] Patch for some-subproject
Patch for some-subproject
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,1 +1,1 @@
-Generated file for SomeProject
+Patched file for SomeProject
"""
See Format patch in the command reference for all options.
Troubleshooting¶
“No diffs found”
dfetch difffound no changes between the working tree and the upstream baseline recorded in.dfetch_data.yaml. If you expected changes, make sure the edits are in the vendored directory and are not excluded by your VCS ignore rules. If the metadata file is missing, rundfetch update some-projectfirst to re-establish the baseline.
Patch fails to apply after an upstream bump
The upstream version introduced changes that conflict with the local edits in the patch. Follow the manual resolution workflow in Upgrading the upstream version: edit the vendored files to the desired state, commit them, then run
dfetch update-patchto regenerate the patch from the resolved working tree.
“skipped - Uncommitted changes”
dfetch update-patchdetected uncommitted changes in the project directory. Commit those changes first (Git users can alsogit stash), then run the command so the patch calculation starts from a clean state.
“skipped - the project was never fetched before”
Run
dfetch update some-projectfirst. The project must exist on disk before a patch can be updated.
“skipped - there is no patch file”
The project has no
patch:entry in the manifest. Usedfetch diff some-projectto create the initial patch, then add it to the manifest as described in Adding the patch to the manifest.