g-branch.ps1

[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidateSet('create','rename','checkout','sync','base','fork-sync')]
    [string]$Action,

    [Parameter(ValueFromPipeline)]
    [string]$Name = "",

    [switch]$Force,
    [switch]$Stack,
    [switch]$NoStashPop
)

begin {
    . (Join-Path $PSScriptRoot 'g-registry.ps1')
}

process {
    $repo       = Get-Location
    $branch     = git -C $repo branch --show-current 2>$null
    if (-not $branch) { Write-Host "not a git repo"; exit 1 }
    $cfg        = Get-GitboxConfig -RepoPath $repo
    $baseBranch = $cfg.BaseBranch

    switch ($Action) {

        'create' {
            if ($Name -notmatch '^[a-zA-Z0-9][a-zA-Z0-9/_\-\.]*$') {
                Write-Host "invalid branch name: $Name"; exit 1
            }
            $existsLocal  = git -C $repo branch --list $Name 2>$null
            $existsRemote = git -C $repo branch -r --list "origin/$Name" 2>$null
            if ($existsLocal -or $existsRemote) {
                Write-Host "branch '$Name' already exists"
                if (-not $Force) {
                    $confirm = $null
                    try { $confirm = Read-Host "check it out? [y/N]" } catch { }
                    if ($confirm -notmatch '^[yY]$') { exit 1 }
                }
                git -C $repo checkout $Name 2>&1 | Out-Null
                Write-Host "checked out $Name"; exit 0
            }
            $parentBranch = $baseBranch
            if ($Stack) {
                $parentBranch = $branch
            } else {
                if ($branch -ne $baseBranch) {
                    if ($branch -match '^wip/') {
                        $coOut = git -C $repo checkout $baseBranch 2>&1
                        if ($LASTEXITCODE -ne 0) { Write-Host "checkout $baseBranch failed: $($coOut -join ' ')"; exit 1 }
                    } else {
                        Write-Host "must be on base branch ($baseBranch); currently on '$branch'"; exit 1
                    }
                }
                $pullOut = git -C $repo pull origin $baseBranch 2>&1
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "pull failed"
                    if ($VerbosePreference -ne 'SilentlyContinue') { $pullOut | ForEach-Object { Write-Host " $_" } }
                    exit 1
                }
            }
            $checkoutOut = git -C $repo checkout -b $Name 2>&1
            if ($LASTEXITCODE -ne 0) { Write-Host "branch create failed: $($checkoutOut -join ' ')"; exit 1 }
            if ($Stack -and $cfg.MergeStrategy -eq 'squash') {
                Write-Host " warning: MergeStrategy=squash causes rebase conflicts during 'gitbox n' -- set MergeStrategy to 'merge' for stacked PRs"
            }
            Write-Host "created $Name from $parentBranch"
        }

        'rename' {
            if ($Name -notmatch '^[a-zA-Z0-9][a-zA-Z0-9/_\-\.]*$') {
                Write-Host "invalid branch name: $Name"; exit 1
            }
            if ($branch -eq $baseBranch) { Write-Host "rename-abort: cannot rename base branch '$branch'"; exit 1 }
            if ($branch -notlike 'wip/*') { Write-Host "warning: current branch '$branch' is not a wip/ branch" }
            $renameOut = git -C $repo branch -m $Name 2>&1
            if ($LASTEXITCODE -ne 0) { Write-Host "rename failed: $($renameOut -join ' ')"; exit 1 }
            git -C $repo rev-parse --verify "origin/$branch" 2>$null | Out-Null
            $hadRemote = ($LASTEXITCODE -eq 0)
            $pushOut = git -C $repo push origin -u $Name 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Host "push failed: branch renamed locally to '$Name' but not pushed"
                if ($VerbosePreference -ne 'SilentlyContinue') { $pushOut | ForEach-Object { Write-Host " $_" } }
                exit 1
            }
            if ($hadRemote) {
                $delOut = git -C $repo push origin --delete $branch 2>&1
                if ($LASTEXITCODE -ne 0) { Write-Host " warning: remote branch delete failed: $($delOut -join ' ')" }
            }
            Write-Host "renamed $branch -> $Name"
        }

        'checkout' {
            if ($branch -eq $Name) { Write-Host "already on $Name"; exit 0 }
            $stashed = $false
            if (@(git -C $repo status --porcelain 2>$null | Where-Object { $_ }).Count -gt 0) {
                $stashOut = git -C $repo stash push -m 'gitbox-checkout' 2>&1
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "stash failed"
                    if ($VerbosePreference -ne 'SilentlyContinue') { $stashOut | ForEach-Object { Write-Host " $_" } }
                    exit 1
                }
                $stashed = $true
            }
            $coOut = git -C $repo checkout $Name 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Host "checkout $Name failed"
                if ($VerbosePreference -ne 'SilentlyContinue') { $coOut | ForEach-Object { Write-Host " $_" } }
                if ($stashed) { git -C $repo stash pop 2>$null | Out-Null }
                exit 1
            }
            if ($stashed) {
                $popOut = git -C $repo stash pop 2>&1
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "warning: stash pop failed -- run: git stash pop"
                    if ($VerbosePreference -ne 'SilentlyContinue') { $popOut | ForEach-Object { Write-Host " $_" } }
                }
            }
            Write-Host "on $Name"
        }

        'sync' {
            if ($branch -eq $baseBranch) { Write-Host "already on base branch; run: git pull origin $baseBranch"; exit 1 }
            $fetchOut = git -C $repo fetch origin $baseBranch 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Host "fetch failed"
                if ($VerbosePreference -ne 'SilentlyContinue') { $fetchOut | ForEach-Object { Write-Host " $_" } }
                exit 1
            }
            $behind = (git -C $repo rev-list "HEAD..origin/${baseBranch}" 2>$null | Measure-Object -Line).Lines
            if ($behind -eq 0) { Write-Host "already up to date with origin/$baseBranch"; exit 0 }
            $stashed = $false
            if (@(git -C $repo status --porcelain 2>$null | Where-Object { $_ }).Count -gt 0) {
                $stashOut = git -C $repo stash push -m 'gitbox-sync' 2>&1
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "stash failed"
                    if ($VerbosePreference -ne 'SilentlyContinue') { $stashOut | ForEach-Object { Write-Host " $_" } }
                    exit 1
                }
                $stashed = $true
            }
            $rebaseOut = git -C $repo rebase "origin/$baseBranch" 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Host "rebase conflict: resolve manually then run: git rebase --continue"
                if ($VerbosePreference -ne 'SilentlyContinue') { $rebaseOut | ForEach-Object { Write-Host " $_" } }
                git -C $repo rebase --abort 2>$null | Out-Null
                Write-Host "rebase aborted; working tree restored"
                if ($stashed) { git -C $repo stash pop 2>$null | Out-Null }
                exit 1
            }
            if ($stashed) {
                $popOut = git -C $repo stash pop 2>&1
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "warning: stash pop after rebase failed -- run: git stash pop"
                    if ($VerbosePreference -ne 'SilentlyContinue') { $popOut | ForEach-Object { Write-Host " $_" } }
                }
            }
            $ahead = (git -C $repo rev-list "origin/${baseBranch}..HEAD" 2>$null | Measure-Object -Line).Lines
            Write-Host "synced $branch onto origin/$baseBranch |+$ahead ahead |0 behind"
        }

        'base' {
            if ($branch -eq $baseBranch) { Write-Host "already on $baseBranch"; exit 0 }
            $stashed = $false
            if (@(git -C $repo status --porcelain 2>$null | Where-Object { $_ }).Count -gt 0) {
                $stashOut = git -C $repo stash push -m 'gitbox-base' 2>&1
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "stash failed"
                    if ($VerbosePreference -ne 'SilentlyContinue') { $stashOut | ForEach-Object { Write-Host " $_" } }
                    exit 1
                }
                $stashed = $true
            }
            $coOut = git -C $repo checkout $baseBranch 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Host "checkout $baseBranch failed"
                if ($VerbosePreference -ne 'SilentlyContinue') { $coOut | ForEach-Object { Write-Host " $_" } }
                if ($stashed) { git -C $repo stash pop 2>$null | Out-Null }
                exit 1
            }
            $pullOut = git -C $repo pull origin $baseBranch 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Host "pull $baseBranch failed"
                if ($VerbosePreference -ne 'SilentlyContinue') { $pullOut | ForEach-Object { Write-Host " $_" } }
                if ($stashed) { git -C $repo stash pop 2>$null | Out-Null }
                exit 1
            }
            if ($stashed) {
                if ($NoStashPop) {
                    Write-Host " stash preserved -- run: git stash pop to restore changes"
                } else {
                    $popOut = git -C $repo stash pop 2>&1
                    if ($LASTEXITCODE -ne 0) {
                        Write-Host "warning: stash pop failed -- run: git stash pop"
                        if ($VerbosePreference -ne 'SilentlyContinue') { $popOut | ForEach-Object { Write-Host " $_" } }
                    }
                }
            }
            Write-Host "on $baseBranch | pulled origin/$baseBranch"
        }

        'fork-sync' {
            if (-not $cfg.Upstream) {
                Write-Host "fork-sync requires Upstream in .gitbox.json -- run: gitbox fork <owner/repo>"; exit 1
            }
            $upstreamUrl = git -C $repo remote get-url upstream 2>$null
            if (-not $upstreamUrl) {
                Write-Host "upstream remote not found -- run: git remote add upstream https://github.com/$($cfg.Upstream).git"; exit 1
            }
            $fetchOut = git -C $repo fetch upstream $baseBranch 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Host "fetch upstream failed"
                if ($VerbosePreference -ne 'SilentlyContinue') { $fetchOut | ForEach-Object { Write-Host " $_" } }
                exit 1
            }
            $behind = (git -C $repo rev-list "origin/${baseBranch}..upstream/${baseBranch}" 2>$null | Measure-Object -Line).Lines
            if ($behind -eq 0) { Write-Host "already up to date with upstream/$baseBranch"; exit 0 }
            $stashed = $false
            if ($branch -ne $baseBranch -and @(git -C $repo status --porcelain 2>$null | Where-Object { $_ }).Count -gt 0) {
                $stashOut = git -C $repo stash push -m 'gitbox-fork-sync' 2>&1
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "stash failed"
                    if ($VerbosePreference -ne 'SilentlyContinue') { $stashOut | ForEach-Object { Write-Host " $_" } }
                    exit 1
                }
                $stashed = $true
            }
            if ($branch -ne $baseBranch) {
                $coOut = git -C $repo checkout $baseBranch 2>&1
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "checkout $baseBranch failed"
                    if ($VerbosePreference -ne 'SilentlyContinue') { $coOut | ForEach-Object { Write-Host " $_" } }
                    if ($stashed) { git -C $repo stash pop 2>$null | Out-Null }
                    exit 1
                }
            }
            $mergeOut = git -C $repo merge --ff-only "upstream/$baseBranch" 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Host "fork-sync failed: $baseBranch cannot fast-forward to upstream/$baseBranch"
                Write-Host " hint: $baseBranch has commits not in upstream -- rebase manually then retry"
                if ($VerbosePreference -ne 'SilentlyContinue') { $mergeOut | ForEach-Object { Write-Host " $_" } }
                if ($branch -ne $baseBranch) { git -C $repo checkout $branch 2>$null | Out-Null }
                if ($stashed) { git -C $repo stash pop 2>$null | Out-Null }
                exit 1
            }
            $pushOut = git -C $repo push origin $baseBranch 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Host "push origin/$baseBranch failed"
                if ($VerbosePreference -ne 'SilentlyContinue') { $pushOut | ForEach-Object { Write-Host " $_" } }
                if ($branch -ne $baseBranch) { git -C $repo checkout $branch 2>$null | Out-Null }
                if ($stashed) { git -C $repo stash pop 2>$null | Out-Null }
                exit 1
            }
            if ($branch -ne $baseBranch) { git -C $repo checkout $branch 2>$null | Out-Null }
            if ($stashed) {
                $popOut = git -C $repo stash pop 2>&1
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "warning: stash pop failed -- run: git stash pop"
                    if ($VerbosePreference -ne 'SilentlyContinue') { $popOut | ForEach-Object { Write-Host " $_" } }
                }
            }
            Write-Host "synced origin/$baseBranch |+$behind from upstream/$baseBranch |pushed fork"
        }
    }
}