|
1 | 1 | package cmd |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "errors" |
4 | 5 | "os" |
5 | 6 | "path/filepath" |
6 | 7 | "strings" |
7 | 8 | "testing" |
8 | 9 |
|
| 10 | + "github.com/go-git/go-billy/v5/osfs" |
| 11 | + "github.com/go-git/go-git/v5" |
9 | 12 | "github.com/go-git/go-git/v5/plumbing" |
10 | 13 | "github.com/zhaochunqi/git-open/internal/testhelper" |
11 | 14 | ) |
@@ -75,3 +78,223 @@ func Test_resolveWebURL_WorktreeConfigExtension(t *testing.T) { |
75 | 78 | t.Errorf("getBranchName() = %q, want %q", branch, "main") |
76 | 79 | } |
77 | 80 | } |
| 81 | + |
| 82 | +// Regression test: the extension-tolerant fallback must also work from a |
| 83 | +// linked worktree, where .git is a file pointing to a git directory that |
| 84 | +// references a commondir. |
| 85 | +func Test_openRepositoryToleratingExtensions_Worktree(t *testing.T) { |
| 86 | + worktreeDir, cleanup := testhelper.SetupTestWorktree(t, "https://github.com/zhaochunqi/git-open.git", "feature-branch") |
| 87 | + defer cleanup() |
| 88 | + |
| 89 | + // Locate the main repository's common git directory through the |
| 90 | + // worktree's .git file and its commondir pointer. |
| 91 | + b, err := os.ReadFile(filepath.Join(worktreeDir, ".git")) |
| 92 | + if err != nil { |
| 93 | + t.Fatalf("read .git file failed: %v", err) |
| 94 | + } |
| 95 | + gitdir := strings.TrimSpace(strings.TrimPrefix(string(b), "gitdir: ")) |
| 96 | + cb, err := os.ReadFile(filepath.Join(gitdir, "commondir")) |
| 97 | + if err != nil { |
| 98 | + t.Fatalf("read commondir failed: %v", err) |
| 99 | + } |
| 100 | + commonDir := strings.TrimSpace(string(cb)) |
| 101 | + if !filepath.IsAbs(commonDir) { |
| 102 | + commonDir = filepath.Join(gitdir, commonDir) |
| 103 | + } |
| 104 | + |
| 105 | + // Enable an extension unknown to go-git in the common config, so the |
| 106 | + // strict PlainOpenWithOptions path fails and the fallback is exercised. |
| 107 | + configPath := filepath.Join(commonDir, "config") |
| 108 | + f, err := os.OpenFile(configPath, os.O_APPEND|os.O_WRONLY, 0644) |
| 109 | + if err != nil { |
| 110 | + t.Fatalf("open config failed: %v", err) |
| 111 | + } |
| 112 | + if _, err := f.WriteString("[extensions]\n\tworktreeConfig = true\n"); err != nil { |
| 113 | + f.Close() |
| 114 | + t.Fatalf("append extensions section failed: %v", err) |
| 115 | + } |
| 116 | + if err := f.Close(); err != nil { |
| 117 | + t.Fatalf("close config failed: %v", err) |
| 118 | + } |
| 119 | + |
| 120 | + repo, err := getCurrentGitDirectory() |
| 121 | + if err != nil { |
| 122 | + t.Fatalf("getCurrentGitDirectory() error in worktree with extensions = %v", err) |
| 123 | + } |
| 124 | + |
| 125 | + branch, err := getBranchName(repo) |
| 126 | + if err != nil { |
| 127 | + t.Fatalf("getBranchName() error = %v", err) |
| 128 | + } |
| 129 | + if branch != "feature-branch" { |
| 130 | + t.Errorf("getBranchName() = %q, want %q", branch, "feature-branch") |
| 131 | + } |
| 132 | + |
| 133 | + remoteURL, err := getRemoteURL(repo) |
| 134 | + if err != nil { |
| 135 | + t.Fatalf("getRemoteURL() error = %v", err) |
| 136 | + } |
| 137 | + if remoteURL != "https://github.com/zhaochunqi/git-open.git" { |
| 138 | + t.Errorf("getRemoteURL() = %q, want %q", remoteURL, "https://github.com/zhaochunqi/git-open.git") |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +func Test_dotGitFilesystems_NotFound(t *testing.T) { |
| 143 | + // A plain temporary directory has no .git anywhere up to the filesystem |
| 144 | + // root, so the lookup must report ErrRepositoryNotExists. |
| 145 | + tmpDir := t.TempDir() |
| 146 | + |
| 147 | + _, _, err := dotGitFilesystems(tmpDir) |
| 148 | + if !errors.Is(err, git.ErrRepositoryNotExists) { |
| 149 | + t.Errorf("dotGitFilesystems() error = %v, want %v", err, git.ErrRepositoryNotExists) |
| 150 | + } |
| 151 | +} |
| 152 | + |
| 153 | +func Test_dotGitFileToFilesystem(t *testing.T) { |
| 154 | + absGitdir := t.TempDir() |
| 155 | + |
| 156 | + tests := []struct { |
| 157 | + name string |
| 158 | + content string |
| 159 | + wantRoot string |
| 160 | + wantErr bool |
| 161 | + }{ |
| 162 | + { |
| 163 | + name: "relative gitdir", |
| 164 | + content: "gitdir: .git-real\n", |
| 165 | + wantRoot: ".git-real", |
| 166 | + }, |
| 167 | + { |
| 168 | + name: "absolute gitdir", |
| 169 | + content: "gitdir: " + absGitdir + "\n", |
| 170 | + wantRoot: absGitdir, |
| 171 | + }, |
| 172 | + { |
| 173 | + name: "missing gitdir prefix", |
| 174 | + content: "not a gitdir pointer\n", |
| 175 | + wantErr: true, |
| 176 | + }, |
| 177 | + } |
| 178 | + |
| 179 | + for _, tt := range tests { |
| 180 | + t.Run(tt.name, func(t *testing.T) { |
| 181 | + tmpDir := t.TempDir() |
| 182 | + if err := os.WriteFile(filepath.Join(tmpDir, ".git"), []byte(tt.content), 0644); err != nil { |
| 183 | + t.Fatal(err) |
| 184 | + } |
| 185 | + |
| 186 | + fs, err := dotGitFileToFilesystem(tmpDir, osfs.New(tmpDir)) |
| 187 | + if (err != nil) != tt.wantErr { |
| 188 | + t.Fatalf("dotGitFileToFilesystem() error = %v, wantErr %v", err, tt.wantErr) |
| 189 | + } |
| 190 | + if tt.wantErr { |
| 191 | + return |
| 192 | + } |
| 193 | + |
| 194 | + want := tt.wantRoot |
| 195 | + if !filepath.IsAbs(want) { |
| 196 | + want = filepath.Join(tmpDir, want) |
| 197 | + } |
| 198 | + // osfs.Root resolves symlinks (e.g. /var -> /private/var on macOS). |
| 199 | + if resolved, err := filepath.EvalSymlinks(want); err == nil { |
| 200 | + want = resolved |
| 201 | + } |
| 202 | + if fs.Root() != want { |
| 203 | + t.Errorf("dotGitFileToFilesystem() root = %q, want %q", fs.Root(), want) |
| 204 | + } |
| 205 | + }) |
| 206 | + } |
| 207 | +} |
| 208 | + |
| 209 | +func Test_dotGitFileToFilesystem_MissingFile(t *testing.T) { |
| 210 | + tmpDir := t.TempDir() |
| 211 | + |
| 212 | + _, err := dotGitFileToFilesystem(tmpDir, osfs.New(tmpDir)) |
| 213 | + if err == nil { |
| 214 | + t.Error("dotGitFileToFilesystem() expected error for missing .git file, got nil") |
| 215 | + } |
| 216 | +} |
| 217 | + |
| 218 | +func Test_dotGitCommonDirectory(t *testing.T) { |
| 219 | + t.Run("no commondir file", func(t *testing.T) { |
| 220 | + fs := osfs.New(t.TempDir()) |
| 221 | + common, err := dotGitCommonDirectory(fs) |
| 222 | + if err != nil { |
| 223 | + t.Fatalf("dotGitCommonDirectory() error = %v", err) |
| 224 | + } |
| 225 | + if common != nil { |
| 226 | + t.Errorf("dotGitCommonDirectory() = %v, want nil", common) |
| 227 | + } |
| 228 | + }) |
| 229 | + |
| 230 | + t.Run("empty commondir file", func(t *testing.T) { |
| 231 | + tmpDir := t.TempDir() |
| 232 | + if err := os.WriteFile(filepath.Join(tmpDir, "commondir"), nil, 0644); err != nil { |
| 233 | + t.Fatal(err) |
| 234 | + } |
| 235 | + common, err := dotGitCommonDirectory(osfs.New(tmpDir)) |
| 236 | + if err != nil { |
| 237 | + t.Fatalf("dotGitCommonDirectory() error = %v", err) |
| 238 | + } |
| 239 | + if common != nil { |
| 240 | + t.Errorf("dotGitCommonDirectory() = %v, want nil", common) |
| 241 | + } |
| 242 | + }) |
| 243 | + |
| 244 | + t.Run("relative commondir", func(t *testing.T) { |
| 245 | + tmpDir := t.TempDir() |
| 246 | + gitdir := filepath.Join(tmpDir, "gitdir") |
| 247 | + commonDir := filepath.Join(tmpDir, "common") |
| 248 | + for _, dir := range []string{gitdir, commonDir} { |
| 249 | + if err := os.MkdirAll(dir, 0755); err != nil { |
| 250 | + t.Fatal(err) |
| 251 | + } |
| 252 | + } |
| 253 | + if err := os.WriteFile(filepath.Join(gitdir, "commondir"), []byte("../common\n"), 0644); err != nil { |
| 254 | + t.Fatal(err) |
| 255 | + } |
| 256 | + common, err := dotGitCommonDirectory(osfs.New(gitdir)) |
| 257 | + if err != nil { |
| 258 | + t.Fatalf("dotGitCommonDirectory() error = %v", err) |
| 259 | + } |
| 260 | + // osfs.Root resolves symlinks (e.g. /var -> /private/var on macOS). |
| 261 | + wantDir, err := filepath.EvalSymlinks(commonDir) |
| 262 | + if err != nil { |
| 263 | + t.Fatal(err) |
| 264 | + } |
| 265 | + if common == nil || common.Root() != wantDir { |
| 266 | + t.Errorf("dotGitCommonDirectory() root = %v, want %q", common, wantDir) |
| 267 | + } |
| 268 | + }) |
| 269 | + |
| 270 | + t.Run("absolute commondir", func(t *testing.T) { |
| 271 | + tmpDir := t.TempDir() |
| 272 | + commonDir := t.TempDir() |
| 273 | + if err := os.WriteFile(filepath.Join(tmpDir, "commondir"), []byte(commonDir+"\n"), 0644); err != nil { |
| 274 | + t.Fatal(err) |
| 275 | + } |
| 276 | + common, err := dotGitCommonDirectory(osfs.New(tmpDir)) |
| 277 | + if err != nil { |
| 278 | + t.Fatalf("dotGitCommonDirectory() error = %v", err) |
| 279 | + } |
| 280 | + // osfs.Root resolves symlinks (e.g. /var -> /private/var on macOS). |
| 281 | + wantDir, err := filepath.EvalSymlinks(commonDir) |
| 282 | + if err != nil { |
| 283 | + t.Fatal(err) |
| 284 | + } |
| 285 | + if common == nil || common.Root() != wantDir { |
| 286 | + t.Errorf("dotGitCommonDirectory() root = %v, want %q", common, wantDir) |
| 287 | + } |
| 288 | + }) |
| 289 | + |
| 290 | + t.Run("commondir does not exist", func(t *testing.T) { |
| 291 | + tmpDir := t.TempDir() |
| 292 | + if err := os.WriteFile(filepath.Join(tmpDir, "commondir"), []byte("missing\n"), 0644); err != nil { |
| 293 | + t.Fatal(err) |
| 294 | + } |
| 295 | + _, err := dotGitCommonDirectory(osfs.New(tmpDir)) |
| 296 | + if !errors.Is(err, git.ErrRepositoryIncomplete) { |
| 297 | + t.Errorf("dotGitCommonDirectory() error = %v, want %v", err, git.ErrRepositoryIncomplete) |
| 298 | + } |
| 299 | + }) |
| 300 | +} |
0 commit comments