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
baseclean 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.
- 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
mainormaster). Do not commit directly here. - integration branch: a disposable branch (e.g.,
integration-build) built frombasethat 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.
- Never commit or push directly to
base; always update it by fetching and fast-forwarding fromupstream. - Make the integration branch disposable: recreate it from the updated
base. Do this regularly to pick up new mainline changes from upstream. - Enable
git rerereso Git remembers conflict resolutions across rebuilds:git config --global rerere.enabled true.
- A fork with remotes set up:
origin-> your fork,upstream-> original repository. gitinstalled and basic familiarity with branches, fetch, merge, and rebase.
- Add upstream remote (if you haven't already):
git remote add upstream https://github.com/original-owner/repo.git
git fetch upstream --tags- Turn on rerere to preserve conflict resolutions:
git config --global rerere.enabled true- Keep copies of which branches / PRs you want to include in text files (examples below):
integration-branches.txt— one branch name per line (local ororigin/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).
- Ensure working directory is clean: stash or commit unfinished work.
git status --porcelain- Update your local
baseto match upstream (do not force anything):
git checkout base-or-main-and-name-it-appropriately
git fetch upstream
git reset --hard upstream/<base>- Recreate the integration branch from the updated base:
git branch -D integration-build 2>/dev/null || true
git checkout -b integration-build- 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; }- 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; }-
Build or run the composite copy locally, or push the integration branch to your fork for a remote build/snapshot
-
If conflicts occur during any merge, fix them, commit the resolution, and re-run the script or continue merges. Because
git rerereis enabled, later rebuilds may auto-apply previous resolutions. -
When finished, either keep
integration-buildpushed (for CI and logs) or delete it locally; it can always be recreated.
- integration-branches.txt
# my local features
feature/awesome
fix/typo
- integration-prs.txt
# upstream PRs to include
1234
2345
# --- 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 GreenNotes:
- 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 --abortgit reset --hard HEAD
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.
- 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.
- 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/fileBehavior:
- 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/fileAlternative: “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/fileUndo:
git update-index --no-skip-worktree path/to/fileWhen 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-unchangedonly when appropriate.
- Use
git rerereto avoid re-resolving the same conflicts across rebuilds. - To keep your fork's
basebranch 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
basein a fresh branch and push that to your fork; do not base PRs onintegration-build.
- The integration branch is disposable; if it becomes messy, delete and recreate it from
base. - Avoid force-pushing to
basein your fork — only fast-forward it to match upstream.
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.