fork of https://github.com/sourcegraph/zoekt
0

Configure Feed

Select the types of activity you want to include in your feed.

Handle git indexing path correctly for Git worktrees (#1025)

`gitindex` assumed repositories could be opened with the default go-git repo opener. That works for normal clones, but it breaks for Git worktrees because refs and object data are split between the worktree git dir and the shared common git dir.

This change introduces a small `plainOpenRepo` helper that uses `git`.
`PlainOpenWithOptions` with `DetectDotGit` and `EnableDotGitCommonDir`. The helper is then used in the plain-open code paths inside `gitindex`, including config/template loading.

With this change, Zoekt can read Git worktrees through go-git using the same logical repository view as Git itself, instead of failing on missing refs or incomplete repository layouts.

+116 -6
+50 -6
gitindex/index.go
··· 182 182 return commitObj, nil 183 183 } 184 184 185 + func plainOpenRepo(repoDir string) (*git.Repository, error) { 186 + return git.PlainOpenWithOptions(repoDir, &git.PlainOpenOptions{ 187 + DetectDotGit: true, 188 + EnableDotGitCommonDir: true, 189 + }) 190 + } 191 + 185 192 func configLookupRemoteURL(cfg *config.Config, key string) string { 186 193 rc := cfg.Remotes[key] 187 194 if rc == nil || len(rc.URLs) == 0 { ··· 193 200 var sshRelativeURLRegexp = regexp.MustCompile(`^([^@]+)@([^:]+):(.*)$`) 194 201 195 202 func setTemplatesFromConfig(desc *zoekt.Repository, repoDir string) error { 196 - repo, err := git.PlainOpen(repoDir) 203 + repo, err := plainOpenRepo(repoDir) 197 204 if err != nil { 198 205 return err 199 206 } ··· 203 210 return err 204 211 } 205 212 213 + return setTemplatesFromRepoConfig(desc, cfg) 214 + } 215 + 216 + func setTemplatesFromRepo(desc *zoekt.Repository, repo *git.Repository, repoDir string) error { 217 + cfg, err := repo.Config() 218 + if err == nil { 219 + return setTemplatesFromRepoConfig(desc, cfg) 220 + } 221 + 222 + return setTemplatesFromConfig(desc, repoDir) 223 + } 224 + 225 + func setTemplatesFromRepoConfig(desc *zoekt.Repository, cfg *config.Config) error { 206 226 sec := cfg.Raw.Section("zoekt") 207 227 208 228 webURLStr := sec.Options.Get("web-url") ··· 451 471 var repo *git.Repository 452 472 legacyRepoOpen := cmp.Or(os.Getenv("ZOEKT_DISABLE_GOGIT_OPTIMIZATION"), "false") 453 473 if b, err := strconv.ParseBool(legacyRepoOpen); b || err != nil { 454 - repo, err = git.PlainOpen(opts.RepoDir) 474 + repo, err = plainOpenRepo(opts.RepoDir) 455 475 if err != nil { 456 - return false, fmt.Errorf("git.PlainOpen: %w", err) 476 + return false, fmt.Errorf("plainOpenRepo: %w", err) 457 477 } 458 478 } else { 459 479 var repoCloser io.Closer ··· 464 484 defer repoCloser.Close() 465 485 } 466 486 467 - if err := setTemplatesFromConfig(&opts.BuildOptions.RepositoryDescription, opts.RepoDir); err != nil { 468 - log.Printf("setTemplatesFromConfig(%s): %s", opts.RepoDir, err) 487 + if err := setTemplatesFromRepo(&opts.BuildOptions.RepositoryDescription, repo, opts.RepoDir); err != nil { 488 + log.Printf("setTemplatesFromRepo(%s): %s", opts.RepoDir, err) 469 489 } 470 490 471 491 branches, err := expandBranches(repo, opts.Branches, opts.BranchPrefix) ··· 705 725 // It copies the relevant logic from git.PlainOpen, and tweaks certain filesystem options. 706 726 func openRepo(repoDir string) (*git.Repository, io.Closer, error) { 707 727 fs := osfs.New(repoDir) 708 - wt := fs 709 728 710 729 // Check if the root directory exists. 711 730 if _, err := fs.Stat(""); err != nil { ··· 715 734 return nil, nil, err 716 735 } 717 736 737 + fi, err := fs.Stat(git.GitDirName) 738 + if err == nil && !fi.IsDir() { 739 + return openCompatibleRepo(repoDir) 740 + } 741 + 742 + return openOptimizedRepo(repoDir) 743 + } 744 + 745 + func openCompatibleRepo(repoDir string) (*git.Repository, io.Closer, error) { 746 + repo, err := plainOpenRepo(repoDir) 747 + if err != nil { 748 + return nil, nil, err 749 + } 750 + 751 + return repo, noopCloser{}, nil 752 + } 753 + 754 + func openOptimizedRepo(repoDir string) (*git.Repository, io.Closer, error) { 755 + fs := osfs.New(repoDir) 756 + wt := fs 757 + 718 758 // If there's a .git directory, use that as the new root. 719 759 if fi, err := fs.Stat(git.GitDirName); err == nil && fi.IsDir() { 720 760 if fs, err = fs.Chroot(git.GitDirName); err != nil { ··· 731 771 repo, err := git.Open(s, wt) 732 772 return repo, s, err 733 773 } 774 + 775 + type noopCloser struct{} 776 + 777 + func (noopCloser) Close() error { return nil } 734 778 735 779 func newIgnoreMatcher(tree *object.Tree) (*ignore.Matcher, error) { 736 780 ignoreFile, err := tree.File(ignore.IgnoreFile)
+66
gitindex/index_test.go
··· 132 132 } 133 133 } 134 134 135 + func TestIndexGitRepo_Worktree(t *testing.T) { 136 + _, worktreeDir := initGitWorktree(t, "file1.go", "package main\n\nfunc main() {}\n") 137 + indexDir := t.TempDir() 138 + 139 + opts := Options{ 140 + RepoDir: worktreeDir, 141 + Branches: []string{"HEAD"}, 142 + BuildOptions: index.Options{ 143 + RepositoryDescription: zoekt.Repository{Name: "repo"}, 144 + IndexDir: indexDir, 145 + }, 146 + } 147 + 148 + if _, err := IndexGitRepo(opts); err != nil { 149 + t.Fatalf("IndexGitRepo(worktree): %v", err) 150 + } 151 + 152 + searcher, err := search.NewDirectorySearcher(indexDir) 153 + if err != nil { 154 + t.Fatal("NewDirectorySearcher", err) 155 + } 156 + defer searcher.Close() 157 + 158 + results, err := searcher.Search(context.Background(), &query.Const{Value: true}, &zoekt.SearchOptions{}) 159 + if err != nil { 160 + t.Fatal("search failed", err) 161 + } 162 + 163 + if len(results.Files) != 1 { 164 + t.Fatalf("got search result %v, want 1 file", results.Files) 165 + } 166 + } 167 + 135 168 func executeCommand(t *testing.T, dir string, cmd *exec.Cmd) *exec.Cmd { 136 169 cmd.Dir = dir 137 170 cmd.Env = []string{ ··· 146 179 t.Fatalf("cmd.Run: %v", err) 147 180 } 148 181 return cmd 182 + } 183 + 184 + func initGitWorktree(t *testing.T, fileName, content string) (string, string) { 185 + t.Helper() 186 + 187 + dir := t.TempDir() 188 + executeCommand(t, dir, exec.Command("git", "init", "-b", "main", "repo")) 189 + 190 + repoDir := filepath.Join(dir, "repo") 191 + if err := os.WriteFile(filepath.Join(repoDir, fileName), []byte(content), 0o644); err != nil { 192 + t.Fatalf("WriteFile: %v", err) 193 + } 194 + executeCommand(t, repoDir, exec.Command("git", "config", "remote.origin.url", "git@github.com:sourcegraph/zoekt.git")) 195 + executeCommand(t, repoDir, exec.Command("git", "add", ".")) 196 + executeCommand(t, repoDir, exec.Command("git", "commit", "-m", "initial commit")) 197 + 198 + worktreeDir := filepath.Join(dir, "wt") 199 + executeCommand(t, repoDir, exec.Command("git", "worktree", "add", "-b", "worktree-branch", worktreeDir)) 200 + 201 + return repoDir, worktreeDir 149 202 } 150 203 151 204 func TestIndexDeltaBasic(t *testing.T) { ··· 833 886 desc := zoekt.Repository{} 834 887 if err := setTemplatesFromConfig(&desc, repositoryDir); err != nil { 835 888 t.Fatalf("setTemplatesFromConfig: %v", err) 889 + } 890 + 891 + if got, want := desc.FileURLTemplate, `{{URLJoinPath "https://github.com/sourcegraph/zoekt" "blob" .Version .Path}}`; got != want { 892 + t.Errorf("got %q, want %q", got, want) 893 + } 894 + } 895 + 896 + func TestSetTemplates_Worktree(t *testing.T) { 897 + _, worktreeDir := initGitWorktree(t, "hello.go", "package main\n") 898 + desc := zoekt.Repository{} 899 + 900 + if err := setTemplatesFromConfig(&desc, worktreeDir); err != nil { 901 + t.Fatalf("setTemplatesFromConfig(worktree): %v", err) 836 902 } 837 903 838 904 if got, want := desc.FileURLTemplate, `{{URLJoinPath "https://github.com/sourcegraph/zoekt" "blob" .Version .Path}}`; got != want {