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, we highly recommend reading Part 1: bgit: One Command for Most of git.
This post is for the curious developer, the aspiring contributor, or the Rust enthusiast 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 powerful state machine written in Rust, built on top of the git2-rs
library. Let's dive in.
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: A Guided State Machine
At its heart, bgit
is a state machine. When you run the bgit
command, you aren't just running a script; you are entering the start of a WorkflowQueue
. This queue is essentially a tree of possible steps, and bgit
's job is to navigate this tree based on the state of your repository and your 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
ActionStepTask
is automated. It makes a decision based only on the environment (e.g., checking if Git is installed). - A
PromptStepTask
is 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 state machine concept, 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.
Here’s the corrected flow of how the pieces fit together:
- A
task
(from astep
in the workflow) determines what needs to happen next. - Before doing anything, the
task
validates the action by checking the necessaryrules
. - If the rules pass, the system executes the corresponding
pre-hook
script, allowing for custom user actions before the event. - The
task
then dispatches theevent
, which is the small, atomic unit of work responsible for making the call togit2-rs
. - After the
event
successfully completes itsgit2-rs
operation, the correspondingpost-hook
script is executed.
This creates a robust, predictable, and extensible data flow: task
→ rule
check → pre-hook
→ event
(calls git2-rs
) → post-hook
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) struct NoLargeFile {
name: String,
description: String,
level: RuleLevel,
threshold_bytes: u64,
}
fn try_fix(&self) -> Result<bool, Box<BGitError>>
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-hook
script runs immediately before theevent
's logic. - The
post-hook
script 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.
A Deep Dive: The hook_executor
and Cross-Platform Hooks
One of bgit
's core architectural challenges was handling Git hooks. The underlying libgit2
library, for performance and safety reasons, does not natively invoke 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
.
The Solution: A Hybrid Hook System
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 inbgit
. Hooks are placed in a.bgit/hooks/
directory within the repository.- Benefits: They are version-controlled, shared across the entire team, and designed to be cross-platform from the ground up.
- Naming: They follow the
[pre|post]_[event_name]
pattern, covering a wide range ofbgit
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 executespre-commit
andpost-commit
. - Unsupported: It detects other native hooks (like
pre-push
orcommit-msg
) and logs a warning, encouraging users to migrate their logic to the more robust.bgit/hooks
system.
- Location: The standard
For the critical commit event, bgit
orchestrates a clear and predictable sequence:
.bgit/hooks/pre_git_commit
(Portablebgit
hook)- Standard Git
pre-commit
(Native hook) - The Commit Action is Performed
.bgit/hooks/post_git_commit
(Portablebgit
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:
.ps1
files are run with PowerShell..bat
and.cmd
files are run withcmd.exe
..exe
files 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.
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 more than just a simple script. It's a robust state machine built in Rust that uses git2-rs
for programmatic Git access. Its architecture is a clear chain of command, flowing from tasks to rules, then wrapping core events with a cross-platform hook executor. All of this complexity is designed for a single purpose: to create a safe, predictable, and helpful experience for the end-user.
Now that you understand the core concepts of workflows, tasks, events, and rules, you're well-equipped to dive into the codebase. 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.