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 { $_.Substring(3) } 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 } } } } |