Create a self-contained ZIP archive of just the files that differ between two git branches — perfect for offline code review without needing a pull request.
- No PR required — compare any two related branches, tags, or commits
- Working tree & staged diffs — archive uncommitted or staged changes
- Three-way diffs — see what each side changed relative to the merge-base
- Offline review — extract the archive and use your favorite diff tool
- Cross-platform — runs on Windows, Linux, and macOS
- Binary-safe — handles text and binary files without corruption
- Tab completion — branches, tags, stashes, and repo paths auto-complete in PowerShell
- Subdirectory launch — run from anywhere inside a git repo
- Quick Start
- Parameters
- Comparison Modes
- Non-Interactive Mode
- Output Format
- Placeholder Files
- Tab Completion
- Diff Tool Recommendations
- Troubleshooting
- License
Prerequisite:
gitmust be on yourPATH
# Interactive — prompts for each input
pwsh ./Git-ArchiveBranchDiffs.ps1
# With parameters
pwsh ./Git-ArchiveBranchDiffs.ps1 -repositoryPath "C:\repos\myRepo" -leftBranch main -rightBranch feature/my-branch -outputDirectory "C:\output"
# Non-interactive — uses smart defaults
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive
# Archive uncommitted working tree changes
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive -workingTree
# Archive staged changes only
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive -staged
# Three-way diff showing merge-base
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive -threeWay
# Compare two tags
pwsh ./Git-ArchiveBranchDiffs.ps1 -leftBranch v1.0.0 -rightBranch v2.0.0
# Compare a tag to a branch
pwsh ./Git-ArchiveBranchDiffs.ps1 -leftBranch v1.0.0 -rightBranch mainIf PowerShell Core is not installed, the bash wrapper will download and install it automatically:
chmod +x ./Git-ArchiveBranchDiffs.sh
sudo bash ./Git-ArchiveBranchDiffs.shOr if pwsh is already installed:
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive| Parameter | Required | Default | Description |
|---|---|---|---|
-repositoryPath |
No | Current directory or auto-detected from subdirectory | Path to any directory inside a git repository |
-leftBranch |
No | Default remote branch (e.g., origin/main) |
Branch, tag, or commit ref for the left side of the diff. Also accepts the tokens WORKING and STAGED (see Uncommitted Tokens). |
-rightBranch |
No | Currently checked-out branch | Branch, tag, or commit ref for the right side of the diff. Also accepts WORKING / STAGED. |
-outputDirectory |
No | Prompted (interactive) or current directory | Where the ZIP file will be created |
-archiveFileName |
No | Auto-generated from branch names | Custom name for the ZIP file |
-nonInteractive |
No | $false |
Skip all prompts and use smart defaults |
-workingTree |
No | $false |
Compare uncommitted working tree changes against the left branch |
-staged |
No | $false |
Compare staged (indexed) changes against the left branch |
-threeWay |
No | $false |
Produce a three-way diff with base, left, and right directories |
-versionedName |
No | $false |
Include commit hashes and a version timestamp in the archive filename |
-patchContextLines |
No | 5 |
Number of context lines in CHANGES.patch (git diff -U<N>). Increase to 10–15 for deeper AI review. |
-noMergesInHistory |
No | $false |
Omit merge commits from HISTORY.md. Useful on PR-workflow branches where merge commits add noise. |
-fetch |
No | $false |
Fetch any remote-tracking ref named on either side before resolving — retrieves it if missing, refreshes it if stale. Targeted and non-destructive (updates refs/remotes/* only). |
-reveal |
No | $false |
After creating the archive, open the OS file manager with the archive selected (Explorer /select, Finder open -R; folder on Linux). Non-blocking; non-fatal if it can't launch. |
-open |
No | $false |
After creating the archive, open it with the default .zip handler. Combinable with -reveal. Non-blocking; non-fatal. |
-includeUntracked |
No | $false |
Include untracked (never-added) files in working-tree comparisons. Off by default. |
The tool compares two endpoints — each a committed ref or an uncommitted-state token:
Compares two committed refs (branches, tags, or commit hashes). The archive contains left and right directories with the differing files.
pwsh ./Git-ArchiveBranchDiffs.ps1 -leftBranch main -rightBranch feature/foo
# Tags work the same way
pwsh ./Git-ArchiveBranchDiffs.ps1 -leftBranch v1.0.0 -rightBranch v2.0.0Besides a branch, tag, or commit, either side accepts two special ref tokens that point at your uncommitted state. Git tracks your code in three snapshots, and these are how you bookend a comparison against the live, not-yet-committed ones:
| Token | Means | Git equivalent |
|---|---|---|
HEAD |
The last commit on the checked-out branch (your most recent save). | HEAD |
STAGED |
Changes you have git add-ed but not committed — the index. |
git diff --staged |
WORKING |
Files on disk right now — staged and unstaged edits together. | git diff |
The progression is: you edit files (WORKING) → git add (STAGED) → git commit (HEAD). Common pairings:
# Everything changed since the last commit (staged + unstaged) — the "what am I about to commit" view
pwsh ./Git-ArchiveBranchDiffs.ps1 -leftBranch HEAD -rightBranch WORKING
# Exactly what is staged for the next commit
pwsh ./Git-ArchiveBranchDiffs.ps1 -leftBranch HEAD -rightBranch STAGED
# What you've edited but have NOT staged yet
pwsh ./Git-ArchiveBranchDiffs.ps1 -leftBranch STAGED -rightBranch WORKINGAliases: WORKING = WORKTREE / WT; STAGED = INDEX / STG (all case-insensitive).
A token always means your live state. WORKING/STAGED reflect the one working tree / index in the folder, which sits on top of HEAD. Comparing a token to any base just runs git diff <base>, so pairing it with a base that isn't HEAD (e.g. origin/main when your branch is ahead of it) naturally shows everything between that base and your live files — the commits since the base plus your uncommitted edits. The tool prints a neutral note saying so; it's the intended "what does my work look like vs the base" review.
# What my branch + my uncommitted edits look like against the upstream base
pwsh ./Git-ArchiveBranchDiffs.ps1 -leftBranch origin/main -rightBranch WORKINGTo get a committed-only comparison (exclude your uncommitted edits), name HEAD instead of a token: origin/main ⟷ HEAD.
Untracked files (brand-new, never git add-ed) are not included by default; add -includeUntracked to archive them too.
Fetching a base that isn't local. If a base names a remote-tracking ref (origin/main, upstream/release) that's missing or stale, add -fetch to retrieve/refresh it first — a targeted, non-destructive git fetch that updates refs/remotes/* only (never your HEAD or working tree). In interactive mode a missing remote ref also prompts to fetch; in -nonInteractive mode the network is only touched when -fetch is given. Bare SHAs can't be auto-fetched.
pwsh ./Git-ArchiveBranchDiffs.ps1 -leftBranch origin/main -rightBranch WORKING -fetchThese switches still work and are shorthand for the tokens above:
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive -workingTree # = -rightBranch WORKING
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive -staged # = -rightBranch STAGEDWhen used without an explicit -leftBranch, the left side defaults to HEAD (so the comparison is your pure uncommitted delta). They are mutually exclusive with each other and with an explicit -rightBranch.
Produces a three-way diff with a base/ directory showing the merge-base, plus left and right directories showing what each side changed. Mutually exclusive with -workingTree and -staged.
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive -threeWay -leftBranch main -rightBranch feature/fooThe -nonInteractive switch enables fully scripted usage with no prompts. Smart defaults are applied for any parameter not explicitly provided:
| Parameter | Default When -nonInteractive |
|---|---|
-repositoryPath |
Auto-detected from current directory (works from any subdirectory) |
-leftBranch |
Default remote branch via git symbolic-ref |
-rightBranch |
Currently checked-out branch (falls back to HEAD on detached HEAD) |
-outputDirectory |
Current working directory |
-archiveFileName |
<leftBranch> ⟷ <rightBranch>.zip (or versioned, see below) |
When -versionedName is set, the auto-generated filename includes short commit hashes and a version timestamp derived from the commit date. This prevents overwrites when re-running the tool after new commits land on the same branches.
Format: {leftName} ⟷ {rightName} ({leftHash}..{rightHash} {Year}.{Quarter}.{MMdd}.{HHmm}).zip
Examples:
| Mode | Filename |
|---|---|
| Branch | main ⟷ f_my-feature (abc1234..def5678 2026.2.0413.1430).zip |
| Three-way | 3way main ⟷ f_my-feature (abc1234..def5678 2026.2.0413.1430).zip |
| Working tree | main ⟷ WORKING-TREE (abc1234..abc1234+wt 2026.2.0413.1502).zip |
| Staged | main ⟷ STAGED (abc1234..abc1234+stg 2026.2.0413.1502).zip |
The version uses the newer commit's date (adapted from the BuildVersion algorithm): Year.Quarter.MMdd.HHmm where Quarter = ⌈Month / 3⌉. An explicit -archiveFileName always takes precedence over versioned naming.
CHANGES.patch is produced with several enhancements over a plain git diff:
- Rename/copy detection —
--find-renames --find-copiesis always active, so renamed files appear as proper rename hunks (similarity index N%/rename from/rename to) instead of a delete + add pair. - Stat summary header — The patch opens with a
git diff --statblock listing each changed file and its line counts. This gives any reader an immediate overview before the first diff hunk and is safe forgit apply(which ignores pre-diff --gitlines). - Configurable context lines — Default is 5 lines of context (vs. git's built-in 3). Pass
-patchContextLines 10(or higher) when you want more surrounding code visible per hunk — useful for AI review. - Merge commit filter — Pass
-noMergesInHistoryto suppress merge commits fromHISTORY.md. On GitHub-flow repos where every PR lands as a merge commit, this removes the noise and shows only real work commits.
# Run from inside a git repo (any subdirectory) — all defaults
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive
# Override just the output directory
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive -outputDirectory /tmp
# Fully specified (nonInteractive prevents any prompts for missing values)
pwsh ./Git-ArchiveBranchDiffs.ps1 -nonInteractive -repositoryPath /c/myRepo -leftBranch main -rightBranch feature/foo -outputDirectory /tmpThe tool creates a ZIP archive with this structure:
archive.zip
├── leftBranch/ # Files from the left (base) branch
│ ├── src/
│ │ ├── App.cs # Original version of modified file
│ │ └── NewFeature.cs-added # Placeholder (file was added in right)
│ ├── OldName.cs-renamed-R095 # Placeholder showing original name before rename
│ └── Removed.cs # Original content of deleted file
│
├── rightBranch/ # Files from the right (feature) branch
│ ├── src/
│ │ ├── App.cs # Modified version
│ │ └── NewFeature.cs # New file content
│ ├── NewName.cs # File after rename
│ └── Removed.cs-deleted # Placeholder (file was deleted)
│
├── HISTORY.md # Commit log, churn summary, per-commit file lists
├── CHANGES.patch # Unified diff (git diff output)
│
└── manifest/ # Metadata generated by the tool
├── Δ leftBranch ⟷ rightBranch # Branch comparison info
└── commit# abc1234.manifest # Change summary and file list
3way archive.zip
├── base/ # Files at the merge-base commit
│ └── ...
├── leftBranch/ # Files from the left branch
│ └── ...
├── rightBranch/ # Files from the right branch
│ └── ...
├── HISTORY.md
├── CHANGES.patch
└── manifest/
└── ...
The history file includes:
- Churn Summary — top 10 files by insertions + deletions, with binary file detection
- Commit log — per-side commit lists with per-commit file breakdowns (capped at 200 commits per side)
A unified diff (git diff output) covering all changed files. To apply the patch:
# Strict — fails on any context mismatch
git apply CHANGES.patch
# Forgiving — skips hunks that don't apply cleanly
git apply --3way CHANGES.patch
# Outside a git repo
patch -p1 < CHANGES.patchPlaceholder files ensure both sides of the diff have a representative file for every change, so directory-diff tools can display them side-by-side.
| Git Status | Left Branch | Right Branch |
|---|---|---|
| Added | NewFile.cs-added (zero bytes) |
NewFile.cs (new content) |
| Deleted | OriginalFile.cs (original content) |
OriginalFile.cs-deleted (zero bytes) |
| Modified | File.cs (original content) |
File.cs (modified content) |
| Renamed | OldName.cs-renamed-R095 (original content) |
NewName.cs (same content, new name) |
| Copied | Original.cs (original content) |
CopiedFile.cs (copied content) |
For renames, the suffix is -renamed-R<NN>, where R<NN> is the raw git rename status code indicating the similarity percentage (e.g., -renamed-R095 = 95% similar).
When using PowerShell, tab completion is available for key parameters:
| Parameter | Completes To |
|---|---|
-repositoryPath |
Directories containing a .git folder |
-leftBranch |
Local and remote branch names, tags, and stash refs |
-rightBranch |
Local and remote branch names, tags, and stash refs |
# Type and press Tab to cycle through matching branches
./Git-ArchiveBranchDiffs.ps1 -leftBranch ma<Tab>
# Completes to: main
./Git-ArchiveBranchDiffs.ps1 -rightBranch feat<Tab>
# Completes to: feature/my-branch
# Tags and stashes also complete
./Git-ArchiveBranchDiffs.ps1 -leftBranch v1.<Tab>
# Completes to: v1.0, v1.1, etc.Note: Branch completion uses the repository specified by
-repositoryPath, or the current directory if not specified.
Extract the archive and open the left/right branch directories in a directory-diff tool:
| Tool | Platform | Notes |
|---|---|---|
| Beyond Compare | Windows, Linux, macOS | Excellent rename detection, can treat placeholder-suffixed files as comparable |
| Meld | Windows, Linux, macOS | Free and open-source |
| VS Code | Windows, Linux, macOS | Use with a folder-diff extension |
| WinMerge | Windows | Free, lightweight |
Ensure git is installed and on your PATH. Verify with git --version.
- Check branch name spelling:
git branch -ato list all branches - Remote-only branches need a fetch first:
git fetch origin - The tool will try the remote version (e.g.,
origin/branchName) if the local branch isn't found
The two refs resolve to the same commit hash. The tool exits early since there is nothing to compare. Verify you specified different branches.
The tool detected that rightBranch is an ancestor of leftBranch (i.e., left has the newer commits). This usually means the arguments are reversed — the left branch should be the base (e.g., main) and the right branch should be the feature branch.
When using -workingTree, there are no uncommitted changes to archive. Make some changes first, or use normal mode to compare committed branches.
When using -staged, there are no staged changes. Stage files with git add first.
The two branches have unrelated histories (no merge-base). This can happen with orphan branches or repos initialized separately. Use normal mode instead.
- Windows: Run PowerShell as Administrator
- Linux/macOS: Use
sudofor the bash wrapper (needed for PowerShell Core installation)
The branches may be identical. Verify differences exist: git diff --stat <leftBranch>...<rightBranch>
- Windows: PowerShell 5.1+ (built-in) or PowerShell Core 7.3.4+
- Linux/macOS: PowerShell Core 7.3.4+ (auto-installed by the bash wrapper)
- Check version:
$PSVersionTable.PSVersion