Building CommitGate - Part 5: CLI Integration
Welcome back to part 5 of building CommitGate! It has been a while, as unfortunately I have not been able to allocate the appropriate time to continue these blog posts while working on my other personal projects and hackathons. Because of that, I plan to end this dev journey blog series within the next few blog posts. My teammate and I have been continuously working on this project even after the hackathon (from which we should hear back about the results in one week). In the upcoming blog posts I want to briefly talk about what we have changed and some of the thinking behind them.
1. CLI
We have made a lot of changes to the CLI. Let’s take a look at some of the changes.
First, let’s look at how the security scan is run:
gitleaks_findings = run_gitleaks_scan(file_paths=file_paths)
if ai_enabled:
ai_findings, ai_review_ok = review(diff=diff, staged_files=file_paths, timeout=timeout)
else:
ai_findings, ai_review_ok = [], True
all_findings = remove_dup(gitleaks_findings + ai_findings)
Gitleaks is run by default, and we haven’t provided (nor intend to) any options to turn off Gitleaks scan yet. The reason we have to have an ai_review_ok variable is to know whether the AI scan was run successfully or not, as it will return [] in ai_findings both when no findings are found and when there is an error during the scanning process.
The findings are then passed into the removed_dup function implemented in the report_generator module. We will then print the appropriate message to the terminal based on the findings.
When there are no findings:
if not all_findings:
if not ai_enabled:
print("[yellow]AI review disabled by config.[/yellow]")
print("[green]No security findings found![/green]")
print("[green]CommitGate scan completed![/green]")
raise typer.Exit(code=0)
When there are findings:
decision = decide(all_findings)
log_decision(decision)
action = decision["action"]
color = "yellow" if action == "warn" else "red"
print(f"[{color}]CommitGate detected {len(all_findings)} security finding(s):[/{color}]")
for index, finding in enumerate(all_findings):
severity = finding.get("severity", "").lower()
sev_color = severity_color(severity=severity)
print(
f"[{sev_color}]"
f"[{severity.upper()}] Finding #{index + 1}"
f"[/{sev_color}]"
)
finding_output = format_finding(finding=finding, fields=fields)
print(finding_output)
print()
We also added an init command that installs the git hook and creates the config file:
@app.command()
def init():
config_file = create_default_config()
hook_path = install_git_hook()
print(f"[green]Created config file:[/green] {config_file}")
print(f"[green]Installed {hook_path.name} hook:[/green] {hook_path}")
2. Config System
We implemented a config system that allows users to configure CommitGate. The configurations are stored in a commitgate.yaml file so that users within a project can share the same configurations (or they can simply add it to .gitignore).
Creating the default config file is very straightforward:
DEFAULT_CONFIG = {
"enabled": True,
"ai": {
"enabled": True,
"provider": "deepseek",
"timeout": 20,
},
"policy": {
"block_severity": "high",
},
"reporting": {
"min_severity": "medium",
"fields": {
"source": True,
"category": True,
# "severity": True,
# "file": True,
# "location": True,
"description": True,
"suggestions": True,
},
},
}
DEFAULT_CONFIG_YAML = """\
# Enable or disable CommitGate for this repository.
enabled: true
ai:
# Enable AI-powered security review.
enabled: true
# AI provider to use.
# Option 1: (AI_KEY in .env): openai, gemini, deepseek, kimi, groq (Tip: groq offers a free API key - at https://console.groq.com)
# Option 2: Claude Code or Codex (no API key): claude-cli, codex-cli
provider: deepseek
# Maximum time (seconds) allowed for AI review.
timeout: 20
policy:
# Findings at or above this severity block the commit/push.
# Options: low, medium, high, critical
block_severity: high
reporting:
# Minimum severity shown in CommitGate output.
# Must be <= block_severity, so a blocking finding is never hidden
# Options: low, medium, high, critical
# Example: medium shows medium, high, and critical findings, but hides low findings.
# Raising this also speeds up the AI review (fewer findings to generate).
min_severity: medium
# Control which optional fields are displayed for each finding.
# Turning off description and suggestions also speeds up the AI review.
fields:
source: true
category: true
description: true
suggestions: true
"""
We’ll use DEFAULT_CONFIG when we need to load the default config and DEFAULT_CONFIG_YAML when creating the default file. This is done so that the file comes with helpful comments to let the user know how to use the different configuration options. To create the file:
path = get_config_path()
if path.exists() and not overwrite:
return path
path.write_text(DEFAULT_CONFIG_YAML, encoding="utf-8")
return path
To load user’s configurations, we merge it with the default configs and then validate them:
path = get_config_path()
if not path.exists():
config = deepcopy(DEFAULT_CONFIG)
else:
with open(path, "r", encoding="utf-8") as f:
user_config = yaml.safe_load(f)
if not user_config:
config = deepcopy(DEFAULT_CONFIG)
elif not isinstance(user_config, dict):
raise ValueError("commitgate.yaml must contain a YAML dictionary")
else:
config = merge_with_defaults(user_config)
validate_config(config)
return config
merge_with_defaults uses the merge_dicts function to recursively merge the user configuration dict with the default one. It needs to be recursive since different configuration options might have further subdicts. merge_dicts is implemented like this:
def merge_dicts(default_dict: dict, user_dict: dict) -> dict:
"""
Helper function: Recursive dictionary merge
"""
result = deepcopy(default_dict)
for key, value in user_dict.items():
# if the current subfield is also a dictionary, then we call the merge function
if (
key in result
and isinstance(result[key], dict)
and isinstance(value, dict)
):
result[key] = merge_dicts(result[key], value)
else:
result[key] = value
return result
I will not show the validation function since it is pretty straightforward. We check the user config against a pre-defined set of valid options and raise a ValueError if it is not valid.
Conclusion
Unfortunately, this will be the last post talking about CommitGate’s implementation. I might write some more posts in the future discussing its updates but nothing is decided yet. In the next post (which will be the last of the whole series), I will talk about the lessons I learned creating CommitGate, and also give some final words on the project. See yall then!
Enjoy Reading This Article?
Here are some more articles you might like to read next:
- Building CommitGate - Part 3: Git Hooks Installation
- Building CommitGate - Part 4: Gitleaks Integration & Git Hook Install Fix
- Building CommitGate - Part 2: Workflow & Repository Setup
- Building CommitGate - Part 1: Planning & Architecture
- NBA Playoff Qualification Prediction Using Logistic Regression
- Predicting NBA Player Salary Using Linear Regression
- Predict NBA Position Using K-nearest Neighbor
- Predict NBA Position Using K-means Clustering
- The most underrated human ability