A vibe coded tangled fork which supports pijul.
at sl/spindle-adapters 169 lines 4.7 kB view raw
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}