A vibe coded tangled fork which supports pijul.
1package repomanager
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "errors"
8 "fmt"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "slices"
13 "strings"
14
15 "github.com/bluesky-social/indigo/atproto/syntax"
16 "github.com/go-git/go-git/v5"
17 "github.com/go-git/go-git/v5/config"
18 "github.com/go-git/go-git/v5/plumbing/object"
19 kgit "tangled.org/core/knotserver/git"
20 "tangled.org/core/types"
21)
22
23// RepoManager manages a `sh.tangled.repo` record with its git context.
24// It can be used to efficiently fetch the filetree of the repository.
25type RepoManager struct {
26 repoDir string
27 // TODO: it would be nice if RepoManager can be configured with different
28 // strategies:
29 // - use db as an only source for repo records
30 // - use atproto if record doesn't exist from the db
31 // - always use atproto
32 // hmm do we need `RepoStore` interface?
33 // now `DbRepoStore` and `AtprotoRepoStore` can implement both.
34 // all `RepoStore` objects will hold `KnotStore` interface, so they can
35 // source the knot store if needed.
36
37 // but now we can't do complex queries like "get repo with issue count"
38 // that kind of queries will be done directly from `appview.DB` struct
39 // is graphql better tech for atproto?
40}
41
42func New(repoDir string) RepoManager {
43 return RepoManager{
44 repoDir: repoDir,
45 }
46}
47
48// TODO: RepoManager can return file tree from repoAt & rev
49// It will start syncing the repository if doesn't exist
50
51// RegisterRepo starts sparse-syncing repository with paths
52func (m *RepoManager) RegisterRepo(ctx context.Context, repoAt syntax.ATURI, paths []string) error {
53 repoPath := m.repoPath(repoAt)
54 exist, err := isDir(repoPath)
55 if err != nil {
56 return fmt.Errorf("checking dir info: %w", err)
57 }
58 var sparsePaths []string
59 if !exist {
60 // init bare git repo
61 repo, err := git.PlainInit(repoPath, true)
62 if err != nil {
63 return fmt.Errorf("initializing repo: %w", err)
64 }
65 _, err = repo.CreateRemote(&config.RemoteConfig{
66 Name: "origin",
67 URLs: []string{m.repoCloneUrl(repoAt)},
68 })
69 if err != nil {
70 return fmt.Errorf("configuring repo remote: %w", err)
71 }
72 } else {
73 // get sparse-checkout list
74 sparsePaths, err = func(path string) ([]string, error) {
75 var stdout bytes.Buffer
76 listCmd := exec.Command("git", "-C", path, "sparse-checkout", "list")
77 listCmd.Stdout = &stdout
78 if err := listCmd.Run(); err != nil {
79 return nil, err
80 }
81
82 var sparseList []string
83 scanner := bufio.NewScanner(&stdout)
84 for scanner.Scan() {
85 line := strings.TrimSpace(scanner.Text())
86 if line == "" {
87 continue
88 }
89 sparseList = append(sparseList, line)
90 }
91 if err := scanner.Err(); err != nil {
92 return nil, fmt.Errorf("scanning stdout: %w", err)
93 }
94
95 return sparseList, nil
96 }(repoPath)
97 if err != nil {
98 return fmt.Errorf("parsing sparse-checkout list: %w", err)
99 }
100
101 // add paths to sparse-checkout list
102 for _, path := range paths {
103 sparsePaths = append(sparsePaths, path)
104 }
105 sparsePaths = slices.Collect(slices.Values(sparsePaths))
106 }
107
108 // set sparse-checkout list
109 args := append([]string{"-C", repoPath, "sparse-checkout", "set", "--no-cone"}, sparsePaths...)
110 if err := exec.Command("git", args...).Run(); err != nil {
111 return fmt.Errorf("setting sparse-checkout list: %w", err)
112 }
113 return nil
114}
115
116// SyncRepo sparse-fetch specific rev of the repo
117func (m *RepoManager) SyncRepo(ctx context.Context, repo syntax.ATURI, rev string) error {
118 // TODO: fetch repo with rev.
119 panic("unimplemented")
120}
121
122func (m *RepoManager) Open(repo syntax.ATURI, rev string) (*kgit.GitRepo, error) {
123 // TODO: don't depend on knot/git
124 return kgit.Open(m.repoPath(repo), rev)
125}
126
127func (m *RepoManager) FileTree(ctx context.Context, repo syntax.ATURI, rev, path string) ([]types.NiceTree, error) {
128 if err := m.SyncRepo(ctx, repo, rev); err != nil {
129 return nil, fmt.Errorf("syncing git repo")
130 }
131 gr, err := m.Open(repo, rev)
132 if err != nil {
133 return nil, err
134 }
135 dir, err := gr.FileTree(ctx, path)
136 if err != nil {
137 if errors.Is(err, object.ErrDirectoryNotFound) {
138 return nil, nil
139 }
140 return nil, fmt.Errorf("loading file tree: %w", err)
141 }
142 return dir, err
143}
144
145func (m *RepoManager) repoPath(repo syntax.ATURI) string {
146 return filepath.Join(
147 m.repoDir,
148 repo.Authority().String(),
149 repo.Collection().String(),
150 repo.RecordKey().String(),
151 )
152}
153
154func (m *RepoManager) repoCloneUrl(repo syntax.ATURI) string {
155 // 1. get repo & knot models from db. fetch it if doesn't exist
156 // 2. construct https clone url
157 panic("unimplemented")
158}
159
160func isDir(path string) (bool, error) {
161 info, err := os.Stat(path)
162 if err == nil && info.IsDir() {
163 return true, nil
164 }
165 if os.IsNotExist(err) {
166 return false, nil
167 }
168 return false, err
169}