package git import ( "fmt" "os/exec" "path/filepath" "strings" ) // GetGitDir returns the actual .git directory path for the current repository. // In a normal repo, this is ".git". In a worktree, .git is a file // containing "gitdir: /path/to/actual/git/dir", so we use git rev-parse. // // This function uses Git's native worktree-aware APIs and should be used // instead of direct filepath.Join(path, ".git") throughout the codebase. func GetGitDir() (string, error) { cmd := exec.Command("git", "rev-parse", "--git-dir") output, err := cmd.Output() if err != nil { return "", fmt.Errorf("not a git repository: %w", err) } return strings.TrimSpace(string(output)), nil } // GetGitHooksDir returns the path to the Git hooks directory. // This function is worktree-aware and handles both regular repos and worktrees. func GetGitHooksDir() (string, error) { gitDir, err := GetGitDir() if err != nil { return "", err } return filepath.Join(gitDir, "hooks"), nil } // GetGitRefsDir returns the path to the Git refs directory. // This function is worktree-aware and handles both regular repos and worktrees. func GetGitRefsDir() (string, error) { gitDir, err := GetGitDir() if err != nil { return "", err } return filepath.Join(gitDir, "refs"), nil } // GetGitHeadPath returns the path to the Git HEAD file. // This function is worktree-aware and handles both regular repos and worktrees. func GetGitHeadPath() (string, error) { gitDir, err := GetGitDir() if err != nil { return "", err } return filepath.Join(gitDir, "HEAD"), nil } // IsWorktree returns true if the current directory is in a Git worktree. // This is determined by comparing --git-dir and --git-common-dir. func IsWorktree() bool { gitDir := getGitDirNoError("--git-dir") if gitDir == "" { return false } commonDir := getGitDirNoError("--git-common-dir") if commonDir == "" { return false } absGit, err1 := filepath.Abs(gitDir) absCommon, err2 := filepath.Abs(commonDir) if err1 != nil || err2 != nil { return false } return absGit != absCommon } // GetMainRepoRoot returns the main repository root directory. // When in a worktree, this returns the main repository root. // Otherwise, it returns the regular repository root. // // For nested worktrees (worktrees located under the main repo, e.g., // /project/.worktrees/feature/), this correctly returns the main repo // root (/project/) by using git rev-parse --git-common-dir which always // points to the main repo's .git directory. (GH#509) func GetMainRepoRoot() (string, error) { // Use --git-common-dir which always returns the main repo's .git directory, // even when running from within a worktree or its subdirectories. // This is the most reliable method for finding the main repo root. commonDir := getGitDirNoError("--git-common-dir") if commonDir == "" { return "", fmt.Errorf("not a git repository") } // Convert to absolute path to handle relative paths correctly absCommonDir, err := filepath.Abs(commonDir) if err != nil { return "", fmt.Errorf("failed to resolve common dir path: %w", err) } // The main repo root is the parent of the .git directory mainRepoRoot := filepath.Dir(absCommonDir) return mainRepoRoot, nil } // getGitDirNoError is a helper that returns empty string on error // to avoid cluttering code with error handling for simple checks. func getGitDirNoError(flag string) string { cmd := exec.Command("git", "rev-parse", flag) out, err := cmd.Output() if err != nil { return "" } return strings.TrimSpace(string(out)) }