Public/GitSync.ps1

# PSSnips — Git repository sync for snippet store.
function Sync-SnipRepo {
    <#
    .SYNOPSIS
        Syncs the local snippet store directory with a remote git repository.
 
    .DESCRIPTION
        Sync-SnipRepo keeps a git-backed clone of the PSSnips snippet store in sync
        with a remote repository. It supports pull-only, push-only, bidirectional
        sync, and status-only modes.
 
        Configuration keys read via the PSSnips config system:
          SnipRepoUrl — remote repository URL
          SnipRepoDir — local clone path (default: <PSSnips home>/repo)
 
        If the local clone directory does not exist, the repository is cloned from
        the configured remote URL before any pull/push operations.
 
    .PARAMETER Remote
        Overrides the SnipRepoUrl config value for a single invocation. Use this
        for one-shot sync against a different remote without changing config.
 
    .PARAMETER Pull
        Pull remote changes into the local repository. Mutually exclusive with
        -Push and -Status.
 
    .PARAMETER Push
        Stage all local changes, commit with an auto-generated message, and push
        to the remote. Mutually exclusive with -Pull and -Status.
 
    .PARAMETER Status
        Show the output of `git status` without modifying the repository. When
        this switch is present, -Pull and -Push are ignored.
 
    .EXAMPLE
        Sync-SnipRepo
 
        Performs a bidirectional sync: pull remote changes then push local changes.
 
    .EXAMPLE
        Sync-SnipRepo -Remote 'https://github.com/user/snips.git' -Pull
 
        Pulls from the specified remote URL without changing the stored config.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        PSCustomObject with properties:
          Status [string] — 'Pulled', 'Pushed', 'Both', or 'StatusOnly'
          Branch [string] — current branch name
          Changes [int] — number of changed files reported by git status --short
 
    .NOTES
        Requires git in PATH. Install from https://git-scm.com if missing.
        All git output is forwarded to the verbose stream.
        Write operations are gated by ShouldProcess (supports -WhatIf/-Confirm).
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [string]$Remote = '',
        [switch]$Pull,
        [switch]$Push,
        [switch]$Status
    )

    # Verify git is available
    if (-not (Get-Command 'git' -ErrorAction SilentlyContinue)) {
        Write-Error 'git not found in PATH. Install Git from https://git-scm.com'
        return
    }

    $cfg     = script:LoadCfg
    $url     = if ($Remote) { $Remote } else { $cfg['SnipRepoUrl'] }
    $repoDir = if ($cfg['SnipRepoDir']) { $cfg['SnipRepoDir'] } else { Join-Path $script:Home 'repo' }

    # Clone if local directory does not exist
    if (-not (Test-Path $repoDir)) {
        if (-not $url) {
            script:Out-Err 'SnipRepoUrl is not configured. Set it with Set-SnipConfig or pass -Remote.'
            return
        }
        if ($PSCmdlet.ShouldProcess($repoDir, "Clone repository from $url")) {
            script:Out-Info "Cloning $url -> $repoDir"
            $cloneOut = & git clone $url $repoDir 2>&1
            $cloneOut | ForEach-Object { Write-Verbose $_ }
            if ($LASTEXITCODE -ne 0) {
                script:Out-Err "git clone failed (exit $LASTEXITCODE)"
                return
            }
        } else {
            return
        }
    }

    # Determine current branch
    $branchOut = & git -C $repoDir rev-parse --abbrev-ref HEAD 2>&1
    $branch    = if ($LASTEXITCODE -eq 0) { ($branchOut | Out-String).Trim() } else { 'unknown' }

    # Count changed files
    $shortOut = & git -C $repoDir status --short 2>&1
    $shortOut | ForEach-Object { Write-Verbose $_ }
    $changes  = @($shortOut | Where-Object { $_ -match '\S' }).Count

    # -Status: report only
    if ($Status) {
        $shortOut | ForEach-Object { script:Out-Info $_ }
        return [PSCustomObject]@{
            Status  = 'StatusOnly'
            Branch  = $branch
            Changes = $changes
        }
    }

    # Resolve mode: default is bidirectional
    $doPull = $Pull -or (-not $Pull -and -not $Push)
    $doPush = $Push -or (-not $Pull -and -not $Push)

    $statusLabel = if ($doPull -and $doPush) { 'Both' } elseif ($doPull) { 'Pulled' } else { 'Pushed' }

    if ($doPull) {
        if ($PSCmdlet.ShouldProcess($repoDir, 'git pull')) {
            $pullOut = & git -C $repoDir pull 2>&1
            $pullOut | ForEach-Object { Write-Verbose $_ }
            if ($LASTEXITCODE -ne 0) {
                script:Out-Err "git pull failed (exit $LASTEXITCODE)"
            } else {
                script:Out-OK 'Pull complete.'
            }
        }
    }

    if ($doPush) {
        if ($PSCmdlet.ShouldProcess($repoDir, 'git add / commit / push')) {
            $addOut = & git -C $repoDir add -A 2>&1
            $addOut | ForEach-Object { Write-Verbose $_ }

            $msg       = "PSSnips sync $(Get-Date -f 'yyyy-MM-dd HH:mm')"
            $commitOut = & git -C $repoDir commit -m $msg 2>&1
            $commitOut | ForEach-Object { Write-Verbose $_ }

            $pushOut = & git -C $repoDir push 2>&1
            $pushOut | ForEach-Object { Write-Verbose $_ }
            if ($LASTEXITCODE -ne 0) {
                script:Out-Err "git push failed (exit $LASTEXITCODE)"
            } else {
                script:Out-OK 'Push complete.'
            }
        }
    }

    # Refresh changed-file count after operations
    $shortAfter = & git -C $repoDir status --short 2>&1
    $changes    = @($shortAfter | Where-Object { $_ -match '\S' }).Count

    return [PSCustomObject]@{
        Status  = $statusLabel
        Branch  = $branch
        Changes = $changes
    }
}