Public/Sync-GitBranches.ps1

<#
.SYNOPSIS
    Fetches all remotes and fast-forwards local branches, warning about conflicts.
.DESCRIPTION
    Runs `git fetch --all --prune`, then for each local branch that tracks a
    remote, attempts a fast-forward merge. If a worktree for that branch has
    uncommitted or unstaged changes that overlap with incoming file changes,
    a warning is emitted instead of updating.
.EXAMPLE
    Sync-GitBranches
.EXAMPLE
    Sync-GitBranches -RepoPath C:\repos\myrepo.git
#>

function Sync-GitBranches {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()]
        [string] $RepoPath = (Get-Location).Path
    )

    $root = Get-GitRoot -Path $RepoPath

    if ($PSCmdlet.ShouldProcess($root, 'Fetch all remotes and fast-forward branches')) {
        Write-Verbose "Fetching all remotes in $root"
        git -C $root fetch --all --prune
        if ($LASTEXITCODE -ne 0) { throw "git fetch failed in '$root'" }

        $worktrees = Get-WorktreeList -RepoPath $root

        # Build a map of branch -> worktree path for quick lookup
        $wtByBranch = @{}
        foreach ($wt in $worktrees) {
            if ($wt.Branch -and -not $wt.IsBare) {
                $wtByBranch[$wt.Branch] = $wt.Path
            }
        }

        # Enumerate all local branches that have an upstream
        $branches = git -C $root branch --format '%(refname:short) %(upstream:short)' 2>$null |
            Where-Object { $_ -match '\S+ \S+' } |
            ForEach-Object {
                $parts = $_ -split ' ', 2
                [PSCustomObject]@{ Local = $parts[0]; Upstream = $parts[1] }
            }

        foreach ($b in $branches) {
            $local    = $b.Local
            $upstream = $b.Upstream

            # Check if fast-forward is possible
            $mergeBase = git -C $root merge-base $local $upstream 2>$null
            $upstreamSha = git -C $root rev-parse $upstream 2>$null
            $localSha    = git -C $root rev-parse $local    2>$null

            if ($localSha -eq $upstreamSha) {
                Write-Verbose "$local is already up to date."
                continue
            }

            if ($mergeBase -ne $localSha) {
                Write-Warning "'$local' has diverged from '$upstream' — skipping (manual merge required)"
                continue
            }

            # Check for dirty worktree that overlaps with incoming changes
            if ($wtByBranch.ContainsKey($local)) {
                $wtPath      = $wtByBranch[$local]
                $dirtyFiles  = git -C $wtPath status --porcelain 2>$null | ForEach-Object { ($_ -split '\s+', 2)[1] }
                if ($dirtyFiles) {
                    $changedFiles = git -C $root diff --name-only "$local...$upstream" 2>$null
                    $conflicts    = $dirtyFiles | Where-Object { $changedFiles -contains $_ }
                    if ($conflicts) {
                        Write-Warning "Skipping '$local': worktree at '$wtPath' has dirty files that conflict with incoming changes:"
                        $conflicts | ForEach-Object { Write-Warning " $_" }
                        continue
                    }
                    else {
                        Write-Warning "'$local' worktree has uncommitted changes (no file overlap with incoming — proceeding with branch update)"
                    }
                }
            }

            Write-Verbose "Fast-forwarding '$local' to '$upstream'"
            git -C $root fetch . "$($upstream):$local"
            if ($LASTEXITCODE -ne 0) {
                Write-Warning "Fast-forward failed for '$local'"
            }
            else {
                Write-Host "Updated: $local" -ForegroundColor Green
            }
        }
    }
}