Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ for repo configs), where `<ext>` is `yml`, `yaml`, `json`, `jsonc`, or `toml`.

- Creates symlinks for all standard git hooks in `~/.lhm/hooks/`, each pointing to the `lhm` binary
- Sets `git config --global core.hooksPath ~/.lhm/hooks`
- With `--default-config`: writes a default `~/.lefthook.yaml` if no global config exists
- Writes a default `~/.lefthook.yaml` if no global config exists

### `lhm dry-run`

Expand All @@ -36,10 +36,11 @@ lhm dry-run
When git triggers a hook, it invokes the symlink in `~/.lhm/hooks/`. `lhm` detects the hook name from `argv[0]` and:

0. **lefthook not in PATH**: falls back to executing `.git/hooks/<hook>` directly (if it exists), bypassing all config merging
1. **Global config** is always available: loaded from `~/.lefthook.yaml` if it exists, otherwise a built-in default is used in memory
1. **No config at all** (no global, no repo, no adapter): hook is skipped silently
2. **Both configs exist** (`~/.lefthook.yaml` + `$REPO/lefthook.yaml`): merges global and repo configs, runs `lefthook run <hook>` with `LEFTHOOK_CONFIG` pointing to the merged temp file
3. **Global only** (no repo config or adapter): runs `lefthook run <hook>` with the global config
4. **No repo config, but adapter detected**: generates a dynamic lefthook config from the adapter, merges it with the global config, and runs `lefthook run <hook>`
4. **Repo/adapter only** (no global config): runs `lefthook run <hook>` with the repo or adapter config
5. **No repo config, but adapter detected**: generates a dynamic lefthook config from the adapter, merges it with the global config (if present), and runs `lefthook run <hook>`

### Adapters

Expand Down
1 change: 0 additions & 1 deletion lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ output:
- success
- failure
pre-commit:
parallel: true
jobs:
- name: fmt
stage_fixed: true
Expand Down
1 change: 1 addition & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
max_width = 120
62 changes: 12 additions & 50 deletions src/adapters/hooks_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ pub struct HooksDirAdapter;

/// Return the first hooks directory name that exists as a directory under `root`.
fn find_hooks_dir(root: &Path) -> Option<&'static str> {
HOOKS_DIR_NAMES
.iter()
.copied()
.find(|name| root.join(name).is_dir())
HOOKS_DIR_NAMES.iter().copied().find(|name| root.join(name).is_dir())
}

/// Collect sorted filenames from `hooks_dir` that match `hook_name` exactly
Expand Down Expand Up @@ -144,10 +141,7 @@ mod tests {
let config = adapter().generate_config(dir.path(), "pre-commit").unwrap();
let out = serde_yaml::to_string(&config).unwrap();
assert!(out.contains(".hooks/pre-commit"), "uses .hooks: {out}");
assert!(
!out.contains("git-hooks/pre-commit"),
"does not use git-hooks: {out}"
);
assert!(!out.contains("git-hooks/pre-commit"), "does not use git-hooks: {out}");
}

#[test]
Expand All @@ -173,10 +167,7 @@ mod tests {
let config = adapter().generate_config(dir.path(), "pre-commit").unwrap();
let out = serde_yaml::to_string(&config).unwrap();
assert!(out.contains("pre-commit:"), "has hook key: {out}");
assert!(
out.contains("git-hooks/pre-commit"),
"has run command: {out}"
);
assert!(out.contains("git-hooks/pre-commit"), "has run command: {out}");
}

#[test]
Expand All @@ -185,11 +176,7 @@ mod tests {
let hooks_dir = dir.path().join(".hooks");
fs::create_dir_all(&hooks_dir).unwrap();

assert!(
adapter()
.generate_config(dir.path(), "pre-commit")
.is_none()
);
assert!(adapter().generate_config(dir.path(), "pre-commit").is_none());
}

#[test]
Expand All @@ -210,11 +197,7 @@ mod tests {
assert!(out.contains("commit-msg:"), "has hook key: {out}");
assert!(out.contains(".hooks/commit-msg"), "has run command: {out}");

assert!(
adapter()
.generate_config(dir.path(), "pre-commit")
.is_none()
);
assert!(adapter().generate_config(dir.path(), "pre-commit").is_none());
}

#[test]
Expand All @@ -232,23 +215,14 @@ mod tests {
let config = adapter().generate_config(dir.path(), "pre-commit").unwrap();
let out = serde_yaml::to_string(&config).unwrap();
assert!(out.contains("hooks-dir:"), "has exact match cmd: {out}");
assert!(
out.contains("hooks-dir-checkstyle:"),
"has checkstyle cmd: {out}"
);
assert!(out.contains("hooks-dir-checkstyle:"), "has checkstyle cmd: {out}");
assert!(out.contains("hooks-dir-detekt:"), "has detekt cmd: {out}");
assert!(
out.contains(".hooks/pre-commit-checkstyle"),
"has checkstyle run: {out}"
);
assert!(
out.contains(".hooks/pre-commit-detekt"),
"has detekt run: {out}"
);
assert!(
!out.contains("pre-push"),
"should not contain pre-push: {out}"
);
assert!(out.contains(".hooks/pre-commit-detekt"), "has detekt run: {out}");
assert!(!out.contains("pre-push"), "should not contain pre-push: {out}");
}

#[test]
Expand All @@ -262,14 +236,8 @@ mod tests {
let out = serde_yaml::to_string(&config).unwrap();
assert!(out.contains("pre-commit:"), "has hook key: {out}");
assert!(out.contains("hooks-dir-ktlint:"), "has ktlint cmd: {out}");
assert!(
out.contains(".hooks/pre-commit-ktlint"),
"has ktlint run: {out}"
);
assert!(
!out.contains("hooks-dir:\n"),
"should not have exact match cmd: {out}"
);
assert!(out.contains(".hooks/pre-commit-ktlint"), "has ktlint run: {out}");
assert!(!out.contains("hooks-dir:\n"), "should not have exact match cmd: {out}");
}

#[test]
Expand All @@ -284,10 +252,7 @@ mod tests {
let out = serde_yaml::to_string(&config).unwrap();
assert!(out.contains("hooks-dir:"), "has exact match cmd: {out}");
assert!(out.contains("hooks-dir-detekt:"), "has detekt cmd: {out}");
assert!(
out.contains("git-hooks/pre-push"),
"uses git-hooks path: {out}"
);
assert!(out.contains("git-hooks/pre-push"), "uses git-hooks path: {out}");
assert!(
out.contains("git-hooks/pre-push-detekt"),
"uses git-hooks path for prefixed: {out}"
Expand Down Expand Up @@ -323,10 +288,7 @@ mod tests {
fs::write(hooks_dir.join("pre-commit"), "#!/bin/sh\n").unwrap();

let scripts = matching_scripts(&hooks_dir, "pre-commit");
assert_eq!(
scripts,
vec!["pre-commit", "pre-commit-aaa", "pre-commit-zzz"]
);
assert_eq!(scripts, vec!["pre-commit", "pre-commit-aaa", "pre-commit-zzz"]);
}

#[test]
Expand Down
15 changes: 3 additions & 12 deletions src/adapters/husky.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ impl Adapter for HuskyAdapter {
return None;
}

let config =
format!("{hook_name}:\n commands:\n husky:\n run: .husky/{hook_name}\n");
let config = format!("{hook_name}:\n commands:\n husky:\n run: .husky/{hook_name}\n");
serde_yaml::from_str(&config).ok()
}
}
Expand Down Expand Up @@ -78,11 +77,7 @@ mod tests {
let husky_dir = dir.path().join(".husky");
fs::create_dir_all(&husky_dir).unwrap();

assert!(
adapter()
.generate_config(dir.path(), "pre-commit")
.is_none()
);
assert!(adapter().generate_config(dir.path(), "pre-commit").is_none());
}

#[test]
Expand All @@ -103,10 +98,6 @@ mod tests {
assert!(out.contains("commit-msg:"), "has hook key: {out}");
assert!(out.contains(".husky/commit-msg"), "has run command: {out}");

assert!(
adapter()
.generate_config(dir.path(), "pre-commit")
.is_none()
);
assert!(adapter().generate_config(dir.path(), "pre-commit").is_none());
}
}
29 changes: 5 additions & 24 deletions src/adapters/pre_commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,11 +479,7 @@ repos:
"#,
);

assert!(
adapter()
.generate_config(dir.path(), "pre-commit")
.is_none()
);
assert!(adapter().generate_config(dir.path(), "pre-commit").is_none());
}

#[test]
Expand Down Expand Up @@ -561,11 +557,7 @@ repos:
"#,
);

assert!(
adapter()
.generate_config(dir.path(), "pre-commit")
.is_some()
);
assert!(adapter().generate_config(dir.path(), "pre-commit").is_some());
assert!(adapter().generate_config(dir.path(), "pre-push").is_none());
}

Expand All @@ -586,11 +578,7 @@ repos:
"#,
);

assert!(
adapter()
.generate_config(dir.path(), "pre-commit")
.is_none()
);
assert!(adapter().generate_config(dir.path(), "pre-commit").is_none());
assert!(adapter().generate_config(dir.path(), "pre-push").is_some());
}

Expand All @@ -613,10 +601,7 @@ repos:

let config = adapter().generate_config(dir.path(), "pre-commit").unwrap();
let out = serde_yaml::to_string(&config).unwrap();
assert!(
out.contains("eslint --fix {staged_files}"),
"entry+args: {out}"
);
assert!(out.contains("eslint --fix {staged_files}"), "entry+args: {out}");
assert!(out.contains("js"), "glob has js: {out}");
assert!(out.contains("ts"), "glob has ts: {out}");
assert!(out.contains("jsx"), "glob has jsx: {out}");
Expand All @@ -627,10 +612,6 @@ repos:
fn test_generate_config_empty_repos() {
let dir = tempfile::tempdir().unwrap();
write_config(dir.path(), "repos: []\n");
assert!(
adapter()
.generate_config(dir.path(), "pre-commit")
.is_none()
);
assert!(adapter().generate_config(dir.path(), "pre-commit").is_none());
}
}
Loading