This repository has no description
3.9 kB
149 lines
1package main
2
3import (
4 "context"
5 "crypto/sha1"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net/url"
11 "os"
12 "path/filepath"
13
14 "github.com/bluesky-social/indigo/atproto/identity"
15 "github.com/bluesky-social/indigo/atproto/syntax"
16 "github.com/samber/lo"
17 "tangled.org/core/repoident"
18 "tangled.org/core/repoverify"
19)
20
21// 1 MB; match https://sourcegraph.sourcegraph.com/r/github.com/sourcegraph/sourcegraph/-/blob/cmd/searcher/internal/search/store.go?L32
22const MaxFileSize = 1 << 20
23
24func gitIndex(ctx context.Context, cfg *Config, dir identity.Directory, req indexRequest) error {
25 ctx, cancel := context.WithTimeout(ctx, cfg.IndexTimeout)
26 defer cancel()
27
28 repo, err := loadRepo(ctx, cfg, dir, req.Repo)
29 if err != nil {
30 return fmt.Errorf("loading repo %s: %w", req.Repo, err)
31 }
32 repo.Branches = req.Branches
33
34 gitDir, err := tmpGitDir(repo.Did.String())
35 if err != nil {
36 return err
37 }
38 defer os.RemoveAll(gitDir) // best-effort cleanup
39
40 if err := fetchRepo(ctx, gitDir, repo.CloneURL(), req.Branches); err != nil {
41 return err
42 }
43
44 if err := indexRepo(ctx, cfg, gitDir, *repo); err != nil {
45 return err
46 }
47
48 return nil
49}
50
51func loadRepo(ctx context.Context, cfg *Config, dir identity.Directory, repoDID repoident.RepoDid) (*Repo, error) {
52 ident, err := dir.LookupDID(ctx, syntax.DID(repoDID))
53 if err != nil {
54 return nil, err
55 }
56
57 knot, err := repoident.KnotURLFromIdentity(ident, cfg.KnotScheme)
58 if err != nil {
59 return nil, fmt.Errorf("repoDid %s: %w", repoDID, err)
60 }
61
62 described, err := repoverify.Describe(ctx, nil, knot, repoDID)
63 if err != nil {
64 return nil, err
65 }
66
67 return &Repo{
68 Did: repoDID,
69 Owner: described.OwnerDid,
70 Slug: described.Rkey,
71 Knot: knot,
72 }, nil
73}
74
75func fetchRepo(ctx context.Context, gitDir, cloneUrl string, branches []indexBranch) error {
76 // Create a repo to fetch into
77 if err := executeCmd(ctx,
78 "git",
79 // use a random default branch. This is so that HEAD isn't a symref to a
80 // branch that is indexed. For example if you are indexing
81 // HEAD,master. Then HEAD would be pointing to master by default.
82 "-c", "init.defaultBranch=nonExistentBranchBB0FOFCH32",
83 "init",
84 // we don't need a working copy
85 "--bare",
86 gitDir,
87 ); err != nil {
88 return err
89 }
90
91 fetchArgs := append([]string{
92 "-C", gitDir,
93 "-c", "protocol.version=2",
94 "fetch", "--depth=1", "--no-tags",
95 // Git's blob:limit filter excludes blobs whose size is >= the given limit,
96 // while zoekt indexes files up to and including FileLimit bytes.
97 fmt.Sprintf("--filter=blob:limit=%d", int64(MaxFileSize)+1),
98 cloneUrl,
99 }, lo.Map(branches, func(b indexBranch, _ int) string { return string(b.Version) })...)
100
101 if err := executeCmd(ctx, "git", fetchArgs...); err != nil {
102 return err
103 }
104
105 return errors.Join(lo.FilterMap(branches, func(b indexBranch, _ int) (error, bool) {
106 err := executeCmd(ctx, "git", "-C", gitDir, "update-ref", b.Name.Ref(), string(b.Version))
107 if err == nil {
108 return nil, false
109 }
110 return fmt.Errorf("failed update-ref %s to %s: %w", b.Name.Ref(), b.Version, err), true
111 })...)
112}
113
114func indexRepo(ctx context.Context, cfg *Config, gitDir string, repo Repo) error {
115 executablePath, err := os.Executable()
116 if err != nil {
117 return err
118 }
119
120 repoJson, err := json.Marshal(repo)
121 if err != nil {
122 return err
123 }
124
125 args := []string{"index"}
126 args = append(args, "-index-dir", cfg.IndexDir)
127 args = append(args, "-appview-url", cfg.AppviewUrl)
128 args = append(args, gitDir, string(repoJson))
129 if err := executeCmd(ctx, executablePath, args...); err != nil {
130 return err
131 }
132 return nil
133}
134
135func tmpGitDir(name string) (string, error) {
136 abs := url.QueryEscape(name)
137 if len(abs) > 200 {
138 h := sha1.New()
139 _, _ = io.WriteString(h, abs)
140 abs = abs[:200] + fmt.Sprintf("%x", h.Sum(nil))[:8]
141 }
142 dir := filepath.Join(os.TempDir(), abs+".git")
143 if _, err := os.Stat(dir); err == nil {
144 if err := os.RemoveAll(dir); err != nil {
145 return "", err
146 }
147 }
148 return dir, nil
149}