ReadFileFromHEAD reads a file from git HEAD using a pre-computed repository root. filePath is resolved with filepath.Abs, so relative paths are interpreted from the current process working directory (not gitRoot). Prefer passing an absolute path within gitRoot, such as filepath.Join(gitRoot, "path/t
(filePath, gitRoot string)
| 148 | // within gitRoot, such as filepath.Join(gitRoot, "path/to/file"). |
| 149 | // Use this when the caller already knows the git root (e.g. from a cached value). |
| 150 | func ReadFileFromHEAD(filePath, gitRoot string) (string, error) { |
| 151 | if gitRoot == "" { |
| 152 | return "", fmt.Errorf("gitRoot must not be empty when reading %q from HEAD", filePath) |
| 153 | } |
| 154 | |
| 155 | absPath, err := filepath.Abs(filePath) |
| 156 | if err != nil { |
| 157 | return "", fmt.Errorf("cannot resolve absolute path for %q: %w", filePath, err) |
| 158 | } |
| 159 | |
| 160 | // git show requires the path to be relative to the repository root and to use |
| 161 | // forward slashes even on Windows. |
| 162 | relPath, err := filepath.Rel(gitRoot, absPath) |
| 163 | if err != nil { |
| 164 | return "", fmt.Errorf("cannot compute path of %q relative to git root %q: %w", absPath, gitRoot, err) |
| 165 | } |
| 166 | |
| 167 | // Reject paths that escape the repository (e.g. "../secret"). |
| 168 | if strings.HasPrefix(relPath, "..") { |
| 169 | return "", fmt.Errorf("path %q is outside the git repository root %q", filePath, gitRoot) |
| 170 | } |
| 171 | |
| 172 | relPath = filepath.ToSlash(relPath) |
| 173 | |
| 174 | gitutilLog.Printf("Reading %q from git HEAD (relative path: %s)", filePath, relPath) |
| 175 | |
| 176 | cmd := exec.Command("git", "-C", gitRoot, "show", "HEAD:"+relPath) |
| 177 | output, err := cmd.Output() |
| 178 | if err != nil { |
| 179 | gitutilLog.Printf("File %q not found in HEAD commit: %v", filePath, err) |
| 180 | return "", fmt.Errorf("file %q not found in HEAD commit: %w", filePath, err) |
| 181 | } |
| 182 | return string(output), nil |
| 183 | } |