Skip to content

Instantly share code, notes, and snippets.

@kbralten
Last active March 6, 2026 20:11
Show Gist options
  • Select an option

  • Save kbralten/d22ab07dce845d2a8643a6d92b318387 to your computer and use it in GitHub Desktop.

Select an option

Save kbralten/d22ab07dce845d2a8643a6d92b318387 to your computer and use it in GitHub Desktop.
Workflow for maintaining a fork that tracks an upstream repository while also integrating both upstream PRs and your own changes.

The Maintainer's Workflow

This document describes a safe, repeatable workflow for maintaining a fork that tracks an upstream repository while also integrating:

  • your local feature branches (those you maintain in your fork)
  • upstream PRs/MRs you want to include before they're merged into the upstream

The core idea: keep your base branch (usually main/master) a pristine mirror of upstream/base, and create a disposable integration branch that composes a specific mix of upstream mainline, selected upstream PRs, and your local feature branches into a buildable copy of the software.

Why this helps

  • Keeps your fork's base clean and trivially syncable with upstream.
  • Lets you compose arbitrary sets of local branches and upstream PRs into a reproducible composite build (a snapshot you can build and run).
  • Avoids long-lived local merge branches that accumulate merge commits and conflicts.

Key concepts

  • upstream: the original project you forked from (remote named upstream).
  • origin: your fork (remote named origin).
  • base branch: the branch in upstream you track (commonly main or master). Do not commit directly here.
  • integration branch: a disposable branch (e.g., integration-build) built from base that merges feature branches and upstream PRs to produce a composite build you can build and run.
  • feature branches: your own branches (e.g., feature/foo) that you may merge into the integration branch.
  • upstream PRs/MRs: contributions in the upstream repo you can fetch by special refs and merge into the integration branch to include in a composite build.

Principles

  • Never commit or push directly to base; always update it by fetching and fast-forwarding from upstream.
  • Make the integration branch disposable: recreate it from the updated base. Do this regularly to pick up new mainline changes from upstream.
  • Enable git rerere so Git remembers conflict resolutions across rebuilds: git config --global rerere.enabled true.

Prerequisites

  • A fork with remotes set up: origin -> your fork, upstream -> original repository.
  • git installed and basic familiarity with branches, fetch, merge, and rebase.

Initial setup (one-time)

  1. Add upstream remote (if you haven't already):
git remote add upstream https://github.com/original-owner/repo.git
git fetch upstream --tags
  1. Turn on rerere to preserve conflict resolutions:
git config --global rerere.enabled true
  1. Keep copies of which branches / PRs you want to include in text files (examples below):
  • integration-branches.txt — one branch name per line (local or origin/branch)
  • integration-prs.txt — upstream PR/MR IDs (one per line)

Comments and blank lines are allowed in those files (lines starting with # are ignored).

The integration rebuild workflow (manual steps)

  1. Ensure working directory is clean: stash or commit unfinished work.
git status --porcelain
  1. Update your local base to match upstream (do not force anything):
git checkout base-or-main-and-name-it-appropriately
git fetch upstream
git reset --hard upstream/<base>
  1. Recreate the integration branch from the updated base:
git branch -D integration-build 2>/dev/null || true
git checkout -b integration-build
  1. Merge a single local feature branch (example):
# Merge a single feature branch named "feature/awesome" from origin:
git merge --no-edit origin/feature/awesome || { echo "CONFLICT merging feature/awesome"; exit 1; }
  1. Merge a single upstream PR (example — GitHub):
# Fetch and merge upstream PR #1234 (GitHub):
git fetch upstream pull/1234/head
git merge --no-edit FETCH_HEAD || { echo "CONFLICT merging PR 1234"; exit 1; }
  1. Build or run the composite copy locally, or push the integration branch to your fork for a remote build/snapshot

  2. If conflicts occur during any merge, fix them, commit the resolution, and re-run the script or continue merges. Because git rerere is enabled, later rebuilds may auto-apply previous resolutions.

  3. When finished, either keep integration-build pushed (for CI and logs) or delete it locally; it can always be recreated.

File formats

  • integration-branches.txt
# my local features
feature/awesome
fix/typo
  • integration-prs.txt
# upstream PRs to include
1234
2345

Example: a minimal powershell script (annotated)

# --- CONFIGURATION ---
$RepoType = "GitHub" # Set to "GitHub" or "GitLab"
$UpstreamRemote = "upstream"
$BaseBranch = "test"
$IntegrationBranch = "integration-build"
$BranchesFile = "integration-branches.txt"
$PrsFile = "integration-prs.txt"

# Helper function to check exit codes and stop execution
function Check-Error {
    param([string]$Message)
    if ($LASTEXITCODE -ne 0) {
        Write-Host "Error: $Message" -ForegroundColor Red
        exit 1
    }
}

Write-Host "=== Starting Integration Rebuild ($RepoType Mode) ===" -ForegroundColor Cyan

# 1. Safety Check: Working Directory
if (git status --porcelain) {
    Write-Host "Error: Working directory dirty. Stash/Commit first." -ForegroundColor Red
    exit 1
}

# 2. Safety Check: Remote Existence
git remote | Where-Object { $_ -eq $UpstreamRemote } | Out-Null
if ($?) { 
    $remotes = git remote
    if ($remotes -notcontains $UpstreamRemote) {
        Write-Host "Error: Remote '$UpstreamRemote' not found." -ForegroundColor Red
        Write-Host "Please add it using: git remote add $UpstreamRemote <URL>" -ForegroundColor Gray
        exit 1
    }
}

# 3. Refresh Base (CRITICAL: Stop if this fails)
Write-Host "-> Updating $BaseBranch..." -ForegroundColor Green
git checkout $BaseBranch
Check-Error "Failed to checkout $BaseBranch. Stopping to prevent accidental merges."

git pull $UpstreamRemote $BaseBranch
Check-Error "Failed to pull from $UpstreamRemote."

# 4. Reset Integration Branch
Write-Host "-> Recreating $IntegrationBranch..." -ForegroundColor Green

# Check if branch exists
git show-ref --verify --quiet refs/heads/$IntegrationBranch
if ($LASTEXITCODE -eq 0) {
    # Branch exists, delete it
    git branch -D $IntegrationBranch
    Check-Error "Failed to delete old $IntegrationBranch. Are you currently on it?"
}

git checkout -b $IntegrationBranch
Check-Error "Failed to create $IntegrationBranch."

# 5. Merge Local Branches
if (Test-Path $BranchesFile) {
    Write-Host "-> Merging Local Branches..." -ForegroundColor Cyan
    $branches = Get-Content $BranchesFile | Where-Object { $_ -notmatch '^\s*#' -and $_ -notmatch '^\s*$' }
    foreach ($branch in $branches) {
        $branch = $branch.Trim()
        Write-Host "   Merging $branch..."
        git merge "origin/$branch" --no-edit -m "Merge local $branch"
        Check-Error "CONFLICT on $branch. Fix, commit, and re-run."
    }
}

# 6. Merge Upstream PRs/MRs
if (Test-Path $PrsFile) {
    Write-Host "-> Merging Upstream PRs/MRs..." -ForegroundColor Cyan
    $prs = Get-Content $PrsFile | Where-Object { $_ -notmatch '^\s*#' -and $_ -notmatch '^\s*$' }
    foreach ($pr_id in $prs) {
        $pr_id = $pr_id.Trim()
        Write-Host "   Fetching #$pr_id..."
        
        if ($RepoType -eq "GitHub") {
            git fetch $UpstreamRemote pull/$pr_id/head
        } elseif ($RepoType -eq "GitLab") {
            git fetch $UpstreamRemote merge-requests/$pr_id/head
        } else {
            Write-Host "Error: Invalid `$RepoType configuration. Set it to 'GitHub' or 'GitLab'." -ForegroundColor Red
            exit 1
        }

        if ($LASTEXITCODE -eq 0) {
            git merge FETCH_HEAD --no-edit -m "Merge #$pr_id"
            Check-Error "CONFLICT on #$pr_id. Fix, commit, and re-run."
        } else {
            Write-Host "Failed to fetch #$pr_id (Does it exist?)" -ForegroundColor Red
        }
    }
}

Write-Host "=== Build Complete on $IntegrationBranch ===" -ForegroundColor Green

Notes:

  • The script exits on the first conflict; this keeps behavior deterministic. Resolve conflicts, commit, and re-run.
  • If you get stuck in a merge conflict, just abandon the merge - integration is disposable. Either
    • git merge --abort
    • git reset --hard HEAD

Keeping local files out of Git changes

You may have local build scripts, config overrides, or environment files that you don't want showing up as repo changes. Here are safe ways to keep those files out of git status without modifying the project's committed files.

  1. Ignore files locally without touching .gitignore

Use .git/info/exclude — it works exactly like .gitignore, but only for your clone. Add file paths or patterns there to keep them untracked locally.

  1. Ignore changes to a tracked file (keep it in the repo)

If the file is already tracked and you want Git to stop noticing your edits:

Tell Git to “assume unchanged”:

git update-index --assume-unchanged path/to/file

Behavior:

  • The file remains in the repository.
  • Your local modifications are ignored by git status.
  • Git will not include your local edits in commits — but be careful: Git may still overwrite the file when switching branches or merging.

Undo:

git update-index --no-assume-unchanged path/to/file

Alternative: “skip worktree” (recommended for local config overrides)

This is a stronger signal used when you intend to keep the repo's version untouched locally. Use it for local configuration files that differ from the repository version.

Ignore local changes:

git update-index --skip-worktree path/to/file

Undo:

git update-index --no-skip-worktree path/to/file

When to use which?

  • --assume-unchanged: lightweight, primarily intended as a performance hint. Use it for short-lived local edits where you don't expect merges to touch the file.
  • --skip-worktree: stronger and preferable when you want to maintain persistent local overrides for a config file while still keeping the file tracked upstream.

Quick recommendations

  • For ignoring untracked local files: use .git/info/exclude.
  • For ignoring changes to tracked files: prefer --skip-worktree; use --assume-unchanged only when appropriate.

Practical tips

  • Use git rerere to avoid re-resolving the same conflicts across rebuilds.
  • To keep your fork's base branch up-to-date on GitHub: after fetching upstream you can push the base to your fork: git push origin <base>.
  • When you want to prepare a PR to upstream, branch off of base in a fresh branch and push that to your fork; do not base PRs on integration-build.

Safety and rollback

  • The integration branch is disposable; if it becomes messy, delete and recreate it from base.
  • Avoid force-pushing to base in your fork — only fast-forward it to match upstream.

Summary

This approach gives you a reproducible, scriptable way to build specific combinations of upstream mainline, selected upstream PRs, and your own local changes without polluting your fork's base branch. Keep the integration branch disposable, enable git rerere to reduce repetitive conflict work, and automate the process for faster, safer iterations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment