Background#

If you build a project on top of an open-source template or boilerplate repo, you eventually hit a fork-management problem: how do you keep pulling in upstream improvements without your own content, config, or custom code getting clobbered every time you merge?

Here’s the setup I landed on. It uses a second Git remote pointed at the upstream repo, plus a custom merge driver that tells Git to always keep “our” version of specific files and folders. Once it’s wired up, pulling updates becomes a two-command routine.

Why Not Just Fork the Upstream Repo?#

The usual advice for this kind of setup is: fork the upstream repo, then build your project inside the fork. That way, your repo is a fork, and pulling upstream changes is just a routine “sync fork” operation.

That advice assumes you’re starting fresh. I wasn’t. My repo was already live, with its own commit history going back years, long before I decided to move it onto an open-source template published on GitHub. There was no clean way to retroactively turn an existing, live repo into a fork of a repo that didn’t exist yet when mine was created - and I wasn’t willing to throw away my history just to get a tidier Git relationship.

So instead of forking, I linked the histories after the fact: added the upstream repo as a second remote on my existing repo, did the initial merge, and set up .gitattributes with a custom merge driver so my content wouldn’t get overwritten by the upstream repo’s own files going forward. If you’re in the same boat - an existing repo that you want to start tracking an external template from, without recreating it as a fork - this is the pattern that worked for me.

Initial Setup (One-Time)#

This part only needs to happen once, on the repo itself, and it’s what makes everything else possible.

  1. Add a .gitattributes file at the root of your repo that marks the paths you want protected with the merge=ours strategy - for example:

    content/** merge=ours
    static/images/** merge=ours
    config.toml merge=ours

    Adjust the paths to match your own project’s layout.

  2. Register the custom merge driver locally so Git knows how to apply the rule:

    git config merge.ours.driver true
  3. Commit and push .gitattributes:

    git add .gitattributes
    git commit -m "Add merge=ours rules to protect personal content during template sync"
    git push

Once .gitattributes is committed and pushed, it’s a permanent part of your repo’s history - it travels with every future clone. You won’t need to redo this step again, even on a fresh machine. What doesn’t travel automatically is the remote pointing to the upstream repo, and the local merge driver registration - that’s the part covered next.

Setting This Up on a New Machine (or Fresh Clone)#

Git remotes and local Git config don’t travel with a repo - only tracked files like .gitattributes do. So on a new clone, you need to re-run two commands:

  1. Add the upstream repo as a remote:

    git remote add template https://github.com/your-username/your-upstream-repo.git

    (Name it whatever makes sense to you - template, upstream, theme, etc. Just stay consistent with whatever name you use in the commands below.)

  2. Register the custom merge driver: This tells Git how to honor the merge=ours rule defined in .gitattributes, so your own content is protected whenever you merge in upstream changes.

    git config merge.ours.driver true

Ongoing Sync: Pulling Updates from Upstream#

Once the remote and merge driver are set up, syncing is just two steps:

  1. Fetch the latest upstream changes:

    git fetch template
  2. Merge the updates:

    git merge template/master

    (Use whatever the upstream repo’s default branch is called - main, master, etc.)

The .gitattributes rules do the heavy lifting here - they automatically prevent your own folders and files (in my case, things like content/, static/images/, and config.toml) from being overwritten during the merge. If you want to exclude (or stop excluding) other files or folders from future syncs, just edit .gitattributes to match your own project’s layout.

That’s it - no manual cherry-picking, no diffing upstream changes by hand. The merge driver keeps your content safe by default, and you opt files in or out as your setup evolves.

Flush!

- Written by human, polished by AI
- This post is licensed under CC BY-SA 4.0