GitRoot
Craft your forge, Build your project, Grow your community freely
1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5package plugin
6
7import (
8 "bytes"
9 "context"
10 "io/fs"
11 "sync"
12
13 "github.com/go-git/go-git/v6/plumbing"
14 "github.com/go-git/go-git/v6/plumbing/protocol/packp"
15 "github.com/samber/oops"
16 "gitroot.dev/libs/golang/plugin/model"
17 pluginLib "gitroot.dev/libs/golang/plugin/model"
18 "gitroot.dev/server/logger"
19 "gitroot.dev/server/repository"
20 "gitroot.dev/server/user"
21)
22
23type Manager struct {
24 logger *logger.Logger
25 conf needConf
26 repoManager needRepo
27 userManager needUser
28 backgroundManager needBackground
29 execManager needManager
30
31 plugins []Plugin
32 pluginsLock sync.Mutex
33 runtimes *runtimes
34}
35
36type needConf interface {
37 ForgeConf() model.ForgeConf
38 GetDirPathDataPlugin(pluginName string) string
39 PathDataPlugin() string
40 PathFilePlugins() string
41 DataWeb(repoName string) fs.FS
42 PathCacheProject(repoName string, pluginName string) string
43 PathCache() string
44 Cache(repoName string, pluginName string) fs.FS
45}
46
47type needRepo interface {
48 OpenForgeRepo(ctx context.Context) (*repository.GitRootRepository, error)
49 Open(ctx context.Context, repoName string) (*repository.GitRootRepository, error)
50}
51
52type needUser interface {
53 NewCommiter(pseudo string) (*user.Commiter, error)
54}
55
56type needBackground interface {
57 PostPush(pusher user.SimpleUser, repoName string, commands []*packp.Command, commitsByRef map[plumbing.ReferenceName][]plumbing.Hash)
58 DeleteBranch(repoName string, toDeleteBranchName string)
59}
60
61type needManager interface {
62 Exec(project *repository.GitRootRepository, branch string, pluginName string, commands pluginLib.Exec) (*pluginLib.ExecStatus, error)
63}
64
65func NewManager(conf needConf, repoManager needRepo, userManager needUser, execManager needManager) *Manager {
66 log := logger.NewLogger(logger.PLUGIN_MANAGER)
67 m := &Manager{
68 logger: log,
69 conf: conf,
70 repoManager: repoManager,
71 userManager: userManager,
72 execManager: execManager,
73
74 plugins: nil,
75 pluginsLock: sync.Mutex{},
76 }
77 m.runtimes = newRuntimes(m, log)
78 return m
79}
80
81func (m *Manager) SetBackgroundManager(backgroundManager needBackground) {
82 m.backgroundManager = backgroundManager
83}
84
85func (m *Manager) Run(ctx context.Context, repoName string, commands []CommandForDiff) error {
86 if len(commands) > 0 {
87 com := ""
88 if len(commands[0].commits) > 0 {
89 com = commands[0].commits[0].hash.String()
90 }
91 m.logger.Debug("call usableFromDefaultBranch from manager run", logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("branch", commands[0].branch), logger.NewLoggerPair("commit", com))
92 plugins, err := m.usableFromDefaultBranch(ctx, repoName)
93 if err != nil {
94 return err
95 }
96
97 // TODO rework plugin start/activation
98 // for each command (branch) we have a list of plugins to start/activate
99 // maybe cut commands into one command previously in code
100 // and so run plugins/commits for one cmd at a time
101
102 m.logger.Debug("call checkPluginConfChange from manager run", logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("branch", commands[0].branch), logger.NewLoggerPair("commit", com))
103 pluginsConfChange, err := m.checkPluginConfChange(ctx, repoName, commands)
104 if err != nil {
105 return err
106 }
107
108 if len(plugins) > 0 {
109 pluginsAlreadyPresent := make([]Plugin, 0)
110 toWorktreePlugin := make([]Plugin, 0)
111 for _, p := range plugins {
112 found := false
113 for _, pp := range pluginsConfChange {
114 for _, ppp := range pp.plugins {
115 if p.Equal(ppp) {
116 found = true
117 break
118 }
119 }
120 if found {
121 break
122 }
123 }
124 if !found {
125 pluginsAlreadyPresent = append(pluginsAlreadyPresent, p)
126 } else {
127 toWorktreePlugin = append(toWorktreePlugin, p)
128 }
129 }
130 if len(pluginsAlreadyPresent) > 0 {
131 m.runtimes.Start(ctx, repoName, pluginsAlreadyPresent, runtimeInputsKindDiff, commands)
132 }
133 if len(toWorktreePlugin) > 0 {
134 m.runtimes.Start(ctx, repoName, toWorktreePlugin, runtimeInputsKindWorktree, commands)
135 }
136 }
137 }
138 return nil
139}
140
141func (m *Manager) Schedule(ctx context.Context, repoName string, pluginPurl string, pluginRun *PluginRun) error {
142 return m.runtimes.Schedule(ctx, repoName, pluginPurl, pluginRun)
143}
144
145type commandPluginActivation struct {
146 command CommandForDiff
147 plugins []Plugin
148}
149
150func (m *Manager) checkPluginConfChange(ctx context.Context, repoName string, commands []CommandForDiff) ([]commandPluginActivation, error) {
151 pluginsActivated := make([]commandPluginActivation, 0)
152 if len(commands) == 0 {
153 return pluginsActivated, nil
154 }
155 repo, err := m.repoManager.Open(logger.AddCaller(ctx, "checkPluginActivation"), repoName)
156 if err != nil {
157 return nil, oops.Wrapf(err, "can't open repo")
158 }
159 defer repo.Close()
160
161 repoConf, err := repo.Configuration()
162 if err != nil {
163 return nil, oops.Wrapf(err, "can't get repo conf")
164 }
165
166 for _, cmd := range commands {
167 if len(cmd.commits) == 0 || !cmd.IsFileTouched(repoConf.DefaultBranch, m.conf.PathFilePlugins()) {
168 continue
169 }
170
171 oldFilecontent, err := repo.ContentPluginsConfAtHash(cmd.commits[0].parentHash)
172 if err != nil {
173 return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't get repo plugin conf at hash")
174 }
175
176 newFilecontent, err := repo.ContentPluginsConfAtRef(cmd.branch)
177 if err != nil {
178 return nil, oops.With("repo", repoName, "ref", cmd.branch.Short()).Wrapf(err, "can't get repo plugin conf at ref")
179 }
180
181 if bytes.Equal(oldFilecontent, newFilecontent) {
182 continue
183 }
184
185 oldConfPlugins, err := ParsePlugins(oldFilecontent, false)
186 if err != nil {
187 return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't parse repo plugin oldconf")
188 }
189 newConfPlugins, err := ParsePlugins(newFilecontent, false)
190 if err != nil {
191 return nil, oops.With("repo", repoName, "Branch", cmd.branch).Wrapf(err, "can't parse repo plugin newconf")
192 }
193
194 plugins := make([]Plugin, 0)
195
196 if len(oldConfPlugins) < len(newConfPlugins) {
197 addedPlugins := []Plugin{}
198 for _, p := range newConfPlugins {
199 found := false
200 for _, op := range oldConfPlugins {
201 if p.Equal(op) {
202 found = true
203 break
204 }
205 }
206 if !found {
207 addedPlugins = append(addedPlugins, p.SetActive(false))
208 }
209 }
210 oldConfPlugins = append(oldConfPlugins, addedPlugins...)
211 }
212
213 for _, oldConfPlugin := range oldConfPlugins {
214 for _, newConfPlugin := range newConfPlugins {
215 if oldConfPlugin.Equal(newConfPlugin) {
216 if !oldConfPlugin.Active && newConfPlugin.Active {
217 m.logger.Info("plugin activation", logger.NewLoggerPair("plugin", newConfPlugin.Log()))
218 plugins = append(plugins, newConfPlugin)
219 break
220 }
221 if !Equal(oldConfPlugin, newConfPlugin) {
222 m.logger.Info("plugin conf change", logger.NewLoggerPair("plugin", newConfPlugin.Log()))
223 plugins = append(plugins, newConfPlugin)
224 break
225 }
226 }
227 }
228 }
229
230 if len(plugins) > 0 {
231 pluginsActivated = append(pluginsActivated, commandPluginActivation{command: cmd, plugins: plugins})
232 }
233 }
234 return pluginsActivated, nil
235}