Skip to content

Instantly share code, notes, and snippets.

@kasuken
Last active March 16, 2026 22:41
Show Gist options
  • Select an option

  • Save kasuken/e128dc563005d13a5065d1d98162eef6 to your computer and use it in GitHub Desktop.

Select an option

Save kasuken/e128dc563005d13a5065d1d98162eef6 to your computer and use it in GitHub Desktop.
A script to update your local GitHub repositories with the remote branches (and cleanup)
function Sync-GitHubWorkspace {
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$Path = (Get-Location).Path,
[string[]]$IncludeBranches = @("main", "master", "develop", "dev"),
[switch]$CleanBinObj,
[switch]$AutoStash
)
function Write-Section {
param([string]$Text)
Write-Host ""
Write-Host "----------------------------------------------------" -ForegroundColor DarkGray
Write-Host $Text -ForegroundColor Cyan
Write-Host "----------------------------------------------------" -ForegroundColor DarkGray
}
function Get-RepoPaths {
param([string]$RootPath)
Get-ChildItem -Path $RootPath -Directory -Force |
Where-Object {
(Test-Path (Join-Path $_.FullName ".git")) -or
(git -C $_.FullName rev-parse --is-inside-work-tree 2>$null)
} |
ForEach-Object { $_.FullName } |
Sort-Object -Unique
}
function Get-CurrentBranch {
param([string]$RepoPath)
$branch = git -C $RepoPath rev-parse --abbrev-ref HEAD 2>$null
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($branch) -or $branch -eq "HEAD") {
return $null
}
$branch.Trim()
}
function Get-UpstreamBranch {
param([string]$RepoPath)
$upstream = git -C $RepoPath rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>$null
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($upstream)) {
return $null
}
$upstream.Trim()
}
function Get-WorkingTreeState {
param([string]$RepoPath)
$statusLines = git -C $RepoPath status --porcelain 2>$null
$hasUntracked = $false
$hasTrackedChanges = $false
foreach ($line in $statusLines) {
if ($line -match '^\?\?') {
$hasUntracked = $true
}
else {
$hasTrackedChanges = $true
}
}
[PSCustomObject]@{
HasChanges = ($hasUntracked -or $hasTrackedChanges)
HasUntracked = $hasUntracked
HasTrackedChanges = $hasTrackedChanges
}
}
function Get-BehindAhead {
param(
[string]$RepoPath,
[string]$Upstream
)
$result = git -C $RepoPath rev-list --left-right --count "HEAD...$Upstream" 2>$null
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($result)) {
return $null
}
$parts = $result.Trim() -split '\s+'
if ($parts.Count -ne 2) {
return $null
}
[PSCustomObject]@{
Ahead = [int]$parts[0]
Behind = [int]$parts[1]
}
}
function Remove-BinObjFolders {
param([string]$RootPath)
Write-Section "Removing bin and obj folders"
$folders = Get-ChildItem -Path $RootPath -Directory -Recurse -Force |
Where-Object {
$_.Name -in @("bin", "obj") -and
$_.FullName -notmatch '[\\/]+node_modules([\\/]|$)'
} |
Sort-Object FullName -Descending
foreach ($folder in $folders) {
if ($PSCmdlet.ShouldProcess($folder.FullName, "Remove folder")) {
Write-Host "Removing: $($folder.FullName)" -ForegroundColor Red
Remove-Item -Path $folder.FullName -Force -Recurse -ErrorAction Continue
}
}
}
$results = New-Object System.Collections.Generic.List[object]
Write-Host "Starting workspace sync in: $Path" -ForegroundColor Green
$repos = Get-RepoPaths -RootPath $Path
foreach ($repo in $repos) {
Write-Section $repo
$result = [PSCustomObject]@{
Repository = $repo
Branch = $null
Status = $null
Details = $null
}
git -C $repo fetch --prune origin 2>$null
if ($LASTEXITCODE -ne 0) {
$result.Status = "FetchFailed"
$result.Details = "git fetch --prune origin failed"
Write-Host $result.Details -ForegroundColor Red
$results.Add($result)
continue
}
$branch = Get-CurrentBranch -RepoPath $repo
$result.Branch = $branch
if (-not $branch) {
$result.Status = "DetachedHead"
$result.Details = "Detached HEAD or branch not detected"
Write-Host $result.Details -ForegroundColor Yellow
$results.Add($result)
continue
}
Write-Host "Current branch: $branch" -ForegroundColor White
if ($IncludeBranches -notcontains $branch) {
$result.Status = "SkippedBranch"
$result.Details = "Not in allowed branches: $($IncludeBranches -join ', ')"
Write-Host "Not in allowed branches. Skipping." -ForegroundColor Yellow
$results.Add($result)
continue
}
$state = Get-WorkingTreeState -RepoPath $repo
if ($state.HasChanges -and -not $AutoStash) {
if ($state.HasUntracked) {
$result.Status = "SkippedUntracked"
$result.Details = "Untracked files present"
Write-Host "Untracked files present. Skipping." -ForegroundColor DarkYellow
}
elseif ($state.HasTrackedChanges) {
$result.Status = "SkippedDirty"
$result.Details = "Uncommitted changes present"
Write-Host "Uncommitted changes present. Skipping." -ForegroundColor DarkYellow
}
$results.Add($result)
continue
}
$stashCreated = $false
if ($state.HasChanges -and $AutoStash) {
Write-Host "Stashing local changes..." -ForegroundColor DarkYellow
git -C $repo stash push -u -m "Sync-GitHubWorkspace auto-stash" 2>$null
if ($LASTEXITCODE -eq 0) {
$stashCreated = $true
}
}
try {
$upstream = Get-UpstreamBranch -RepoPath $repo
if (-not $upstream) {
$result.Status = "NoUpstream"
$result.Details = "No upstream configured"
Write-Host "No upstream configured. Skipping." -ForegroundColor Yellow
$results.Add($result)
continue
}
Write-Host "Upstream: $upstream" -ForegroundColor Gray
$delta = Get-BehindAhead -RepoPath $repo -Upstream $upstream
if (-not $delta) {
$result.Status = "CompareFailed"
$result.Details = "Could not compare HEAD with upstream"
Write-Host $result.Details -ForegroundColor Red
$results.Add($result)
continue
}
if ($delta.Ahead -eq 0 -and $delta.Behind -eq 0) {
$result.Status = "UpToDate"
$result.Details = "Already up to date"
Write-Host "Already up to date." -ForegroundColor Green
$results.Add($result)
continue
}
if ($delta.Ahead -gt 0 -and $delta.Behind -eq 0) {
$result.Status = "Ahead"
$result.Details = "Local branch ahead by $($delta.Ahead) commit(s)"
Write-Host $result.Details -ForegroundColor Yellow
$results.Add($result)
continue
}
if ($delta.Ahead -gt 0 -and $delta.Behind -gt 0) {
$result.Status = "Diverged"
$result.Details = "Branch diverged. Ahead: $($delta.Ahead), Behind: $($delta.Behind)"
Write-Host $result.Details -ForegroundColor Red
$results.Add($result)
continue
}
if ($delta.Behind -gt 0) {
if ($PSCmdlet.ShouldProcess($repo, "Pull latest changes on $branch")) {
Write-Host "Pulling latest changes..." -ForegroundColor Green
git -C $repo pull --ff-only 2>$null
if ($LASTEXITCODE -eq 0) {
$result.Status = "Updated"
$result.Details = "Pulled $($delta.Behind) commit(s)"
Write-Host "Pull completed." -ForegroundColor Green
}
else {
$result.Status = "PullFailed"
$result.Details = "git pull --ff-only failed"
Write-Host $result.Details -ForegroundColor Red
}
}
else {
$result.Status = "WhatIf"
$result.Details = "Would pull $($delta.Behind) commit(s)"
}
$results.Add($result)
continue
}
}
finally {
if ($stashCreated) {
Write-Host "Restoring stashed changes..." -ForegroundColor DarkYellow
git -C $repo stash pop 2>$null | Out-Null
}
}
}
if ($CleanBinObj) {
Remove-BinObjFolders -RootPath $Path
}
Write-Section "Summary"
$results |
Sort-Object Status, Repository |
Format-Table -AutoSize Repository, Branch, Status, Details
return $results
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment