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 toWorktreePlugin := make([]Plugin, 0)
110 for _, p := range plugins {
111 found := false
112 for _, pp := range pluginsConfChange {
113 for _, ppp := range pp.plugins {
114 if p.Equal(ppp) {
115 found = true
116 break
117 }
118 }
119 if found {
120 break
121 }
122 }
123 if found {
124 toWorktreePlugin = append(toWorktreePlugin, p)
125 }
126 }
127
128 if len(toWorktreePlugin) > 0 {
129 m.logger.Debug("manager start worktree", logger.NewLoggerPair("repo", repoName))
130 m.runtimes.Start(ctx, repoName, toWorktreePlugin, runtimeInputsKindWorktree, commands, func(newCmds []CommandForDiff) {
131 m.logger.Debug("manager then worktree start diff with newCmds", logger.NewLoggerPair("repo", repoName))
132 go m.runtimes.Start(ctx, repoName, plugins, runtimeInputsKindDiff, append(commands, newCmds...), nil)
133 })
134 } else {
135 m.logger.Debug("manager start diff", logger.NewLoggerPair("repo", repoName))
136 m.runtimes.Start(ctx, repoName, plugins, runtimeInputsKindDiff, commands, nil)
137 }
138 }
139 }
140 return nil
141}
142
143func (m *Manager) Schedule(ctx context.Context, repoName string, pluginPurl string, pluginRun *PluginRun) error {
144 return m.runtimes.Schedule(ctx, repoName, pluginPurl, pluginRun)
145}
146
147type commandPluginActivation struct {
148 command CommandForDiff
149 plugins []Plugin
150}
151
152func (m *Manager) checkPluginConfChange(ctx context.Context, repoName string, commands []CommandForDiff) ([]commandPluginActivation, error) {
153 pluginsActivated := make([]commandPluginActivation, 0)
154 if len(commands) == 0 {
155 return pluginsActivated, nil
156 }
157 repo, err := m.repoManager.Open(logger.AddCaller(ctx, "checkPluginActivation"), repoName)
158 if err != nil {
159 return nil, oops.Wrapf(err, "can't open repo")
160 }
161 defer repo.Close()
162
163 repoConf, err := repo.Configuration()
164 if err != nil {
165 return nil, oops.Wrapf(err, "can't get repo conf")
166 }
167
168 for _, cmd := range commands {
169 if len(cmd.commits) == 0 || !cmd.IsFileTouched(repoConf.DefaultBranch, m.conf.PathFilePlugins()) {
170 continue
171 }
172
173 oldFilecontent, err := repo.ContentPluginsConfAtHash(cmd.commits[0].parentHash)
174 if err != nil {
175 return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't get repo plugin conf at hash")
176 }
177
178 newFilecontent, err := repo.ContentPluginsConfAtRef(cmd.branch)
179 if err != nil {
180 return nil, oops.With("repo", repoName, "ref", cmd.branch.Short()).Wrapf(err, "can't get repo plugin conf at ref")
181 }
182
183 if bytes.Equal(oldFilecontent, newFilecontent) {
184 continue
185 }
186
187 oldConfPlugins, err := ParsePlugins(oldFilecontent, false)
188 if err != nil {
189 return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't parse repo plugin oldconf")
190 }
191 newConfPlugins, err := ParsePlugins(newFilecontent, false)
192 if err != nil {
193 return nil, oops.With("repo", repoName, "Branch", cmd.branch).Wrapf(err, "can't parse repo plugin newconf")
194 }
195
196 plugins := make([]Plugin, 0)
197
198 if len(oldConfPlugins) < len(newConfPlugins) {
199 addedPlugins := []Plugin{}
200 for _, p := range newConfPlugins {
201 found := false
202 for _, op := range oldConfPlugins {
203 if p.Equal(op) {
204 found = true
205 break
206 }
207 }
208 if !found {
209 addedPlugins = append(addedPlugins, p.SetActive(false))
210 }
211 }
212 oldConfPlugins = append(oldConfPlugins, addedPlugins...)
213 }
214
215 for _, oldConfPlugin := range oldConfPlugins {
216 for _, newConfPlugin := range newConfPlugins {
217 if oldConfPlugin.Equal(newConfPlugin) {
218 if !oldConfPlugin.Active && newConfPlugin.Active {
219 m.logger.Info("plugin activation", logger.NewLoggerPair("plugin", newConfPlugin.Log()))
220 plugins = append(plugins, newConfPlugin)
221 break
222 }
223 if !Equal(oldConfPlugin, newConfPlugin) {
224 m.logger.Info("plugin conf change", logger.NewLoggerPair("plugin", newConfPlugin.Log()))
225 plugins = append(plugins, newConfPlugin)
226 break
227 }
228 }
229 }
230 }
231
232 if len(plugins) > 0 {
233 pluginsActivated = append(pluginsActivated, commandPluginActivation{command: cmd, plugins: plugins})
234 }
235 }
236 return pluginsActivated, nil
237}