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