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