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