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:

  1. Capturing local changes — capture local edits as a .patch file with dfetch diff

  2. Adding the patch to the manifest — reference the patch from the manifest so it is applied on every fetch

  3. Refreshing a patch — refresh the patch as your edits evolve with dfetch update-patch

  4. Upgrading the upstream version — re-apply your patch when you move to a new upstream version

  5. 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"

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 23864ef2

  • Explicit 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
    """

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:

  1. Re-fetches the upstream revision (without applying any patches).

  2. Computes the diff between that clean baseline and your current working tree.

  3. Overwrites the last patch in the manifest list with the new diff.

  4. 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

        """

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"

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"

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.

See Format patch in the command reference for all options.

Troubleshooting

“No diffs found”

dfetch diff found 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, run dfetch update some-project first 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-patch to regenerate the patch from the resolved working tree.

“skipped - Uncommitted changes”

dfetch update-patch detected uncommitted changes in the project directory. Commit those changes first (Git users can also git 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-project first. 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. Use dfetch diff some-project to create the initial patch, then add it to the manifest as described in Adding the patch to the manifest.