| param( |
| [string[]]$Remotes = @('space', 'space-show'), |
| [string]$SourceBranch = 'main', |
| [string]$TempBranch = '__space-sync-tmp', |
| [switch]$AutoStash = $true, |
| [switch]$IncludeUntracked = $true |
| ) |
|
|
| $ErrorActionPreference = 'Stop' |
|
|
| $excludePatterns = @( |
| 'public/readme-images/*.png', |
| 'src/audio/tracks/*.mp3' |
| ) |
|
|
| $hfReadmeFrontmatterPath = Join-Path $PSScriptRoot 'hf-readme-frontmatter.txt' |
|
|
| function Remove-ReadmeFrontmatter { |
| param( |
| [Parameter(Mandatory = $true)] |
| [string]$Content |
| ) |
|
|
| if (-not $Content.StartsWith("---`n") -and -not $Content.StartsWith("---`r`n")) { |
| return $Content |
| } |
|
|
| $normalized = $Content -replace "`r`n", "`n" |
| $lines = $normalized -split "`n" |
| if ($lines.Count -lt 3 -or $lines[0] -ne '---') { |
| return $Content |
| } |
|
|
| for ($i = 1; $i -lt $lines.Count; $i++) { |
| if ($lines[$i] -eq '---') { |
| $remaining = if ($i + 1 -lt $lines.Count) { |
| ($lines[($i + 1)..($lines.Count - 1)] -join "`n").TrimStart("`n") |
| } else { |
| '' |
| } |
|
|
| if ([string]::IsNullOrEmpty($remaining)) { |
| return '' |
| } |
|
|
| return $remaining |
| } |
| } |
|
|
| return $Content |
| } |
|
|
| function Update-SpaceReadme { |
| $readmePath = Join-Path (Get-Location) 'README.md' |
| if (-not (Test-Path $readmePath) -or -not (Test-Path $hfReadmeFrontmatterPath)) { |
| return |
| } |
|
|
| $frontmatter = [System.IO.File]::ReadAllText($hfReadmeFrontmatterPath).Trim() |
| $readmeBody = [System.IO.File]::ReadAllText($readmePath) |
| $strippedBody = (Remove-ReadmeFrontmatter -Content $readmeBody).TrimStart() |
| $updatedReadme = "$frontmatter`r`n`r`n$strippedBody" |
| [System.IO.File]::WriteAllText($readmePath, $updatedReadme) |
| } |
|
|
| function Invoke-Git { |
| param( |
| [Parameter(Mandatory = $true)] |
| [string[]]$Args, |
| [string]$ErrorMessage = 'Git command failed.' |
| ) |
|
|
| & git @Args |
| if ($LASTEXITCODE -ne 0) { |
| throw $ErrorMessage |
| } |
| } |
|
|
| function Get-TrimmedGitOutput { |
| param( |
| [Parameter(Mandatory = $true)] |
| [string[]]$Args, |
| [string]$ErrorMessage = 'Git command failed.' |
| ) |
|
|
| $output = (& git @Args) |
| if ($LASTEXITCODE -ne 0) { |
| throw $ErrorMessage |
| } |
| return ($output | Out-String).Trim() |
| } |
|
|
| function Test-GitRemoteExists { |
| param( |
| [Parameter(Mandatory = $true)] |
| [string]$Remote |
| ) |
|
|
| & git remote get-url $Remote *> $null |
| return $LASTEXITCODE -eq 0 |
| } |
|
|
| $resolvedRemotes = @($Remotes | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) |
| if ($resolvedRemotes.Count -eq 0) { |
| throw 'Error: no remotes specified.' |
| } |
|
|
| $missingRemotes = @($resolvedRemotes | Where-Object { -not (Test-GitRemoteExists $_) }) |
| if ($missingRemotes.Count -gt 0) { |
| throw "Error: remote not found: $($missingRemotes -join ', ')" |
| } |
|
|
| $current = Get-TrimmedGitOutput -Args @('branch', '--show-current') -ErrorMessage 'Failed to read current branch.' |
| if ($current -ne $SourceBranch) { |
| throw "Error: please checkout $SourceBranch first" |
| } |
|
|
| $status = Get-TrimmedGitOutput -Args @('status', '--porcelain') -ErrorMessage 'Failed to check working tree status.' |
| $workingTreeDirty = -not [string]::IsNullOrWhiteSpace($status) |
| $stashCreated = $false |
| $createdTempBranch = $false |
| $pushFailures = New-Object System.Collections.Generic.List[string] |
|
|
| try { |
| if ($workingTreeDirty) { |
| if (-not $AutoStash) { |
| throw 'Error: working tree not clean, please commit or stash first' |
| } |
|
|
| Write-Host 'Working tree is not clean. Auto-stashing changes...' |
| $stashArgs = @('stash', 'push', '-m', '__space_sync_auto__') |
| if ($IncludeUntracked) { |
| $stashArgs += '--include-untracked' |
| } |
| Invoke-Git -Args $stashArgs -ErrorMessage 'Failed to stash working tree.' |
| $stashCreated = $true |
| } |
|
|
| Invoke-Git -Args @('checkout', '--orphan', $TempBranch) -ErrorMessage 'Failed to create orphan temp branch.' |
| $createdTempBranch = $true |
|
|
| Update-SpaceReadme |
|
|
| Invoke-Git -Args @('add', '-A') -ErrorMessage 'Failed to stage files on temp branch.' |
|
|
| foreach ($pattern in $excludePatterns) { |
| & git rm -rf --cached --ignore-unmatch -- $pattern 2>$null |
| if ($LASTEXITCODE -ne 0) { |
| Write-Host "Warning: failed to exclude pattern: $pattern" |
| } |
| } |
|
|
| $head = Get-TrimmedGitOutput -Args @('log', $SourceBranch, '-1', "--format=%h %s") -ErrorMessage "Failed to get latest commit from $SourceBranch." |
| Invoke-Git -Args @('commit', '-m', "Sync from ${SourceBranch}: $head") -ErrorMessage 'Failed to create sync snapshot commit.' |
|
|
| foreach ($remote in $resolvedRemotes) { |
| Write-Host "Pushing to $remote..." |
| & git push $remote "${TempBranch}:main" --force |
| if ($LASTEXITCODE -eq 0) { |
| Write-Host " ✓ $remote pushed" |
| } else { |
| Write-Host " ✗ $remote push failed" |
| $pushFailures.Add($remote) | Out-Null |
| } |
| } |
|
|
| if ($pushFailures.Count -gt 0) { |
| throw "Push failed for remotes: $($pushFailures -join ', ')" |
| } |
| } |
| finally { |
| if ($createdTempBranch) { |
| & git checkout -f $SourceBranch | Out-Null |
| if ($LASTEXITCODE -ne 0) { |
| Write-Host "Warning: failed to switch back to $SourceBranch" |
| } |
|
|
| & git branch -D $TempBranch | Out-Null |
| if ($LASTEXITCODE -ne 0) { |
| Write-Host "Warning: failed to delete temp branch $TempBranch" |
| } |
| } |
|
|
| if ($stashCreated) { |
| Write-Host 'Restoring stashed changes...' |
| & git stash pop --index 'stash@{0}' | Out-Null |
| if ($LASTEXITCODE -ne 0) { |
| Write-Host 'Warning: failed to auto-restore stash. Recover it manually with: git stash list' |
| } |
| } |
| } |
|
|
| Write-Host 'Done!' |
|
|