~/projects/bgit
Published on

Bgit -- A Modular Git Workflow Engine Built in Rust

2124 words11 min read–––
Views
Authors

Bgit -- A Modular Git Workflow Engine Built in Rust

The Problem

Git is powerful, but its flexibility is a double-edged sword. In practice, teams and individual developers repeatedly run into the same class of preventable mistakes:

  • Secrets get committed. A .env file, an API key embedded in source, a private SSH key -- once pushed, the damage is done. Rotating credentials after the fact is painful and often incomplete.
  • Large files bloat repositories. A single 50 MB binary checked in by accident can haunt a repository forever. Git LFS exists, but nothing stops you from forgetting to use it.
  • Commit messages are inconsistent. Without enforcement, commit histories devolve into fix, wip, asdf, making changelogs and bisects useless.
  • Hooks are not portable. Git hooks live in .git/hooks/ and are not tracked by version control. Every new clone starts with a blank hook directory. Teams resort to third-party hook managers or manual setup scripts.
  • Authentication is fragile. SSH agent management, HTTPS PAT storage, and credential persistence vary across platforms and are easy to misconfigure.

Existing solutions address these problems in isolation: CI pipelines catch issues post-push (too late), linters require separate setup per project, and shell aliases do not compose into a coherent workflow. There is no single tool that validates locally, executes Git operations safely, and adapts to per-project policies -- all before anything leaves the developer's machine.

What Bgit Does

Bgit is a Git workflow engine that replaces the manual git add && git commit && git push ceremony with a single interactive command:

bgit

Behind that one command is a step-based execution engine that walks the user through an intelligent workflow: detecting repository state, prompting for decisions, enforcing validation rules, running hooks, and executing Git operations through git2-rs -- all configurable via TOML without recompilation.

Bgit is designed around three principles:

  1. Validate before you push, not after. Rules run locally as pre-checks on every Git operation, catching secrets, oversized files, and malformed commit messages before they enter history.
  2. Configure, don't hard-code. A two-tier TOML configuration system lets teams define per-repository rule levels and workflow flags, while users maintain global auth preferences and API keys.
  3. One command, full workflow. The interactive step engine guides beginners through Git while giving experienced developers a fast, opinionated pipeline with guardrails.

Architecture Deep Dive

Bgit is structured around four core subsystems: a step-based workflow engine, a rule validation system, an atomic event layer wrapping Git operations, and a TOML-driven configuration system. These are supported by cross-platform authentication, a portable hook executor, and optional LLM integration.

Step-Based Workflow Engine

The execution model is a linked-list state machine built on two enums and two traits:

enum Step {
    Start(Task),  // entry point -- only valid as the queue's initial step
    Task(Task),   // next node in the chain
    Stop,         // terminal -- workflow complete
}

enum Task {
    ActionStepTask(Box<dyn ActionStep>),   // automated decision
    PromptStepTask(Box<dyn PromptStep>),   // interactive user prompt
}

Each step implements either ActionStep (for automated decisions like "is this a Git repo?" or "are there unstaged changes?") or PromptStep (for interactive prompts like "which files do you want to stage?" or "enter a commit message"). Both traits share the same execute signature:

fn execute(
    &self,
    step_config_flags: Option<&StepFlags>,
    workflow_rules_config: Option<&WorkflowRules>,
    global_config: &BGitGlobalConfig,
) -> Result<Step, Box<BGitError>>;

The return type is the key design decision: every step returns the next Step to execute. This means workflow routing is fully distributed -- each step decides where to go based on repository state, user input, and configuration flags. There is no central routing table.

WorkflowQueue drives the loop. It takes the initial Start(Task), executes it, and chains through Task(...) nodes until it hits Stop. Prompt steps run inside indicatif's pb.suspend() so that dialoguer prompts render cleanly alongside the progress spinner.

The default workflow currently includes 9 action steps and 15 prompt steps, covering the full lifecycle from "is this a Git repo?" through staging, committing, branch management, and push/pull synchronization.

Rule Validation System

Rules are the enforcement layer. Every rule implements the Rule trait:

trait Rule {
    fn check(&self) -> Result<RuleOutput, Box<BGitError>>;
    fn try_fix(&self) -> Result<bool, Box<BGitError>>;
    fn verify(&self) -> Result<bool, Box<BGitError>>;
    fn execute(&self) -> Result<bool, Box<BGitError>>;
    fn get_level(&self) -> RuleLevel;
    // ...
}

The execute() method implements a three-phase protocol driven by the rule's configured RuleLevel:

  • Skip: bypass entirely, return success.
  • Warning: run check(), attempt try_fix() on failure, but always continue the workflow.
  • Error: run check(), attempt try_fix() on failure, then verify() the fix. If verification fails, halt the workflow with a structured BGitError.

This design lets teams tune rule severity per repository. A secret detection rule might be Error in production repos but Warning in personal experiments.

Currently implemented rules include:

RulePurpose
NoSecretsStagedScans staged diffs for secret patterns using regex matching and entropy analysis
NoSecretFilesStagedBlocks staging of files with sensitive names (.env, credential files)
NoLargeFileRejects individual staged files exceeding 2 MiB or cumulative staged size exceeding 32 MiB
IsRepoSizeTooBigWarns when the working tree exceeds 128 MiB
ConventionalCommitMessageValidates commit messages against the Conventional Commits specification
RemoteExistsEnsures a remote (default origin) exists; can auto-fix by prompting to add one
IsGitInstalledLocallyVerifies git is available on the system
GitNameEmailSetupEnsures user.name and user.email are configured

Atomic Events

Every Git operation is wrapped in the AtomicEvent trait, which enforces a consistent execution pipeline:

trait AtomicEvent<'a> {
    fn raw_execute(&self) -> Result<bool, Box<BGitError>>;
    fn add_pre_check_rule(&mut self, rule: Box<dyn Rule + Send + Sync>);
    fn execute(&self) -> Result<bool, Box<BGitError>>;
    // ...
}

The default execute() implementation runs four phases in order:

  1. Rule checks -- iterate all attached rules, calling rule.execute() on each.
  2. Pre-event hooks -- run .bgit/hooks/pre_{event_name} if present, plus standard pre-commit for commit events.
  3. Raw execution -- the actual git2-rs operation (add, commit, push, etc.).
  4. Post-event hooks -- run .bgit/hooks/post_{event_name} if present, plus standard post-commit for commit events.

Any phase failure short-circuits the pipeline with a structured error. This means a secret detected in staged files will block the commit before it is created, and a failing pre-push hook will prevent data from leaving the machine.

Twelve events are currently implemented: git_add, git_branch, git_clone, git_commit, git_config, git_init, git_log, git_pull, git_push, git_restore, git_stash, and git_status.

TOML-Driven Configuration

Bgit uses a two-tier configuration system that dynamically loads and merges settings at runtime.

Local configuration (.bgit/config.toml at the repository root) controls per-workflow rule levels and step flags:

[rules.default]
NoSecretsStaged = "Error"
NoLargeFile = "Warning"
IsGitInstalledLocally = "Skip"

[workflow.default.is_sole_contributor]
overrideCheckForAuthors = ["Alice <alice@example.com>"]

[workflow.default.git_add]
skipAddAll = true
includeUntracked = false

Internally, this maps to HashMap<String, WorkflowRules> for rule levels and HashMap<String, WorkflowSteps> for step flags, both keyed by workflow name. Step flags are stored as HashMap<String, serde_json::Value>, giving each step full flexibility to define its own configuration schema without changing the config parser.

Global configuration (~/.config/bgit/config.toml on Linux/macOS, %APPDATA%/bgit/config.toml on Windows) stores user-wide authentication preferences and API keys:

[auth]
preferred = "ssh"

[auth.https]
username = "alice"
pat = "Z2hwXzEyMzQ1Njc4OTBhYmNkZWY="  # base64-encoded PAT

[auth.ssh]
key_file = "~/.ssh/id_ed25519"

[integrations]
google_api_key = "QUl6YVN5..."  # base64-encoded API key

Sensitive fields (PATs, API keys) are stored as base64 in the TOML file and decoded transparently on load via custom serde deserializers. No recompilation is required to change any behavior -- teams enforce custom validation policies by editing a config file.

Cross-Platform Authentication and SSH Agent Management

Bgit integrates authentication directly into the Git operation pipeline through git2's RemoteCallbacks. The system supports three modes controlled by the PreferredAuth setting:

  • RepositoryURLBased (default): infers SSH or HTTPS from the remote URL scheme.
  • Ssh: forces SSH authentication with agent-based key management.
  • Https: uses username + PAT from global config or interactive prompt.

The SSH subsystem manages the full agent lifecycle:

  1. Persistent agent: bgit maintains its own SSH agent bound to ~/.ssh/bgit_ssh_agent.sock (Unix) that persists across bgit invocations.
  2. Stale cleanup: on startup, bgit validates that the socket is live (via ssh-add -l). If stale, the socket file is deleted and a fresh agent is spawned.
  3. Automatic key discovery: if no identities are loaded, bgit scans ~/.ssh/ for standard key files (id_ed25519, id_rsa, etc.), with priority given to any key configured in auth.ssh.key_file.
  4. Retry logic: authentication attempts are capped at 3, with fallback from agent-based auth to interactive key selection to direct key file auth.

Platform-specific implementations (Unix domain sockets vs Windows named pipes) are isolated behind conditional compilation (#[cfg(unix)], #[cfg(windows)]), with an unsupported stub for other targets.

Portable Hook Execution

Bgit introduces a portable hook system that lives in version control. Hooks are placed in .bgit/hooks/ at the repository root:

.bgit/hooks/
  pre_git_add.sh
  post_git_commit.sh
  pre_git_push.py

Unlike .git/hooks/, this directory is tracked by Git and shared across clones. The hook executor resolves scripts by file extension and runs them with the appropriate interpreter. On Unix, it handles the executable bit and falls back to /bin/sh on ENOEXEC. On Windows, a parallel implementation handles platform-specific execution semantics.

For commit events specifically, bgit also runs standard Git hooks (pre-commit, post-commit) from the resolved core.hooksPath, maintaining compatibility with existing Git hook setups.

LLM-Powered Commit Messages

Bgit optionally integrates with Google Gemini to generate commit messages. When the user opts in during the commit prompt, bgit sends the staged diff to Gemini with instructions to produce a Conventional Commits-compliant message.

The generated message is validated through the same ConventionalCommitMessage rule used for manual messages, exposed to the LLM as a tool call via the rig-core framework. This means the LLM can self-correct: if its first attempt fails validation, it can re-invoke the validation tool and iterate.

The API key is resolved from (in order): the global config integrations.google_api_key, the GOOGLE_API_KEY environment variable, or an interactive prompt with optional persistence to the global config.


Tech Stack

  • Rust -- memory safety, performance, and cross-platform compilation.
  • git2-rs -- native libgit2 bindings for direct repository interaction without shelling out to git.
  • clap -- CLI argument parsing with derive macros and shell completion generation.
  • dialoguer -- interactive terminal prompts (selection, confirmation, text input).
  • indicatif -- progress spinners and status feedback during workflow execution.
  • serde + toml -- configuration parsing with custom deserializers for base64-encoded secrets.
  • regex -- pattern matching for secret detection rules.
  • rig-core -- LLM tool-calling framework for Gemini integration.
  • tokio -- async runtime for LLM API calls.

Getting Started

Install the precompiled binary:

# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/rootCircle/bgit/main/scripts/install.sh | bash

# Windows (PowerShell)
iwr -useb https://raw.githubusercontent.com/rootCircle/bgit/main/scripts/install.ps1 | iex

Or install via Cargo:

cargo install bgit

Then navigate to any Git repository and run:

bgit

Follow the on-screen prompts. Bgit detects repository state, walks you through staging, committing, and pushing, and enforces configured rules at every step. To customize behavior, create a .bgit/config.toml at your repository root.


Contributing

Bgit is open source under the MIT license. Contributions around new rules, events, workflow steps, and platform support are welcome. Open an issue or submit a pull request on the GitHub repository.