How bgit works

In the first part of this series, we explored what bgit is and how its user-friendly, interactive approach helps simplify Git for beginners. Now, it's time to pop the hood.
If you're new to the project and want to understand its user-facing features and philosophy first, I highly recommend reading Part 1: bgit: One Command for Most of git.
This post is for the curious developer, the aspiring contributor, the Rust enthusiast or anyone who wants to understand how bgit works internally. We won't be covering user features here; instead, we'll dissect the engine that powers them. At its core, bgit is a workflow engine with a pipeline architecture that orchestrates tasks via a chain-of-responsibility pattern, wrapped in an FSM-inspired API for clean, explicit state transitions—built in Rust on top of the git2-rs library.
The Foundation: git2-rs
A fundamental design decision in bgit was to avoid spawning git as a separate command-line process. While that approach can work, it comes with the overhead of managing processes, tracking progress, and parsing plain text output, which can be brittle.
Instead, bgit is built on git2-rs, a library that provides safe, programmatic Rust bindings for libgit2, a powerful C implementation of Git's core functions. This gives us direct, granular control over every Git operation. For example, creating a commit with git2-rs looks like this:
repo.commit(
Some("HEAD"), // Update HEAD
&signature, // Author
&signature, // Committer
message, // Commit message
&tree, // Tree
&[&parent_commit], // Parents
).unwrap();While this level of control is essential, it also exposes the raw complexity of Git. A core goal of bgit is to wrap this power in a safe, user-friendly architecture. Maintained by the Rust project itself, git2-rs is the solid foundation that makes this possible.
The Core Architecture: Workflow Engine
At its heart, bgit is a workflow engine that drives a pipeline of steps. When you run the bgit command, you enter the start of a WorkflowQueue. This queue represents an ordered pipeline of possible steps(that can branch), and bgit orchestrates progression based on the state of the repository and the user input.
A typical step in the workflow is defined by this enum:
pub(crate) enum Step {
Start(Task),
Stop,
Task(Task),
}Each Step can either be the start of a workflow, the end of a workflow (Stop), or contain another Task. A Task, in turn, is one of two types:
pub(crate) enum Task {
ActionStepTask(Box<dyn ActionStep>),
PromptStepTask(Box<dyn PromptStep>),
}This distinction is the key to bgit's interactive nature:
- An
ActionStepTaskis automated. It makes a decision based only on the environment (e.g., checking if Git is installed). - A
PromptStepTaskis interactive. It depends on user input to proceed (e.g., asking the user if they want to stage unstaged files).
Together, these components create a guided workflow that can branch into multiple paths, handle complex scenarios, and always end in a defined state.

The Building Blocks: From Command to Action
Now that we understand the workflow engine, let's look at the individual components that bring it to life. bgit's architecture is a clear chain of responsibility, where each component has a single, well-defined job.
A typical workflow looks like this:

Here’s the flow of how the pieces fit together:
- A
task(from astepin the workflow) determines what needs to happen next. - Before doing anything, the
taskvalidates the action by checking the necessaryrules. - If the rules pass, the system executes the corresponding
pre-hookscript, allowing for custom user actions before the event. - The
taskthen dispatches theevent, which is the small, atomic unit of work responsible for making the call togit2-rs. - After the
eventsuccessfully completes itsgit2-rsoperation, the correspondingpost-hookscript is executed.
Each task implements the ActionStep or PromptStep trait, which defines the execute() method. This method encapsulates the entire logic of that step, including rule checks, hook executions, and event dispatching. Each task returns the next Step in the workflow, allowing for dynamic progression based on the current state.
pub(crate) trait ActionStep { // or PromptStep
...
fn execute(
&self,
... // args
) -> Result<Step, Box<BGitError>>;
}In short, the entire data-flow looks like: task → rule check → pre-hook → event (calls git2-rs) → post-hook → next step.
A Closer Look: Rules, Events, and Hooks
The real "magic" of bgit happens at the lowest levels of its abstraction, where rules, events, and hooks interact to create a safe and powerful system.
The Power of Rules
In bgit, rules are intelligent guardrails checked by a task before an event is ever dispatched. This ensures that no invalid action is even attempted. A rule is a simple struct defining its conditions, but its most powerful feature is the try_fix() method. This is where bgit's "helper" personality comes from. A rule doesn't just fail; it can contain logic to offer a solution, like automatically unstaging a file that violates a size constraint.
pub(crate) trait Rule {
...
fn check(&self) -> Result<RuleOutput, Box<BGitError>>;
fn try_fix(&self) -> Result<bool, Box<BGitError>>;
}The complete list of approved rules is available in the docs.
Events and Hooks: The Action Core
Once the rules are satisfied, the action begins. The event is the final, smallest unit of work that makes the direct call to the git2-rs library.
This is also where the user-configurable hooks we discussed in our first blog post come into play. The hook_executor is designed to wrap the event:
- The
pre-hookscript runs immediately before theevent's logic. - The
post-hookscript runs immediately after theevent's logic successfully completes.
This powerful combination means that the core, compiled bgit logic is bracketed by flexible, user-defined scripts, allowing for incredible customization while maintaining a safe and validated core.
The hook_executor and Cross-Platform Hooks
One of bgit's core architectural challenges was handling Git hooks. The underlying git2-rs library, for some reasons, does not natively support invoking the standard Git hooks found in .git/hooks/. However, many developers rely on these hooks for their workflows. bgit bridges this gap with a sophisticated, hybrid approach managed by its hook_executor.
hook_executor
The hook_executor is designed to provide the best of both worlds: the portability of version-controlled hooks and compatibility with the most common native hooks.
Portable bgit Hooks: This is the preferred method in bgit. Hooks are placed in a
.bgit/hooks/directory within the repository.- Benefits: They are version-controlled, shared across the entire team, highly flexible and designed to be cross-platform from the ground up.
- Naming: They follow the
[pre|post]_[event_name]pattern, covering a wide range of bgit events.
Native Git Hooks: For compatibility, bgit provides best-effort support for the most critical native hooks.
- Location: The standard
.git/hooks/directory. - Supported: bgit explicitly looks for and executes
pre-commitandpost-commit. - Unsupported: It detects other native hooks (like
pre-pushorcommit-msg) and logs a warning if not supported.
- Location: The standard
NOTE
Projects like pre-commit do exists, which help with version controlling hooks, but they are not supported yet on bgit 😦
For example for the commit event, bgit orchestrates a the given sequence:
.bgit/hooks/pre_git_commit(Portable bgit hook)- Standard Git
pre-commit(Native hook) - The Commit event is Performed
.bgit/hooks/post_git_commit(Portable bgit hook)- Standard Git
post-commit(Native hook)
The Cross-Platform Challenge
The true complexity of the hook_executor is revealed in how it handles cross-platform execution, especially on Windows.
On Unix-like systems (Linux/macOS), the process is straightforward: bgit simply ensures the hook scripts in .bgit/hooks/ are executable (chmod +x) and runs them.
On Windows, however, the hook_executor becomes a far more sophisticated piece of logic. It intelligently finds the correct way to run a script by following a detailed execution strategy:
- It checks for hooks by extension precedence: It first looks for a script with no extension, then
.bat,.cmd,.ps1, and finally.exe. - It chooses the right runner:
.ps1files are run with PowerShell..batand.cmdfiles are run withcmd.exe..exefiles are executed directly.
- It intelligently finds Bash: If a script has a shebang (
#!/bin/bash), the executor searches for a Bash interpreter in common locations (Git Bash, MSYS2, WSL) or in the systemPATH. - It has a fallback: If all else fails, it attempts to run the script with
cmd.exe.
This robust strategy ensures that hooks defined by a team on Linux will work as expected for a teammate on Windows, solving a common pain point in cross-platform development. We plan to improve this further in future releases.
The config System: Explicit and Separated by Design
As we discussed in the first post, bgit's configuration system is built on a principle of strict separation to avoid confusion and ensure predictable behavior.
Global Config (
~/.config/bgit/config.toml): This file is exclusively for your personal, user-specific settings that apply across all projects. Think of authentication, API keys, and other personal preferences.Local Config (
.bgit/config.toml): This file is exclusively for project-specific settings that are version controlled and shared with the team. This includes things like workflow rules and behaviors for the repository.
A setting designed for the global file will not work in the local file, and vice versa. Because of this strict separation, one file cannot override the other—they simply manage completely different sets of options. This design is a deliberate choice to make a project's behavior explicit and prevent it from being modified by a hidden global setting.
Example Global Config (Only User-Specific Keys):
# Keys related to you, the user.
[auth]
preferred = "ssh"
key_file = "/home/user/.ssh/id_ed25519"Example Local Config (Only Project-Specific Keys):
# Keys related to the project's rules.
[rules.default]
NoSecretsStaged = "Error"Conclusion: How to Contribute
To recap, bgit is much more than just a simple script. In one line, it is just "one command for most of git". Its architecture flows from tasks to rules, then wraps core events with a cross-platform hook executor. All of this is designed for a single purpose: to create a safe, predictable, and helpful experience for the end-user.
We welcome contributions of all kinds and believe that the best tools are built by the community. Whether it's by tackling an existing issue, proposing a new rule, or improving the documentation, we'd love your help.
Project Details & Links
- Source Code: rootCircle/bgit on GitHub
- Package: bgit on Crates.io
- License: MIT License
- Platforms: Windows, macOS, & Linux
- Current Status: Pre-alpha (Contributions are highly welcome!)
We're excited to see what you'll build with us.