1package jobs
2
3import (
4 "context"
5 "fmt"
6 "path/filepath"
7 "runtime"
8
9 "github.com/charmbracelet/log"
10 "github.com/charmbracelet/soft-serve/git"
11 "github.com/charmbracelet/soft-serve/pkg/backend"
12 "github.com/charmbracelet/soft-serve/pkg/config"
13 "github.com/charmbracelet/soft-serve/pkg/db"
14 "github.com/charmbracelet/soft-serve/pkg/lfs"
15 "github.com/charmbracelet/soft-serve/pkg/store"
16 "github.com/charmbracelet/soft-serve/pkg/sync"
17)
18
19func init() {
20 Register("mirror-pull", mirrorPull{})
21}
22
23type mirrorPull struct{}
24
25// Spec derives the spec used for pull mirrors and implements Runner.
26func (m mirrorPull) Spec(ctx context.Context) string {
27 cfg := config.FromContext(ctx)
28 if cfg.Jobs.MirrorPull != "" {
29 return cfg.Jobs.MirrorPull
30 }
31 return "@every 10m"
32}
33
34// Func runs the (pull) mirror job task and implements Runner.
35func (m mirrorPull) Func(ctx context.Context) func() {
36 cfg := config.FromContext(ctx)
37 logger := log.FromContext(ctx).WithPrefix("jobs.mirror")
38 b := backend.FromContext(ctx)
39 dbx := db.FromContext(ctx)
40 datastore := store.FromContext(ctx)
41 return func() {
42 repos, err := b.Repositories(ctx)
43 if err != nil {
44 logger.Error("error getting repositories", "err", err)
45 return
46 }
47
48 // Divide the work up among the number of CPUs.
49 wq := sync.NewWorkPool(ctx, runtime.GOMAXPROCS(0),
50 sync.WithWorkPoolLogger(logger.Errorf),
51 )
52
53 logger.Debug("updating mirror repos")
54 for _, repo := range repos {
55 if repo.IsMirror() {
56 r, err := repo.Open()
57 if err != nil {
58 logger.Error("error opening repository", "repo", repo.Name(), "err", err)
59 continue
60 }
61
62 name := repo.Name()
63 wq.Add(name, func() {
64 repo := repo
65 cmd := git.NewCommand("remote", "update", "--prune").WithContext(ctx)
66 cmd.AddEnvs(
67 fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
68 filepath.Join(cfg.DataPath, "ssh", "known_hosts"),
69 cfg.SSH.ClientKeyPath,
70 ),
71 )
72
73 if _, err := cmd.RunInDir(r.Path); err != nil {
74 logger.Error("error running git remote update", "repo", name, "err", err)
75 }
76
77 if cfg.LFS.Enabled {
78 rcfg, err := r.Config()
79 if err != nil {
80 logger.Error("error getting git config", "repo", name, "err", err)
81 return
82 }
83
84 lfsEndpoint := rcfg.Section("lfs").Option("url")
85 if lfsEndpoint == "" {
86 // If there is no LFS url defined, means the repo
87 // doesn't use LFS and we can skip it.
88 return
89 }
90
91 ep, err := lfs.NewEndpoint(lfsEndpoint)
92 if err != nil {
93 logger.Error("error creating LFS endpoint", "repo", name, "err", err)
94 return
95 }
96
97 client := lfs.NewClient(ep)
98 if client == nil {
99 logger.Errorf("failed to create lfs client: unsupported endpoint %s", lfsEndpoint)
100 return
101 }
102
103 if err := backend.StoreRepoMissingLFSObjects(ctx, repo, dbx, datastore, client); err != nil {
104 logger.Error("failed to store missing lfs objects", "err", err, "path", r.Path)
105 return
106 }
107 }
108 })
109 }
110 }
111
112 wq.Run()
113 }
114}