Skip to content

feat(git): support GIT_DIR/GIT_WORK_TREE for bare-repo dotfile managers#847

Merged
jdx merged 3 commits intomainfrom
fix/git-dir-work-tree-env-vars
Apr 19, 2026
Merged

feat(git): support GIT_DIR/GIT_WORK_TREE for bare-repo dotfile managers#847
jdx merged 3 commits intomainfrom
fix/git-dir-work-tree-env-vars

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Apr 19, 2026

Summary

  • Honor GIT_DIR / GIT_WORK_TREE env vars during repository discovery so hk works with YADM and other bare-repo dotfile managers where there is no .git in the work tree
  • Fall back to shell-git when libgit2 opens a bare repo (libgit2 refuses status/diff on bare repos even with a GIT_WORK_TREE override)
  • hk builtins no longer loads project settings, so it works outside a repo instead of panicking

Fixes #831.

Changes

  • src/git.rsGit::new() uses Repository::open_from_env() when either env var is set; bare repos fall back to shell git
  • src/git_util.rsfind_git_path() returns $GIT_DIR when set; new find_work_tree_root() helper
  • src/tera.rs, src/test_runner.rs — use find_work_tree_root() instead of walking up for .git
  • src/cli/mod.rs — add Commands::Builtins to the settings-skip list

Test plan

  • hk check, hk fix, hk install, hk uninstall, hk builtins all work against a bare repo + GIT_WORK_TREE setup
  • Verified on both HK_LIBGIT2=1 (default) and HK_LIBGIT2=0
  • Regression-tested against a normal (non-bare) repo

🤖 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 hk to operate in bare-repo dotfile-manager setups (e.g., YADM) by honoring GIT_DIR/GIT_WORK_TREE, normalizing relative git env vars, and always cding to the effective work-tree root during Git::new().

When libgit2 opens a bare repo, git status/diff now falls back to shell git to keep operations working. find_git_path() and a new find_work_tree_root() helper are added/used to resolve roots consistently, and hk builtins now skips project settings loading so it runs outside a repo. Adds bats regression 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.

jdx and others added 2 commits April 18, 2026 19:17
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]>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/git.rs Outdated
Comment on lines +114 to +120
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)?;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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)?;

Comment thread src/git_util.rs Outdated
/// (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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
let cwd = std::env::current_dir().unwrap_or_default();
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 19, 2026

Greptile Summary

This PR adds support for bare-repo dotfile managers (YADM, etc.) by honoring GIT_DIR/GIT_WORK_TREE env vars during repository discovery, falling back to shell git when libgit2 encounters a bare repo, and skipping project-settings loading for hk builtins. The previous reviewer concern about CWD not being set for non-bare env-var repos is addressed by the fix commit (always cd to work tree root when GIT_DIR/GIT_WORK_TREE is set) and covered by a new bats regression test.

Confidence Score: 5/5

Safe 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

Filename Overview
src/git.rs Git::new() now absolutizes GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE, uses open_from_env() when env vars are set, and falls back to shell git for bare repos; uses set_var in a live tokio runtime (technically unsound).
src/git_util.rs New find_work_tree_root() helper correctly honors GIT_WORK_TREE and falls back to walking up for .git; find_git_path() updated to return GIT_DIR when set.
src/tera.rs BASE_CONTEXT now derives root via find_work_tree_root() which correctly handles GIT_WORK_TREE; LazyLock initialized after Git::new() sets CWD so ordering is safe in practice.
src/cli/mod.rs Commands::Builtins added to the settings-skip list so it no longer tries to load project config, allowing it to run outside a repo without panicking.
test/bare_repo_env_vars.bats New Bats regression suite covering builtins (outside repo), check (libgit2 on/off), install/uninstall to bare hooks dir, and the previously-flagged subdirectory path-resolution bug.

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
Loading

Fix All in Claude Code

Reviews (2): Last reviewed commit: "fix(git): always cd to work tree root wh..." | Re-trigger Greptile

Comment thread src/git.rs Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread src/git.rs Outdated
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]>
@jdx jdx merged commit ad8cf88 into main Apr 19, 2026
20 checks passed
@jdx jdx deleted the fix/git-dir-work-tree-env-vars branch April 19, 2026 00:58
@jdx jdx mentioned this pull request Apr 19, 2026
jdx added a commit that referenced this pull request Apr 23, 2026
### 🚀 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant