git-loom 0.25.0

A Git CLI tool that weaves together multiple feature branches into integration branches
# git-loom completions for PowerShell
# Add to your $PROFILE: Invoke-Expression (&git-loom completions powershell | Out-String)

$_gitLoomCompleter = {
    param($wordToComplete, $commandAst, $cursorPosition)

    # Every subcommand, in the order `git-loom --help` groups them. Aliases that
    # are a distinct word (fixup, rm, ...) are listed next to the command they
    # stand for; the ones that merely abbreviate it (ci, sh, ...) still work but
    # are left out to keep the list readable.
    $commands = @(
        # Workflow
        @{ Name = 'init'; Description = 'Initialize a new integration branch tracking a remote' },
        @{ Name = 'update'; Description = 'Pull-rebase the integration branch and update submodules' },
        @{ Name = 'push'; Description = 'Push a feature branch to remote' },
        @{ Name = 'pr'; Description = 'Alias of push' },
        @{ Name = 'agent'; Description = 'Install the loom skill for AI agents' },
        # Staging
        @{ Name = 'add'; Description = 'Stage files using short IDs, paths, or zz for all' },
        # Commits
        @{ Name = 'commit'; Description = 'Create a commit on a feature branch' },
        @{ Name = 'fold'; Description = 'Amend, fixup, or move commits' },
        @{ Name = 'amend'; Description = 'Alias of fold' },
        @{ Name = 'fixup'; Description = 'Alias of fold' },
        @{ Name = 'mv'; Description = 'Alias of fold' },
        @{ Name = 'rub'; Description = 'Alias of fold' },
        @{ Name = 'absorb'; Description = 'Absorb working tree changes into originating commits' },
        @{ Name = 'split'; Description = 'Split a commit into two sequential commits' },
        @{ Name = 'swap'; Description = 'Swap two commits within the same sequence' },
        @{ Name = 'reword'; Description = 'Reword a commit message or rename a branch' },
        @{ Name = 'drop'; Description = 'Drop a change, a commit, or a branch from history' },
        @{ Name = 'rm'; Description = 'Alias of drop' },
        # Branches
        @{ Name = 'branch'; Description = 'Manage feature branches (create, merge, unmerge)' },
        @{ Name = 'switch'; Description = 'Switch to any branch for testing (without weaving)' },
        # Inspection
        @{ Name = 'status'; Description = 'Show the branch-aware status' },
        @{ Name = 'tui'; Description = 'Interactive status TUI (tree + diff, with actions)' },
        @{ Name = 'show'; Description = 'Show the diff and metadata for a commit (like git show)' },
        @{ Name = 'diff'; Description = 'Show a diff using short IDs (like git diff)' },
        @{ Name = 'trace'; Description = 'Show the latest command trace' },
        # Recovery
        @{ Name = 'continue'; Description = 'Resume a paused operation after resolving conflicts' },
        @{ Name = 'abort'; Description = 'Cancel a paused operation and restore original state' }
    )

    # Alias -> the command whose flags apply. Includes the abbreviations that
    # are not offered above, so typing one still completes its flags.
    $aliases = @{
        'up' = 'update'; 'pr' = 'push'; 'ci' = 'commit'
        'amend' = 'fold'; 'am' = 'fold'; 'fixup' = 'fold'; 'mv' = 'fold'; 'rub' = 'fold'
        'rw' = 'reword'; 'rm' = 'drop'; 'br' = 'branch'; 'sw' = 'switch'
        'sh' = 'show'; 'di' = 'diff'; 'c' = 'continue'; 'a' = 'abort'
    }

    $helpFlags = @(
        @{ Name = '-h'; Description = 'Show help information' },
        @{ Name = '--help'; Description = 'Show help information' }
    )

    # Only valid before the subcommand.
    $topFlags = @(
        @{ Name = '--no-color'; Description = 'Disable colored output' },
        @{ Name = '--theme'; Description = 'Color theme: auto, dark, or light' },
        @{ Name = '--version'; Description = 'Show version information' }
    ) + $helpFlags

    $themeValues = @('auto', 'dark', 'light')

    # Tokens already typed, minus the one under the cursor (it is a prefix).
    $tokens = @($commandAst.ToString() -split '\s+' | Where-Object { $_ -ne '' })
    if ($wordToComplete -and $tokens.Count -gt 1 -and $tokens[-1] -eq $wordToComplete) {
        $tokens = $tokens[0..($tokens.Count - 2)]
    }

    # `--theme <Tab>` completes its values.
    if ($tokens.Count -gt 0 -and $tokens[-1] -eq '--theme') {
        return $themeValues | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', 'Color theme')
        }
    }

    # Find the subcommand, skipping any global flag (and the value of --theme).
    $subcommand = $null
    $subIndex = -1
    for ($i = 1; $i -lt $tokens.Count; $i++) {
        if ($tokens[$i] -match '^-') {
            if ($tokens[$i] -eq '--theme') { $i++ }
            continue
        }
        $subcommand = $tokens[$i]
        $subIndex = $i
        break
    }

    # Nothing decided yet: offer the subcommands, or the global flags.
    if ($null -eq $subcommand) {
        $candidates = if ($wordToComplete -match '^-') { $topFlags } else { $commands }
        return $candidates | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Description)
        }
    }

    if ($aliases.ContainsKey($subcommand)) { $subcommand = $aliases[$subcommand] }

    $subFlags = @()
    switch ($subcommand) {
        'update' {
            $subFlags = @(
                @{ Name = '-y'; Description = 'Remove local branches whose upstream was deleted' },
                @{ Name = '--yes'; Description = 'Remove local branches whose upstream was deleted' }
            )
        }
        'push' {
            $subFlags = @(
                @{ Name = '--no-pr'; Description = 'Push without creating a PR or Gerrit review' },
                @{ Name = '-f'; Description = 'Push with --force instead of --force-with-lease' },
                @{ Name = '--force'; Description = 'Push with --force instead of --force-with-lease' }
            )
        }
        'add' {
            $subFlags = @(
                @{ Name = '-p'; Description = 'Interactively select hunks to stage' },
                @{ Name = '--patch'; Description = 'Interactively select hunks to stage' }
            )
        }
        'commit' {
            $subFlags = @(
                @{ Name = '-b'; Description = 'Target feature branch' },
                @{ Name = '--branch'; Description = 'Target feature branch' },
                @{ Name = '-m'; Description = 'Commit message' },
                @{ Name = '--message'; Description = 'Commit message' },
                @{ Name = '-p'; Description = 'Interactively select hunks to stage' },
                @{ Name = '--patch'; Description = 'Interactively select hunks to stage' }
            )
        }
        'fold' {
            $subFlags = @(
                @{ Name = '-c'; Description = 'Create a new branch from the source commit(s)' },
                @{ Name = '--create'; Description = 'Create a new branch from the source commit(s)' },
                @{ Name = '-p'; Description = 'Interactively select hunks to fold' },
                @{ Name = '--patch'; Description = 'Interactively select hunks to fold' },
                @{ Name = '--above'; Description = 'Move the source commit(s) above this commit' },
                @{ Name = '--below'; Description = 'Move the source commit(s) below this commit' },
                @{ Name = '--hunks'; Description = 'Hunk id to pick, once per id (needs --hunks-from)' },
                @{ Name = '--hunks-from'; Description = 'Fingerprint of the listing --hunks came from' }
            )
        }
        'absorb' {
            $subFlags = @(
                @{ Name = '-n'; Description = 'Show what would be absorbed without making changes' },
                @{ Name = '--dry-run'; Description = 'Show what would be absorbed without making changes' }
            )
        }
        'split' {
            $subFlags = @(
                @{ Name = '-m'; Description = 'Message for the first commit' },
                @{ Name = '--message'; Description = 'Message for the first commit' },
                @{ Name = '-p'; Description = 'Interactively pick hunks for the first commit' },
                @{ Name = '--patch'; Description = 'Interactively pick hunks for the first commit' },
                @{ Name = '--hunks'; Description = 'Hunk id to pick, once per id (needs --hunks-from)' },
                @{ Name = '--hunks-from'; Description = 'Fingerprint of the listing --hunks came from' }
            )
        }
        'reword' {
            $subFlags = @(
                @{ Name = '-m'; Description = 'New message or branch name' },
                @{ Name = '--message'; Description = 'New message or branch name' }
            )
        }
        'drop' {
            $subFlags = @(
                @{ Name = '-y'; Description = 'Skip confirmation prompt' },
                @{ Name = '--yes'; Description = 'Skip confirmation prompt' }
            )
        }
        'status' {
            $subFlags = @(
                @{ Name = '-f'; Description = 'Show files changed in each commit' },
                @{ Name = '--files'; Description = 'Show files changed in each commit' },
                @{ Name = '-a'; Description = 'Show all branches including hidden ones' },
                @{ Name = '--all'; Description = 'Show all branches including hidden ones' }
            )
        }
        'diff' {
            # Git's own options go after a `--`, so only loom's are listed.
            $subFlags = @(
                @{ Name = '--staged'; Description = 'Show staged changes (index vs HEAD)' },
                @{ Name = '--cached'; Description = 'Alias of --staged' },
                @{ Name = '-a'; Description = 'Show all changes, staged and unstaged' },
                @{ Name = '--all'; Description = 'Show all changes, staged and unstaged' }
            )
        }
        'agent' {
            # The sub-subcommand is the first non-flag token after `agent`.
            $agentSubcommand = $null
            for ($i = $subIndex + 1; $i -lt $tokens.Count; $i++) {
                if ($tokens[$i] -match '^-') { continue }
                $agentSubcommand = $tokens[$i]
                break
            }

            if ($null -eq $agentSubcommand -and -not ($wordToComplete -match '^-')) {
                $agentSubs = @(
                    @{ Name = 'init'; Description = 'Install the loom skill for an AI agent' }
                )
                return $agentSubs | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {
                    [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Description)
                }
            }

            if ($agentSubcommand -eq 'init') {
                $subFlags = @(
                    @{ Name = '--project'; Description = 'Install into the repository instead of the home directory' }
                )
            }
        }
        'branch' {
            # The sub-subcommand is the first non-flag token after `branch`.
            $branchSubcommand = $null
            for ($i = $subIndex + 1; $i -lt $tokens.Count; $i++) {
                if ($tokens[$i] -match '^-') { continue }
                $branchSubcommand = $tokens[$i]
                break
            }

            if ($null -eq $branchSubcommand -and -not ($wordToComplete -match '^-')) {
                $branchSubs = @(
                    @{ Name = 'new'; Description = 'Create a new feature branch' },
                    @{ Name = 'create'; Description = 'Alias of new' },
                    @{ Name = 'merge'; Description = 'Weave an existing branch into integration' },
                    @{ Name = 'unmerge'; Description = 'Remove a branch from integration' }
                )
                return $branchSubs | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {
                    [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Description)
                }
            }

            switch ($branchSubcommand) {
                'merge' {
                    $subFlags = @(
                        @{ Name = '-a'; Description = 'Also show remote branches' },
                        @{ Name = '--all'; Description = 'Also show remote branches' }
                    )
                }
                'unmerge' { $subFlags = @() }
                default {
                    # `branch`, `branch new` and `branch create` all take a target.
                    $subFlags = @(
                        @{ Name = '-t'; Description = 'Target commit, branch, or shortID' },
                        @{ Name = '--target'; Description = 'Target commit, branch, or shortID' }
                    )
                }
            }
        }
    }

    # Only offer flags once the user commits to one, so that plain words fall
    # back to PowerShell's file completion (commit, add, fold, ... take paths).
    if (-not ($wordToComplete -match '^-')) { return }

    $allFlags = $subFlags + $helpFlags
    $allFlags | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Description)
    }
}

$_gitLoomNames = @('git-loom') + @(Get-Alias -ErrorAction SilentlyContinue | Where-Object { $_.Definition -eq 'git-loom' } | Select-Object -ExpandProperty Name)
Register-ArgumentCompleter -Native -CommandName $_gitLoomNames -ScriptBlock $_gitLoomCompleter