mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 11:13:33 +01:00
Searching for `.git/` in file paths incorrectly matched directory names ending with `.git` (e.g., `personal.git/cheat/hello`), causing sheets under such paths to be silently skipped. Fix by requiring the path separator on both sides (`/.git/`), so `.git` is only matched as a complete path component. Rewrites test suite with comprehensive coverage for all six documented edge cases, including the #711 scenario and combination cases (e.g., a real .git directory inside a .git-suffixed parent). Closes #711 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
356 lines
9.8 KiB
Go
356 lines
9.8 KiB
Go
package repo
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// setupGitDirTestTree creates a temporary directory structure that exercises
|
|
// every case documented in GitDir's comment block. The caller must defer
|
|
// os.RemoveAll on the returned root.
|
|
//
|
|
// Layout:
|
|
//
|
|
// root/
|
|
// ├── plain/ # not a repository
|
|
// │ └── sheet
|
|
// ├── repo/ # a repository (.git is a directory)
|
|
// │ ├── .git/
|
|
// │ │ ├── HEAD
|
|
// │ │ ├── objects/
|
|
// │ │ │ └── pack/
|
|
// │ │ └── refs/
|
|
// │ │ └── heads/
|
|
// │ ├── .gitignore
|
|
// │ ├── .gitattributes
|
|
// │ └── sheet
|
|
// ├── submodule/ # a submodule (.git is a file)
|
|
// │ ├── .git # file, not directory
|
|
// │ └── sheet
|
|
// ├── dotgit-suffix.git/ # directory name ends in .git (#711)
|
|
// │ └── cheat/
|
|
// │ └── sheet
|
|
// ├── dotgit-mid.git/ # .git suffix mid-path (#711)
|
|
// │ └── nested/
|
|
// │ └── sheet
|
|
// ├── .github/ # .github directory (not .git)
|
|
// │ └── workflows/
|
|
// │ └── ci.yml
|
|
// └── .hidden/ # generic hidden directory
|
|
// └── sheet
|
|
func setupGitDirTestTree(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
root := t.TempDir()
|
|
|
|
dirs := []string{
|
|
// case 1: not a repository
|
|
filepath.Join(root, "plain"),
|
|
|
|
// case 2: a repository (.git directory with contents)
|
|
filepath.Join(root, "repo", ".git", "objects", "pack"),
|
|
filepath.Join(root, "repo", ".git", "refs", "heads"),
|
|
|
|
// case 4: a submodule (.git is a file)
|
|
filepath.Join(root, "submodule"),
|
|
|
|
// case 6: directory name ending in .git (#711)
|
|
filepath.Join(root, "dotgit-suffix.git", "cheat"),
|
|
filepath.Join(root, "dotgit-mid.git", "nested"),
|
|
|
|
// .github (should not be confused with .git)
|
|
filepath.Join(root, ".github", "workflows"),
|
|
|
|
// generic hidden directory
|
|
filepath.Join(root, ".hidden"),
|
|
}
|
|
|
|
for _, dir := range dirs {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
t.Fatalf("failed to create dir %s: %v", dir, err)
|
|
}
|
|
}
|
|
|
|
files := map[string]string{
|
|
// sheets
|
|
filepath.Join(root, "plain", "sheet"): "plain sheet",
|
|
filepath.Join(root, "repo", "sheet"): "repo sheet",
|
|
filepath.Join(root, "submodule", "sheet"): "submod sheet",
|
|
filepath.Join(root, "dotgit-suffix.git", "cheat", "sheet"): "dotgit sheet",
|
|
filepath.Join(root, "dotgit-mid.git", "nested", "sheet"): "dotgit nested",
|
|
filepath.Join(root, ".hidden", "sheet"): "hidden sheet",
|
|
|
|
// git metadata
|
|
filepath.Join(root, "repo", ".git", "HEAD"): "ref: refs/heads/main\n",
|
|
filepath.Join(root, "repo", ".gitignore"): "*.tmp\n",
|
|
filepath.Join(root, "repo", ".gitattributes"): "* text=auto\n",
|
|
filepath.Join(root, "submodule", ".git"): "gitdir: ../.git/modules/sub\n",
|
|
filepath.Join(root, ".github", "workflows", "ci.yml"): "name: CI\n",
|
|
}
|
|
|
|
for path, content := range files {
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
t.Fatalf("failed to write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
return root
|
|
}
|
|
|
|
func TestGitDir(t *testing.T) {
|
|
root := setupGitDirTestTree(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
want bool
|
|
}{
|
|
// Case 1: not a repository — no .git anywhere in path
|
|
{
|
|
name: "plain directory, no repo",
|
|
path: filepath.Join(root, "plain", "sheet"),
|
|
want: false,
|
|
},
|
|
|
|
// Case 2: a repository — paths *inside* .git/ should be detected
|
|
{
|
|
name: "inside .git directory",
|
|
path: filepath.Join(root, "repo", ".git", "HEAD"),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "inside .git/objects",
|
|
path: filepath.Join(root, "repo", ".git", "objects", "pack", "somefile"),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "inside .git/refs",
|
|
path: filepath.Join(root, "repo", ".git", "refs", "heads", "main"),
|
|
want: true,
|
|
},
|
|
|
|
// Case 2 (cont.): files *alongside* .git should NOT be detected
|
|
{
|
|
name: "sheet in repo root (beside .git dir)",
|
|
path: filepath.Join(root, "repo", "sheet"),
|
|
want: false,
|
|
},
|
|
|
|
// Case 3: .git* files (like .gitignore) should NOT trigger
|
|
{
|
|
name: ".gitignore file",
|
|
path: filepath.Join(root, "repo", ".gitignore"),
|
|
want: false,
|
|
},
|
|
{
|
|
name: ".gitattributes file",
|
|
path: filepath.Join(root, "repo", ".gitattributes"),
|
|
want: false,
|
|
},
|
|
|
|
// Case 4: submodule — .git is a file, not a directory
|
|
{
|
|
name: "sheet in submodule (where .git is a file)",
|
|
path: filepath.Join(root, "submodule", "sheet"),
|
|
want: false,
|
|
},
|
|
|
|
// Case 6: directory name ends with .git (#711)
|
|
{
|
|
name: "sheet under directory ending in .git",
|
|
path: filepath.Join(root, "dotgit-suffix.git", "cheat", "sheet"),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "sheet under .git-suffixed dir, nested deeper",
|
|
path: filepath.Join(root, "dotgit-mid.git", "nested", "sheet"),
|
|
want: false,
|
|
},
|
|
|
|
// .github directory — must not be confused with .git
|
|
{
|
|
name: "file inside .github directory",
|
|
path: filepath.Join(root, ".github", "workflows", "ci.yml"),
|
|
want: false,
|
|
},
|
|
|
|
// Hidden directory that is not .git
|
|
{
|
|
name: "file inside generic hidden directory",
|
|
path: filepath.Join(root, ".hidden", "sheet"),
|
|
want: false,
|
|
},
|
|
|
|
// Path with no .git at all
|
|
{
|
|
name: "path with no .git component whatsoever",
|
|
path: filepath.Join(root, "nonexistent", "file"),
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := GitDir(tt.path)
|
|
if err != nil {
|
|
t.Fatalf("GitDir(%q) returned unexpected error: %v", tt.path, err)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("GitDir(%q) = %v, want %v", tt.path, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGitDirWithNestedGitDir tests a repo inside a .git-suffixed parent
|
|
// directory. This is the nastiest combination: a real .git directory that
|
|
// appears *after* a .git suffix in the path.
|
|
func TestGitDirWithNestedGitDir(t *testing.T) {
|
|
root := t.TempDir()
|
|
|
|
// Create: root/cheats.git/repo/.git/HEAD
|
|
// root/cheats.git/repo/sheet
|
|
gitDir := filepath.Join(root, "cheats.git", "repo", ".git")
|
|
if err := os.MkdirAll(gitDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(gitDir, "HEAD"), []byte("ref: refs/heads/main\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "cheats.git", "repo", "sheet"), []byte("content"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "sheet beside .git in .git-suffixed parent",
|
|
path: filepath.Join(root, "cheats.git", "repo", "sheet"),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "file inside .git inside .git-suffixed parent",
|
|
path: filepath.Join(root, "cheats.git", "repo", ".git", "HEAD"),
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := GitDir(tt.path)
|
|
if err != nil {
|
|
t.Fatalf("GitDir(%q) returned unexpected error: %v", tt.path, err)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("GitDir(%q) = %v, want %v", tt.path, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGitDirSubmoduleInsideDotGitSuffix tests a submodule (.git file)
|
|
// inside a .git-suffixed parent directory.
|
|
func TestGitDirSubmoduleInsideDotGitSuffix(t *testing.T) {
|
|
root := t.TempDir()
|
|
|
|
// Create: root/personal.git/submod/.git (file)
|
|
// root/personal.git/submod/sheet
|
|
subDir := filepath.Join(root, "personal.git", "submod")
|
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// .git as a file (submodule pointer)
|
|
if err := os.WriteFile(filepath.Join(subDir, ".git"), []byte("gitdir: ../../.git/modules/sub\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(subDir, "sheet"), []byte("content"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := GitDir(filepath.Join(subDir, "sheet"))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got {
|
|
t.Error("GitDir should return false for sheet in submodule under .git-suffixed parent")
|
|
}
|
|
}
|
|
|
|
// TestGitDirIntegrationWalk simulates what sheets.Load does: walking a
|
|
// directory tree and checking each path with GitDir. This verifies that
|
|
// the function works correctly in the context of filepath.Walk, which is
|
|
// how it is actually called.
|
|
func TestGitDirIntegrationWalk(t *testing.T) {
|
|
root := setupGitDirTestTree(t)
|
|
|
|
// Walk the tree and collect which paths GitDir says to skip
|
|
var skipped []string
|
|
var visited []string
|
|
|
|
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
isGit, err := GitDir(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isGit {
|
|
skipped = append(skipped, path)
|
|
} else {
|
|
visited = append(visited, path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Walk failed: %v", err)
|
|
}
|
|
|
|
// Files inside .git/ should be skipped
|
|
expectSkipped := []string{
|
|
filepath.Join(root, "repo", ".git", "HEAD"),
|
|
}
|
|
for _, want := range expectSkipped {
|
|
found := false
|
|
for _, got := range skipped {
|
|
if got == want {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("expected %q to be skipped, but it was not", want)
|
|
}
|
|
}
|
|
|
|
// Sheets should NOT be skipped — including the #711 case
|
|
expectVisited := []string{
|
|
filepath.Join(root, "plain", "sheet"),
|
|
filepath.Join(root, "repo", "sheet"),
|
|
filepath.Join(root, "submodule", "sheet"),
|
|
filepath.Join(root, "dotgit-suffix.git", "cheat", "sheet"),
|
|
filepath.Join(root, "dotgit-mid.git", "nested", "sheet"),
|
|
filepath.Join(root, ".hidden", "sheet"),
|
|
}
|
|
for _, want := range expectVisited {
|
|
found := false
|
|
for _, got := range visited {
|
|
if got == want {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("expected %q to be visited (not skipped), but it was not found in visited paths", want)
|
|
}
|
|
}
|
|
}
|