Tools 2025-06-15

Introducing ggc — A Go-Based Git Tool

A complete walkthrough of ggc: the CLI/interactive split architecture, fuzzy-search engine implementation, Workflow Mode internals, customisable aliases, and cross-platform keybinding profiles.

Read in: ja
Introducing ggc — A Go-Based Git Tool

Introducing ggc — A Go-Based Git Tool

What is ggc?

ggc is a Git workflow tool written in Go. It wraps everyday Git sub-commands under a consistent surface and adds an interactive fuzzy-search TUI so you can find and execute commands without memorising their exact names. It includes Workflow Mode, customisable aliases with placeholder support, and a layered keybinding profile system — this article covers them all. ggc is listed on Awesome Go.

Demo

Branch Management CLI Workflow Interactive Overview
Branch management demo CLI workflow demo Interactive overview demo

Why use ggc?

No memorising required

Git ships hundreds of subcommands. Mid-task, pausing to recall "what was the flag for amend without editing the message?" or "how do I reference stash@{2} again?" is a common friction point. In ggc's interactive mode, typing bd instantly narrows to branch delete, ca to commit amend, and ss to stash show. The discovery cost drops to zero.

Representative use cases

Daily add → commit → push: Queue all three commands in Workflow Mode and press x to run them. Placeholder tokens such as <message> pause and prompt you at runtime, so you are never retyping the same workflow just to change the commit message.

Branch housekeeping: branch checkout remote creates a local tracking branch from a remote in one step; branch delete merged bulk-removes merged local branches. ggc surfaces these composite operations as first-class named commands rather than flags to look up.

Fixup workflow: Register commit fixup <commit>rebase autosquash as a sequence alias. One ggc fixup <sha> invocation applies the fixup commit.

Hook management: hook list / hook enable / hook disable let you manage project Git hooks without touching the file system directly.

How ggc compares to other tools

Tool Mode Strength vs ggc
git (bare) CLI Full power, fine-grained flags Requires memorising command names and flags
lazygit TUI only Rich panel UI Hard to call from scripts
tig TUI (read-only) Log / diff visualisation No mutation operations
gh CLI GitHub integration Not a local Git tool
git aliases CLI Fully custom No discoverability; must write everything yourself

ggc's key advantage is free switching between CLI and TUI. Scripts call ggc push force directly; interactive sessions run ggc for the fuzzy TUI. Both paths hit the same Route() implementation, so behaviour is identical.

CLI/Interactive Architecture

ggc has two execution paths: the CLI path for direct commands and the interactive path for the full-screen TUI. The Execute() method in cmd/execute.go is the single entry point that decides which path to take:

func (c *Cmd) Execute(args []string) error {
    if len(args) == 0 {
        c.Interactive()
        return nil
    }

    cmdName, cmdArgs := args[0], args[1:]

    // Check if this is an alias
    if c.configManager != nil && c.configManager.GetConfig().IsAlias(cmdName) {
        return c.executeAlias(cmdName, cmdArgs)
    }

    // Regular command
    return c.Route(args)
}

When no arguments arrive, Interactive() launches the TUI. When the caller provides an alias name, executeAlias() resolves and runs it. Everything else goes to Route(), which dispatches to individual Git command handlers. The CLI path, the alias expander, and the Workflow executor all share the same Route() function.

Interactive Mode Design

The interactive TUI splits into two sub-modes that share a single terminal screen, toggled with Ctrl+t.

Search Mode is the default. It shows a list of all ggc commands and updates the filtered list as you type. The matchPattern() function in internal/interactive/fuzzy.go handles the scoring:

func matchPattern(textRunes, patternRunes []rune) (bool, matchMetadata) {
    meta := matchMetadata{firstIndex: -1, lastIndex: -1}
    textIdx := 0
    patternIdx := 0

    for textIdx < len(textRunes) && patternIdx < len(patternRunes) {
        if textRunes[textIdx] == patternRunes[patternIdx] {
            if meta.firstIndex == -1 {
                meta.firstIndex = textIdx
            }
            if meta.lastIndex != -1 {
                meta.gapScore += textIdx - meta.lastIndex - 1
            }
            meta.lastIndex = textIdx
            patternIdx++
        }
        textIdx++
    }

    if patternIdx != len(patternRunes) {
        return false, meta
    }
    return true, meta
}

The algorithm is a classic subsequence fuzzy matcher: a pattern matches if all its characters appear in the text in order, but not necessarily consecutively. The matchMetadata struct accumulates three signals — firstIndex (where the match starts), gapScore (total inter-character gaps), and lastIndex — which combine into a matchScore for sorting. A tighter, earlier match ranks higher than a sparse, late one.

Workflow Mode

Pressing Ctrl+t calls ToggleWorkflowView() in internal/interactive/ui_mode.go:

func (ui *UI) ToggleWorkflowView() {
    if ui.state.IsWorkflowMode() {
        ui.enterSearchMode()
        return
    }
    ui.enterWorkflowMode()
}

In Workflow Mode you can build up a sequence of ggc commands (e.g. addcommitpush) by pressing Tab on any item in the search list. When you press x, the WorkflowExecutor.Execute() method runs them in order:

func (we *WorkflowExecutor) Execute(workflow *Workflow) error {
    steps := workflow.GetSteps()
    if len(steps) == 0 {
        return fmt.Errorf("workflow is empty")
    }

    for i, step := range steps {
        resolvedArgs, canceled := resolveStepPlaceholders(we.ui, step)
        if canceled {
            return ErrWorkflowCanceled
        }

        parts := append([]string{step.Command}, resolvedArgs...)
        if err := we.router.Route(parts); err != nil {
            return fmt.Errorf("step %d/%d failed: %w", i+1, len(steps), err)
        }
    }
    return nil
}

Before each step, resolveStepPlaceholders() scans the step arguments for <name> tokens and prompts you interactively if any turn up. A workflow step like commit -m <message> will pause and ask for the commit message at runtime.

Available Commands

A full reference of every ggc command. All of these can be called directly from the CLI or discovered via the interactive fuzzy-search UI using the same names.

Command Description
add . Add all changes to the index
add <file> Add a specific file to the index
add interactive Add changes interactively
add patch Add changes interactively (patch mode)
help Show main help message
help <command> Show help for a specific command
reset Hard reset to origin/ and clean working directory
reset hard <commit> Hard reset to specified commit
reset soft <commit> Soft reset: move HEAD but keep changes staged
branch checkout Switch to an existing branch
branch checkout remote Create and checkout a local branch from the remote
branch contains <commit> Show branches containing a commit
branch create Create and checkout a new branch
branch current Show current branch name
branch delete Delete local branch
branch delete merged Delete local merged branches
branch info <branch> Show detailed branch information
branch list local List local branches
branch list remote List remote branches
branch list verbose Show detailed branch listing
branch move <branch> <commit> Move branch to specified commit
branch rename <old> <new> Rename a branch
branch set upstream <branch> <upstream> Set upstream for a branch
branch sort [date|name] List branches sorted by date or name
commit <message> Create commit with a message
commit allow empty Create an empty commit
commit amend Amend previous commit (editor)
commit amend no-edit Amend without editing commit message
commit fixup <commit> Create a fixup commit targeting <commit>
log graph Show log with graph
log simple Show simple historical log
fetch Fetch from the remote
fetch prune Fetch and clean stale references
pull current Pull current branch from remote
pull rebase Pull and rebase
push current Push current branch to remote
push force Force push current branch
remote add <name> <url> Add remote repository
remote list List all remote repositories
remote remove <name> Remove remote repository
remote set-url <name> <url> Change remote URL
status Show working tree status
status short Show concise status (porcelain format)
clean dirs Clean untracked directories
clean files Clean untracked files
clean interactive Clean files interactively
restore . Restore all files in working directory from index
restore <commit> <file> Restore file from specific commit
restore <file> Restore file in working directory from index
restore staged . Unstage all files
restore staged <file> Unstage file (restore from HEAD to index)
diff Show changes (git diff HEAD)
diff head Alias for default diff against HEAD
diff staged Show staged changes
diff unstaged Show unstaged changes
tag annotated <tag> <message> Create annotated tag
tag create <tag> Create tag
tag delete <tag> Delete tag
tag list List all tags
tag push Push tags to remote
tag show <tag> Show tag information
config get <key> Get a specific config value
config list List all configuration
config set <key> <value> Set a configuration value
hook disable <hook> Disable a hook
hook edit <hook> Edit a hook's contents
hook enable <hook> Enable a hook
hook install <hook> Install a hook
hook list List all hooks
hook uninstall <hook> Uninstall an existing hook
rebase <upstream> Rebase current branch onto <upstream>
rebase abort Abort an in-progress rebase
rebase autosquash Interactive rebase with --autosquash
rebase continue Continue an in-progress rebase
rebase interactive Interactive rebase
rebase skip Skip current patch and continue
stash Stash current changes
stash apply Apply stash without removing it
stash apply <stash> Apply specific stash without removing it
stash branch <branch> Create branch from stash
stash branch <branch> <stash> Create branch from specific stash
stash clear Remove all stashes
stash create Create stash and return object name
stash drop Remove the latest stash
stash drop <stash> Remove specific stash
stash list List all stashes
stash pop Apply and remove the latest stash
stash pop <stash> Apply and remove specific stash
stash push Save changes to new stash
stash push -m <message> Save changes to new stash with message
stash save <message> Save changes to new stash with message
stash show Show changes in stash
stash show <stash> Show changes in specific stash
stash store <object> Store stash object
debug-keys Show current keybindings
debug-keys raw Capture key sequences interactively
debug-keys raw <file> Capture key sequences and save to a file
quit Exit interactive mode
version Display current ggc version

Other Features

Config-Based Workflows

Separate from aliases, the workflows: section of ~/.ggcconfig.yaml lets you pre-register workflows that load automatically into Workflow Mode when the TUI starts. They sit alongside any workflows you build interactively with Tab.

workflows:
  daily:
    - "add ."
    - "commit -m <message>"
    - "push current"
  deploy:
    - "branch checkout <branch>"
    - "pull current"
    - "push <branch>"

Any <name> token in a step is prompted interactively at execution time. The key difference from aliases (shorthand invoked from the CLI) is that config-based workflows are multi-step sequences that live in Workflow Mode and are reusable across sessions.

Command Aliases

ggc defines aliases in ~/.ggcconfig.yaml. Both simple and sequence formats work:

aliases:
  br: branch          # simple — ggc br list
  ci: commit

  quick:              # sequence
    - status
    - add .
    - commit

  deploy:             # sequence with placeholders
    - "branch checkout {0}"
    - "pull current"
    - "push {0}"

Keybinding Profiles

The interactive UI supports four built-in keybinding profiles (default, emacs, vi, readline). Configuration resolves in six layers — defaults → profile → platform → terminal → user config → environment overrides:

interactive:
  profile: emacs
  keybindings:
    move_up: "ctrl+p"
    move_down: "ctrl+n"
    toggle_workflow_view: "ctrl+t"
    add_to_workflow: "tab"

Cross-Platform Builds

ggc distributes pre-built binaries for Linux, macOS, and Windows (amd64 / arm64 / 386) via GoReleaser. All interactive TUI features work on all three platforms.

Shell Completion

ggc ships completion scripts for Bash, Zsh, and Fish. Pre-built scripts live in tools/completions/ and can be regenerated from the command registry with make completions.

# Bash (add to ~/.bash_profile or ~/.bashrc)
if [ -f ~/.ggc-completion.bash ]; then
  . ~/.ggc-completion.bash
fi

# Zsh (add to ~/.zshrc)
if [ -f ~/.ggc-completion.zsh ]; then
  . ~/.ggc-completion.zsh
fi

# Fish (add to ~/.config/fish/config.fish)
if test -f ~/.ggc-completion.fish
    source ~/.ggc-completion.fish
end

Once enabled, ggc b<Tab> completes to branch, and ggc branch <Tab> expands into the full list of subcommands.

Unified Syntax and -- Separator

ggc uses a flagless, space-separated syntax — no -x/--long options exist. All operations are expressed as subcommands (e.g. ggc fetch prune, ggc commit allow empty). Arguments after -- are treated as data rather than commands, so strings starting with - can be passed safely.

# Passing an argument that starts with -
ggc commit -- "-fix leading dash"

This keeps CLI behaviour predictable and easy to embed in scripts.

Soft Cancel

To abandon the current interactive operation and return to Search Mode without exiting the TUI, press Ctrl+G (or Esc when no escape sequence follows). Ctrl+C exits interactive mode entirely; Ctrl+G returns you to the search screen while staying inside the TUI.

debug-keys Command

A built-in command for verifying and troubleshooting keybindings:

# Display all current keybinding settings
ggc debug-keys

# Capture key sequences sent by the terminal in real time
ggc debug-keys raw

# Save captured key sequences to a file
ggc debug-keys raw keydump.txt

Running ggc debug-keys raw and pressing a key shows the exact byte sequence your terminal sends — useful for diagnosing why a keybinding is not triggering as expected.

tmux Support

If key input behaves unexpectedly inside tmux, add the following to .tmux.conf:

set -g xterm-keys on

Installation

# Homebrew (macOS/Linux)
brew install ggc

# Install script (easiest)
curl -sSL https://raw.githubusercontent.com/bmf-san/ggc/main/install.sh | bash

# Go install
go install github.com/bmf-san/ggc/v8@latest

Summary

ggc is an opinionated but flexible Git workflow tool. The CLI path and the interactive path share the same Route() layer, keeping behaviours consistent. The fuzzy-search scorer is a pure function with no dependencies, and the Workflow executor reuses Route() rather than duplicating command dispatch logic.

Tags: Golang Git CLI TUI
Share: 𝕏 Post Facebook Hatena
✏️ View source / Discuss on GitHub
☕ Support

If you enjoy this blog, consider supporting it. Every bit helps keep it running!


Related Articles