feat(git): support GIT_DIR/GIT_WORK_TREE for bare-repo dotfile managers#847
feat(git): support GIT_DIR/GIT_WORK_TREE for bare-repo dotfile managers#847
Conversation
Honor the standard git environment variables during repository discovery so hk works with YADM and other bare-repo dotfile managers where there is no `.git` in the work tree. - `Git::new()` uses `Repository::open_from_env()` when either var is set, and falls back to the shell-git path when the opened repo is bare (libgit2 refuses status/diff on bare repos even when a work tree is provided via `GIT_WORK_TREE`). - `find_git_path()` returns `$GIT_DIR` directly when set so hooks resolve to the bare repository's hooks dir. - `tera.rs` and `test_runner.rs` use a new `find_work_tree_root()` helper that honors `GIT_WORK_TREE`. - `hk builtins` no longer loads project settings, so it works outside a repo instead of panicking. Fixes #831 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Regression test for the GIT_DIR/GIT_WORK_TREE support, exercising both the libgit2 and shell-git code paths. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
Code Review
This pull request introduces support for GIT_DIR and GIT_WORK_TREE environment variables, enabling compatibility with bare-repository managers like YADM. It centralizes work-tree resolution logic in git_util and updates the Git initialization to handle bare repositories by falling back to shell-based Git when libgit2 is insufficient. Feedback was provided regarding the handling of current directory changes when Git environment variables are present, specifically the need to absolute-ify relative paths and ensure the process remains at the repository root to avoid breaking internal operations. Additionally, a suggestion was made to improve error handling when retrieving the current working directory to avoid issues with empty paths.
| if !has_git_env { | ||
| let cwd = std::env::current_dir()?; | ||
| let root = xx::file::find_up(&cwd, &[".git"]) | ||
| .and_then(|p| p.parent().map(|p| p.to_path_buf())) | ||
| .ok_or(eyre!("failed to find git repository"))?; | ||
| std::env::set_current_dir(&root)?; | ||
| } |
There was a problem hiding this comment.
By skipping set_current_dir when GIT_DIR or GIT_WORK_TREE is set, hk remains in the user's current working directory. This breaks many internal operations (like status or all_files) that expect the process to be at the repository root to correctly resolve relative paths returned by Git.
Additionally, if we do change the directory, any relative paths in GIT_DIR or GIT_WORK_TREE must be converted to absolute paths first, otherwise they will point to the wrong location after the cd.
let root = crate::git_util::find_work_tree_root();
if !has_git_env {
let cwd = std::env::current_dir()?;
if xx::file::find_up(&cwd, &[".git"]).is_none() {
return Err(eyre!("failed to find git repository"));
}
}
// Absolute-ify relative git env vars before changing directory
for var in ["GIT_DIR", "GIT_WORK_TREE"] {
if let Some(val) = std::env::var_os(var) {
let p = std::path::Path::new(&val);
if p.is_relative() {
std::env::set_var(var, std::env::current_dir()?.join(p));
}
}
}
std::env::set_current_dir(&root)?;| /// (for bare-repo setups like YADM). Falls back to walking up for `.git`, and | ||
| /// finally to `cwd` if no repository is found. | ||
| pub fn find_work_tree_root() -> PathBuf { | ||
| let cwd = std::env::current_dir().unwrap_or_default(); |
There was a problem hiding this comment.
Using unwrap_or_default() on current_dir() returns an empty PathBuf if the call fails. This can lead to confusing behavior or incorrect path joining later. It's safer to handle the error or at least fall back to . explicitly.
| let cwd = std::env::current_dir().unwrap_or_default(); | |
| let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); |
Greptile SummaryThis PR adds support for bare-repo dotfile managers (YADM, etc.) by honoring Confidence Score: 5/5Safe to merge; the one remaining concern is a narrow theoretical race with set_var in a live tokio runtime, which has very low practical impact. All changes are well-scoped: the previously flagged CWD issue is addressed and regression-tested, the bare-repo fallback logic is clean, and the builtins settings-skip is a safe no-op for existing users. The only outstanding finding is a P2 note about set_var soundness in an async context — not a production blocker. src/git.rs lines 119–129 (unsafe set_var in live tokio runtime) Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Git::new] --> B{GIT_DIR or\nGIT_WORK_TREE set?}
B -- yes --> C[Absolutize relative\nGIT_DIR / GIT_WORK_TREE\n/ GIT_INDEX_FILE via set_var]
C --> D[root = find_work_tree_root\nhonors GIT_WORK_TREE]
B -- no --> E[root = walk up for .git\nparent dir]
D --> F[set_current_dir to root]
E --> F
F --> G{HK_LIBGIT2?}
G -- yes --> H{has_git_env?}
H -- yes --> I[Repository::open_from_env]
H -- no --> J[Repository::open dot]
I --> K{repo.is_bare?}
J --> K
K -- yes --> L[repo = None\nshell-git fallback]
K -- no --> M[Apply GIT_INDEX_FILE\nif set, repo = Some]
G -- no --> L
L --> N[Git struct created]
M --> N
Reviews (2): Last reviewed commit: "fix(git): always cd to work tree root wh..." | Re-trigger Greptile |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4d3c390. Configure here.
Addresses code-review feedback on #847. Skipping set_current_dir in the env-var branch left cwd pointing at a subdirectory of the work tree, which broke path.exists() filtering in status() — modified files silently disappeared from hk's file list when running from a subdir. Always cd to the work tree root after absolutizing relative GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE values (so libgit2 and shell-git resolve them correctly after the cwd change). Adds a regression test that modifies a file at the work-tree root, runs hk check from a subdirectory, and asserts the file is processed. The test fails on both HK_LIBGIT2=1 and HK_LIBGIT2=0 without this fix. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
### 🚀 Features - **(check)** implement --plan, --why, and --json by [@jdx](https://github.com/jdx) in [#848](#848) - **(cocogitto)** add cocogitto conventional commits config to hk builtin config by [@hituzi-no-sippo](https://github.com/hituzi-no-sippo) in [#838](#838) - **(git)** support GIT_DIR/GIT_WORK_TREE for bare-repo dotfile managers by [@jdx](https://github.com/jdx) in [#847](#847) - **(install)** use Git 2.54 config-based hooks with --global support by [@jdx](https://github.com/jdx) in [#853](#853) ### 🐛 Bug Fixes - use text progress in CI by [@jdx](https://github.com/jdx) in [#845](#845) ### 📚 Documentation - generalize agent guidelines by [@jdx](https://github.com/jdx) in [#846](#846) - add releases nav and aube lock by [@jdx](https://github.com/jdx) in [#849](#849) ### 🔍 Other Changes - bump communique to 1.0.1 by [@jdx](https://github.com/jdx) in [#850](#850) ### 📦️ Dependency Updates - update actions-rust-lang/setup-rust-toolchain digest to 2b1f5e9 by [@renovate[bot]](https://github.com/renovate[bot]) in [#832](#832) - update anthropics/claude-code-action digest to c3d45e8 by [@renovate[bot]](https://github.com/renovate[bot]) in [#833](#833) - update rust crate tokio to v1.52.1 by [@renovate[bot]](https://github.com/renovate[bot]) in [#834](#834) - update actions/upload-pages-artifact action to v5 by [@renovate[bot]](https://github.com/renovate[bot]) in [#835](#835) - update taiki-e/upload-rust-binary-action digest to f0d45ae by [@renovate[bot]](https://github.com/renovate[bot]) in [#839](#839) - update rust crate clx to v2 by [@renovate[bot]](https://github.com/renovate[bot]) in [#836](#836) - update anthropics/claude-code-action digest to 0d2971c by [@renovate[bot]](https://github.com/renovate[bot]) in [#841](#841) - update anthropics/claude-code-action digest to 38ec876 by [@renovate[bot]](https://github.com/renovate[bot]) in [#842](#842) - lock file maintenance by [@renovate[bot]](https://github.com/renovate[bot]) in [#851](#851) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk release bookkeeping: version bumps and doc/CLI artifact updates, plus minor dependency patch updates in `Cargo.lock`. No functional Rust source changes are included in this diff. > > **Overview** > Bumps `hk` to **v1.44.0** and publishes the corresponding release notes in `CHANGELOG.md`. > > Updates generated/packaged artifacts to match the new version (CLI docs/specs and Pkl package URLs in docs/examples), and refreshes `Cargo.lock` for the release (including patch-level updates like `rustls` and `winnow`). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a36c7a6. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: mise-en-dev <[email protected]>

Summary
GIT_DIR/GIT_WORK_TREEenv vars during repository discovery so hk works with YADM and other bare-repo dotfile managers where there is no.gitin the work treeGIT_WORK_TREEoverride)hk builtinsno longer loads project settings, so it works outside a repo instead of panickingFixes #831.
Changes
src/git.rs—Git::new()usesRepository::open_from_env()when either env var is set; bare repos fall back to shell gitsrc/git_util.rs—find_git_path()returns$GIT_DIRwhen set; newfind_work_tree_root()helpersrc/tera.rs,src/test_runner.rs— usefind_work_tree_root()instead of walking up for.gitsrc/cli/mod.rs— addCommands::Builtinsto the settings-skip listTest plan
hk check,hk fix,hk install,hk uninstall,hk builtinsall work against a bare repo +GIT_WORK_TREEsetupHK_LIBGIT2=1(default) andHK_LIBGIT2=0🤖 Generated with Claude Code
Note
Medium Risk
Moderate risk because it changes repository discovery and working-directory behavior in
Git::new(), which can affect path resolution and git operations across many commands; mitigated by explicit fallbacks and new regression tests.Overview
Enables
hkto operate in bare-repo dotfile-manager setups (e.g., YADM) by honoringGIT_DIR/GIT_WORK_TREE, normalizing relative git env vars, and alwayscding to the effective work-tree root duringGit::new().When libgit2 opens a bare repo, git status/diff now falls back to shell
gitto keep operations working.find_git_path()and a newfind_work_tree_root()helper are added/used to resolve roots consistently, andhk builtinsnow skips project settings loading so it runs outside a repo. Addsbatsregression tests covering check/install/uninstall and subdirectory execution under bare-repo env vars.Reviewed by Cursor Bugbot for commit 3799316. Bugbot is set up for automated code reviews on this repo. Configure here.