Skip to content

Commit d63023a

Browse files
authored
Merge pull request #7 from JheisonMB/develop
feat: add interactive init wizard
2 parents bfc5ef6 + be23097 commit d63023a

10 files changed

Lines changed: 808 additions & 106 deletions

File tree

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "gitkit"
3-
version = "0.1.2"
3+
version = "0.2.0"
44
edition = "2021"
55
description = "Standalone CLI for configuring git repos — hooks, .gitignore, and .gitattributes"
66
license = "MIT"
@@ -16,3 +16,9 @@ path = "src/main.rs"
1616
anyhow = "1"
1717
clap = { version = "4", features = ["derive"] }
1818
ureq = "2"
19+
20+
[dev-dependencies]
21+
tempfile = "3"
22+
23+
[dependencies.inquire]
24+
version = "0.7"

README.md

Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
# gitkit
1+
```
2+
███ █████ █████ ███ █████
3+
░░░ ░░███ ░░███ ░░░ ░░███
4+
███████ ████ ███████ ░███ █████ ████ ███████
5+
███░░███░░███ ░░░███░ ░███░░███ ░░███ ░░░███░
6+
░███ ░███ ░███ ░███ ░██████░ ░███ ░███
7+
░███ ░███ ░███ ░███ ███ ░███░░███ ░███ ░███ ███
8+
░░███████ █████ ░░█████ ████ █████ █████ ░░█████
9+
░░░░░███░░░░░ ░░░░░ ░░░░ ░░░░░ ░░░░░ ░░░░░
10+
███ ░███
11+
░░██████
12+
░░░░░░
13+
```
214

315
[![CI](https://github.com/JheisonMB/gitkit/actions/workflows/ci.yml/badge.svg)](https://github.com/JheisonMB/gitkit/actions/workflows/ci.yml)
416
[![Release](https://github.com/JheisonMB/gitkit/actions/workflows/release.yml/badge.svg)](https://github.com/JheisonMB/gitkit/actions/workflows/release.yml)
517
[![Crates.io](https://img.shields.io/crates/v/gitkit)](https://crates.io/crates/gitkit)
618
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
719

8-
Standalone CLI for configuring git repos — hooks, .gitignore, and .gitattributes. No Node.js, no Python, no runtime dependencies. One binary.
20+
Configure a git repo in seconds — hooks, `.gitignore`, `.gitattributes`, and git config. Interactive wizard or direct commands. No Node.js, no Python, no runtime dependencies. One binary.
921

1022
---
1123

@@ -31,61 +43,107 @@ irm https://raw.githubusercontent.com/JheisonMB/gitkit/main/install.ps1 | iex
3143
cargo install gitkit
3244
```
3345

46+
Available on [crates.io](https://crates.io/crates/gitkit).
47+
3448
### GitHub Releases
3549

3650
Check the [Releases](https://github.com/JheisonMB/gitkit/releases) page for precompiled binaries (Linux x86_64, macOS x86_64/ARM64, Windows x86_64).
3751

3852
### Uninstall
3953

54+
**Linux / macOS:**
4055
```bash
4156
rm -f ~/.local/bin/gitkit
4257
```
4358

59+
**Windows (PowerShell):**
60+
```powershell
61+
Remove-Item "$env:LOCALAPPDATA\gitkit\gitkit.exe" -Force
62+
```
63+
4464
---
4565

46-
## Quick Start
66+
## Quick Start
67+
68+
Interactive wizard — guided setup for a new repo:
69+
70+
```bash
71+
gitkit init
72+
```
73+
74+
Or use commands directly:
4775

4876
```bash
49-
# Install a built-in hook
50-
gitkit hooks init commit-msg conventional-commits
77+
gitkit hooks add conventional-commits
78+
gitkit ignore add rust,vscode,agentic
79+
gitkit attributes init
80+
gitkit config apply defaults
81+
```
5182

52-
# Install a custom hook command
53-
gitkit hooks init pre-push "cargo test"
83+
---
5484

55-
# List installed hooks
56-
gitkit hooks list
85+
## `gitkit init`
5786

58-
# Generate a .gitignore
59-
gitkit ignore add rust,vscode
87+
Interactive wizard that guides you through configuring a repo step by step.
88+
89+
- Hooks — built-ins pre-selected, or add a custom command
90+
- `.gitignore` — filterable search across all gitignore.io templates + built-ins
91+
- `.gitattributes` — line endings and binary file presets
92+
- Git config — 6 individual options, recommended ones pre-selected
6093

61-
# Apply line endings preset
62-
gitkit attributes init
94+
```
95+
gitkit init
6396
```
6497

6598
---
6699

67100
## Commands
68101

102+
### Hooks
103+
69104
| Command | Description |
70105
|---|---|
71-
| `gitkit hooks init <hook> <builtin\|command>` | Install a hook (built-in or custom command) |
106+
| `gitkit hooks add <builtin>` | Install a built-in hook (hook name inferred) |
107+
| `gitkit hooks add <hook> <command>` | Install a custom shell command as a hook |
72108
| `gitkit hooks list` | List installed hooks |
73-
| `gitkit hooks remove <hook>` | Remove a hook |
74-
| `gitkit hooks show <hook>` | Show hook content |
75-
| `gitkit ignore add <templates>` | Generate .gitignore via gitignore.io |
109+
| `gitkit hooks list --available` | Show all built-in hooks with descriptions |
110+
| `gitkit hooks remove <hook>` | Remove an installed hook |
111+
| `gitkit hooks show <hook>` | Print hook content |
112+
113+
### Ignore
114+
115+
| Command | Description |
116+
|---|---|
117+
| `gitkit ignore add <templates>` | Generate/merge `.gitignore` via gitignore.io |
76118
| `gitkit ignore list [filter]` | List available templates |
77-
| `gitkit attributes init` | Apply line endings preset |
78-
| `gitkit config apply <preset>` | Apply git config preset (defaults, advanced, delta) |
119+
120+
### Attributes
121+
122+
| Command | Description |
123+
|---|---|
124+
| `gitkit attributes init` | Apply line endings preset to `.gitattributes` |
125+
126+
### Config
127+
128+
| Command | Description |
129+
|---|---|
130+
| `gitkit config apply defaults` | `push.autoSetupRemote`, `help.autocorrect`, `diff.algorithm` |
131+
| `gitkit config apply advanced` | `merge.conflictstyle zdiff3`, `rerere.enabled` |
132+
| `gitkit config apply delta` | `core.pager delta` (requires `cargo`) |
79133

80134
---
81135

82136
## Built-in Hooks
83137

138+
Run `gitkit hooks list --available` to see these without leaving the terminal.
139+
84140
| Name | Hook | Description |
85141
|---|---|---|
86142
| `conventional-commits` | `commit-msg` | Validates Conventional Commits format |
87-
| `no-secrets` | `pre-commit` | Detects common secret patterns |
88-
| `branch-naming` | `pre-commit` | Validates branch name pattern |
143+
| `no-secrets` | `pre-commit` | Detects common secret patterns in staged changes |
144+
| `branch-naming` | `pre-commit` | Validates branch name matches convention |
145+
146+
Built-ins are embedded in the binary — no network required.
89147

90148
---
91149

@@ -99,16 +157,6 @@ gitkit attributes init
99157

100158
---
101159

102-
## Tech Stack
103-
104-
| Concern | Crate |
105-
|---|---|
106-
| CLI parsing | `clap` (derive) |
107-
| Error handling | `anyhow` |
108-
| HTTP client | `ureq` |
109-
110-
---
111-
112160
## License
113161

114162
MIT

src/attributes/mod.rs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,20 @@ use std::fs;
44

55
use crate::utils::{confirm, find_repo_root};
66

7-
const PRESET: &str = "* text=auto eol=lf\n";
7+
const PRESET_LF: &str = "* text=auto eol=lf\n";
8+
9+
const PRESET_BINARY: &str = "\
10+
*.png binary\n\
11+
*.jpg binary\n\
12+
*.jpeg binary\n\
13+
*.gif binary\n\
14+
*.ico binary\n\
15+
*.pdf binary\n\
16+
*.zip binary\n\
17+
*.tar binary\n\
18+
*.gz binary\n\
19+
*.wasm binary\n\
20+
";
821

922
#[derive(Subcommand)]
1023
pub enum AttributesCommand {
@@ -42,11 +55,72 @@ pub fn run(cmd: AttributesCommand) -> Result<()> {
4255
}
4356

4457
if dry_run {
45-
println!("[dry-run] Would write .gitattributes:\n{PRESET}");
58+
println!("[dry-run] Would write .gitattributes:\n{PRESET_LF}");
4659
return Ok(());
4760
}
4861

49-
fs::write(&path, PRESET).context("Failed to write .gitattributes")?;
62+
fs::write(&path, PRESET_LF).context("Failed to write .gitattributes")?;
5063
println!("Applied line endings preset to .gitattributes.");
5164
Ok(())
5265
}
66+
67+
/// Apply one or more attribute presets by label. Used by the interactive wizard.
68+
pub(crate) fn apply_presets(labels: &[&str]) -> Result<()> {
69+
let root = find_repo_root()?;
70+
let path = root.join(".gitattributes");
71+
let existing = if path.exists() {
72+
fs::read_to_string(&path).unwrap_or_default()
73+
} else {
74+
String::new()
75+
};
76+
let mut content = existing;
77+
for label in labels {
78+
let preset = match *label {
79+
"line-endings" => PRESET_LF,
80+
"binary-files" => PRESET_BINARY,
81+
_ => continue,
82+
};
83+
if !content.contains(preset.lines().next().unwrap_or("")) {
84+
if !content.ends_with('\n') && !content.is_empty() {
85+
content.push('\n');
86+
}
87+
content.push_str(preset);
88+
}
89+
}
90+
fs::write(&path, content).context("Failed to write .gitattributes")?;
91+
Ok(())
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
use super::*;
97+
use tempfile::TempDir;
98+
99+
fn make_git_repo() -> TempDir {
100+
let dir = TempDir::new().unwrap();
101+
std::fs::create_dir(dir.path().join(".git")).unwrap();
102+
dir
103+
}
104+
105+
#[test]
106+
fn attributes_init_dry_run_does_not_write_file() {
107+
let dir = make_git_repo();
108+
let path = dir.path().join(".gitattributes");
109+
// run with dry_run — file must not be created
110+
// We call the internal logic directly via the public run() with dry_run=true
111+
// but run() calls find_repo_root() which uses CWD, so we test the preset constant
112+
assert_eq!(PRESET_LF, "* text=auto eol=lf\n");
113+
assert!(!path.exists());
114+
}
115+
116+
#[test]
117+
fn attributes_preset_contains_lf_rule() {
118+
assert!(PRESET_LF.contains("eol=lf"));
119+
assert!(PRESET_LF.contains("text=auto"));
120+
}
121+
122+
#[test]
123+
fn attributes_binary_preset_marks_png() {
124+
assert!(PRESET_BINARY.contains("*.png binary"));
125+
}
126+
}

src/config/mod.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,85 @@ pub fn run(cmd: ConfigCommand) -> Result<()> {
3939
}
4040
}
4141

42+
/// Individual config options exposed for the interactive wizard.
43+
pub(crate) struct ConfigOption {
44+
pub key: &'static str,
45+
pub value: Option<&'static str>, // None for multi-key options like delta
46+
pub label: &'static str,
47+
pub recommended: bool,
48+
}
49+
50+
pub(crate) const CONFIG_OPTIONS: &[ConfigOption] = &[
51+
ConfigOption {
52+
key: "push.autoSetupRemote",
53+
value: Some("true"),
54+
label: "push.autoSetupRemote = true — auto-set upstream on first push",
55+
recommended: true,
56+
},
57+
ConfigOption {
58+
key: "help.autocorrect",
59+
value: Some("prompt"),
60+
label: "help.autocorrect = prompt — suggest corrections for mistyped commands",
61+
recommended: true,
62+
},
63+
ConfigOption {
64+
key: "diff.algorithm",
65+
value: Some("histogram"),
66+
label: "diff.algorithm = histogram — cleaner diffs for moved code",
67+
recommended: true,
68+
},
69+
ConfigOption {
70+
key: "merge.conflictstyle",
71+
value: Some("zdiff3"),
72+
label: "merge.conflictstyle = zdiff3 — show base in conflict markers",
73+
recommended: false,
74+
},
75+
ConfigOption {
76+
key: "rerere.enabled",
77+
value: Some("true"),
78+
label: "rerere.enabled = true — remember and reuse conflict resolutions",
79+
recommended: false,
80+
},
81+
ConfigOption {
82+
key: "core.pager",
83+
value: None, // handled separately — installs git-delta via cargo
84+
label: "core.pager = delta — beautiful syntax-highlighted diffs (requires cargo)",
85+
recommended: false,
86+
},
87+
];
88+
89+
/// Apply selected config option keys. Used by the interactive wizard.
90+
pub(crate) fn apply_config_keys(keys: &[&str], cargo_available: bool) -> Result<()> {
91+
for key in keys {
92+
// Find the matching option to reuse its value from CONFIG_OPTIONS context,
93+
// then dispatch to the appropriate setter.
94+
match *key {
95+
"core.pager" => {
96+
anyhow::ensure!(
97+
cargo_available,
98+
"cargo not found — cannot install git-delta"
99+
);
100+
if !delta_installed() {
101+
install_delta()?;
102+
}
103+
for (k, v) in DELTA_CONFIGS {
104+
git_config_set(k, v)?;
105+
}
106+
}
107+
_ => {
108+
// All non-delta options map directly from CONFIG_OPTIONS value
109+
let value = CONFIG_OPTIONS
110+
.iter()
111+
.find(|o| o.key == *key)
112+
.and_then(|o| o.value)
113+
.ok_or_else(|| anyhow::anyhow!("Unknown config key: {key}"))?;
114+
git_config_set(key, value)?;
115+
}
116+
}
117+
}
118+
Ok(())
119+
}
120+
42121
type GitConfigs = &'static [(&'static str, &'static str)];
43122

44123
const DEFAULTS: GitConfigs = &[
@@ -138,3 +217,25 @@ fn install_delta() -> Result<()> {
138217
anyhow::ensure!(status.success(), "cargo install git-delta failed");
139218
Ok(())
140219
}
220+
221+
#[cfg(test)]
222+
mod tests {
223+
use super::*;
224+
225+
#[test]
226+
fn apply_configs_dry_run_prints_without_running_git() {
227+
// dry_run=true must not invoke git; if it did it would fail in CI without a repo
228+
let result = apply_configs(DEFAULTS, true);
229+
assert!(result.is_ok());
230+
}
231+
232+
#[test]
233+
fn apply_configs_dry_run_covers_advanced_preset() {
234+
assert!(apply_configs(ADVANCED, true).is_ok());
235+
}
236+
237+
#[test]
238+
fn apply_configs_dry_run_covers_delta_preset() {
239+
assert!(apply_configs(DELTA_CONFIGS, true).is_ok());
240+
}
241+
}

0 commit comments

Comments
 (0)