Skip to content

Commit 96b1721

Browse files
authored
Merge pull request #100 from acunniffe/feat/enterprise-allowlist
Feat/enterprise allowlist
2 parents d3e93e5 + d784e2c commit 96b1721

File tree

6 files changed

+153
-3
lines changed

6 files changed

+153
-3
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "git-ai"
3-
version = "1.0.1"
3+
version = "1.0.2"
44
edition = "2024"
55

66

docs/enterprise-configuration.mdx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
title: Enterprise Configuration
3+
---
4+
5+
`git-ai`'s behavior can be configured on developer machines by writing a JSON file in the user's home directory.
6+
7+
On Linux and macOS, this file is located at `$HOME/.git-ai/config.json`.
8+
On Windows, this file is located at `%USERPROFILE%\.git-ai\config.json`.
9+
10+
## Options
11+
12+
All the options in `config.json` are optional, and will fall back to default values if not provided.
13+
14+
| --- | --- | --- |
15+
| `git_path` `Path` | The path to the (unaltered) `git` binary you distribute on developer machines | Defaults to whichever git is on the shell path |
16+
| `ignore_prompts` `boolean` flag | Prompts be excluded from authorship logs | `false` |
17+
| `allow_repositories` `Path[]` | Allow `git-ai` in only these remotes | If not specified or set to an empty list, all repositories are allowed. |
18+
19+
```json
20+
{
21+
"git_path": "/usr/bin/git",
22+
"ignore_prompts": false,
23+
"allow_repositories": [
24+
"https://github.com/acunniffe/git-ai.git"
25+
]
26+
}
27+
```
28+
29+
## Installing `git-ai` binary on developer machines
30+
31+
When `git-ai` is installed using the [`install.sh` script](https://github.com/acunniffe/git-ai?tab=readme-ov-file#install) (reccomended for personal use) the downloaded binary will be configured to handle calls to both `git` and `git-ai`, effectively creating a wrapper/proxy to `git`.
32+
33+
If you would like to create a custom installation here is what `git-ai` requires to works correctly cross platform:
34+
35+
### Directory Structure
36+
37+
**Unix/Linux/macOS:**
38+
- Install the `git-ai` binary to: `$HOME/.git-ai/bin/git-ai`
39+
- Create a symlink: `$HOME/.git-ai/bin/git``$HOME/.git-ai/bin/git-ai`
40+
- Create a symlink: `$HOME/.git-ai/bin/git-og``/path/to/original/git`
41+
- Make the binary executable: `chmod +x $HOME/.git-ai/bin/git-ai`
42+
- On macOS only: Remove quarantine attribute: `xattr -d com.apple.quarantine $HOME/.git-ai/bin/git-ai`
43+
44+
**Windows:**
45+
- Install the binary to: `%USERPROFILE%\.git-ai\bin\git-ai.exe`
46+
- Create a copy: `%USERPROFILE%\.git-ai\bin\git.exe` (copy of `git-ai.exe`)
47+
- Create a batch file: `%USERPROFILE%\.git-ai\bin\git-og.cmd` that calls the original git executable
48+
- Unblock the downloaded files (PowerShell: `Unblock-File`)
49+
50+
### PATH Configuration
51+
52+
**Unix/Linux/macOS:**
53+
- Add `$HOME/.git-ai/bin` to the beginning of the user's PATH
54+
- Update the appropriate shell config file (`.zshrc`, `.bashrc`, etc.)
55+
56+
**Windows:**
57+
- Add `%USERPROFILE%\.git-ai\bin` to the System PATH
58+
- The directory should be positioned **before** any existing Git installation directories to ensure the git-ai shim takes precedence
59+
60+
### Configuration File
61+
62+
Create `$HOME/.git-ai/config.json` (or `%USERPROFILE%\.git-ai\config.json` on Windows) with the options outlined at the top of this page.
63+
64+
### IDE/Agent Hook Installation
65+
66+
After installing the binary and configuring PATH, run:
67+
68+
```bash
69+
git-ai install-hooks
70+
```
71+
72+
This sets up integration with supported IDEs and AI coding agents (Cursor, VS Code with GitHub Copilot, etc.).
73+
74+
### Reference Implementation
75+
76+
Our official install scripts implement all of these requirements and can serve as references:
77+
- Unix/Linux/macOS: [`install.sh`](https://github.com/acunniffe/git-ai/blob/main/install.sh)
78+
- Windows: [`install.ps1`](https://github.com/acunniffe/git-ai/blob/main/install.ps1)
79+
80+
These scripts handle edge cases like detecting the original git path, preventing recursive installations, and gracefully handling errors.
81+
82+

src/commands/git_handlers.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,17 @@ pub fn handle_git(args: &[String]) {
8181
// println!("command_args: {:?}", parsed_args.command_args);
8282
// println!("to_invocation_vec: {:?}", parsed_args.to_invocation_vec());
8383

84+
let config = config::Config::get();
85+
86+
let skip_hooks = !config.is_allowed_repository(&repository_option);
87+
if skip_hooks {
88+
debug_log(
89+
"Skipping git-ai hooks because repository does not have at least one remote in allow_repositories list",
90+
);
91+
}
92+
8493
// run with hooks
85-
let exit_status = if !parsed_args.is_help && has_repo {
94+
let exit_status = if !parsed_args.is_help && has_repo && !skip_hooks {
8695
let repository = repository_option.as_mut().unwrap();
8796
run_pre_command_hooks(&mut command_hooks_context, &parsed_args, repository);
8897
let exit_status = proxy_to_git(&parsed_args.to_invocation_vec(), false);

src/config.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
1+
use std::collections::HashSet;
12
use std::env;
23
use std::fs;
34
use std::path::{Path, PathBuf};
45
use std::sync::OnceLock;
56

67
use serde::Deserialize;
78

9+
use crate::git::repository::Repository;
10+
811
/// Centralized configuration for the application
912
pub struct Config {
1013
git_path: String,
1114
ignore_prompts: bool,
15+
allow_repositories: HashSet<String>,
1216
}
1317
#[derive(Deserialize)]
1418
struct FileConfig {
1519
#[serde(default)]
1620
git_path: Option<String>,
1721
#[serde(default)]
1822
ignore_prompts: Option<bool>,
23+
#[serde(default)]
24+
allow_repositories: Option<Vec<String>>,
1925
}
2026

2127
static CONFIG: OnceLock<Config> = OnceLock::new();
@@ -42,6 +48,25 @@ impl Config {
4248
self.ignore_prompts
4349
}
4450

51+
pub fn is_allowed_repository(&self, repository: &Option<Repository>) -> bool {
52+
// If allowlist is empty, allow everything
53+
if self.allow_repositories.is_empty() {
54+
return true;
55+
}
56+
57+
// If allowlist is defined, only allow repos whose remotes match the list
58+
if let Some(repository) = repository {
59+
match repository.remotes_with_urls().ok() {
60+
Some(remotes) => remotes
61+
.iter()
62+
.any(|remote| self.allow_repositories.contains(&remote.1)),
63+
None => false, // Can't verify, deny by default when allowlist is active
64+
}
65+
} else {
66+
false // No repository provided, deny by default when allowlist is active
67+
}
68+
}
69+
4570
/// Returns whether prompts should be ignored (currently unused by internal APIs).
4671
#[allow(dead_code)]
4772
pub fn ignore_prompts(&self) -> bool {
@@ -55,12 +80,19 @@ fn build_config() -> Config {
5580
.as_ref()
5681
.and_then(|c| c.ignore_prompts)
5782
.unwrap_or(false);
83+
let allow_repositories = file_cfg
84+
.as_ref()
85+
.and_then(|c| c.allow_repositories.clone())
86+
.unwrap_or(vec![])
87+
.into_iter()
88+
.collect();
5889

5990
let git_path = resolve_git_path(&file_cfg);
6091

6192
Config {
6293
git_path,
6394
ignore_prompts,
95+
allow_repositories,
6496
}
6597
}
6698

src/git/repository.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,33 @@ impl Repository {
586586
Ok(remotes.trim().split("\n").map(|s| s.to_string()).collect())
587587
}
588588

589+
// List all remotes with their URLs as tuples (name, url)
590+
pub fn remotes_with_urls(&self) -> Result<Vec<(String, String)>, GitAiError> {
591+
let mut args = self.global_args_for_exec();
592+
args.push("remote".to_string());
593+
args.push("-v".to_string());
594+
595+
let output = exec_git(&args)?;
596+
let remotes_output = String::from_utf8(output.stdout)?;
597+
598+
let mut remotes = Vec::new();
599+
let mut seen = std::collections::HashSet::new();
600+
601+
for line in remotes_output.trim().split("\n").filter(|s| !s.is_empty()) {
602+
let parts: Vec<&str> = line.split_whitespace().collect();
603+
if parts.len() >= 2 {
604+
let name = parts[0].to_string();
605+
let url = parts[1].to_string();
606+
// Only add each remote once (git remote -v shows fetch and push)
607+
if seen.insert(name.clone()) {
608+
remotes.push((name, url));
609+
}
610+
}
611+
}
612+
613+
Ok(remotes)
614+
}
615+
589616
pub fn config_get_str(&self, key: &str) -> Result<Option<String>, GitAiError> {
590617
let mut args = self.global_args_for_exec();
591618
args.push("config".to_string());

0 commit comments

Comments
 (0)