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, commands []pluginLib.Exec) (string, 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 // var cpa commandPluginActivation
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 for _, cmd := range commands {
151 if !cmd.IsFileTouched(cmd.branch, m.conf.PathFilePlugins()) || len(cmd.commits) == 0 {
152 continue
153 }
154
155 oldFilecontent, err := repo.ContentPluginsConfAtHash(cmd.commits[0].parentHash)
156 if err != nil {
157 return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't get repo plugin conf at hash")
158 }
159
160 newFilecontent, err := repo.ContentPluginsConfAtRef(cmd.branch)
161 if err != nil {
162 return nil, oops.With("repo", repoName, "ref", cmd.branch.Short()).Wrapf(err, "can't get repo plugin conf at ref")
163 }
164
165 if bytes.Equal(oldFilecontent, newFilecontent) {
166 continue
167 }
168
169 oldConfPlugins, err := ParsePlugins(oldFilecontent, false)
170 if err != nil {
171 return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't parse repo plugin oldconf")
172 }
173 newConfPlugins, err := ParsePlugins(newFilecontent, false)
174 if err != nil {
175 return nil, oops.With("repo", repoName, "Branch", cmd.branch).Wrapf(err, "can't parse repo plugin newconf")
176 }
177
178 plugins := make([]Plugin, 0)
179
180 if len(oldConfPlugins) < len(newConfPlugins) {
181 addedPlugins := []Plugin{}
182 for _, p := range newConfPlugins {
183 found := false
184 for _, op := range oldConfPlugins {
185 if p.Name == op.Name {
186 found = true
187 break
188 }
189 }
190 if !found {
191 addedPlugins = append(addedPlugins, p.SetActive(false))
192 }
193 }
194 oldConfPlugins = append(oldConfPlugins, addedPlugins...)
195 }
196
197 for _, oldConfPlugin := range oldConfPlugins {
198 for _, newConfPlugin := range newConfPlugins {
199 if oldConfPlugin.Name == newConfPlugin.Name {
200 if !oldConfPlugin.Active && newConfPlugin.Active {
201 m.logger.Info("plugin activation", logger.NewLoggerPair("plugin", newConfPlugin.Name))
202 plugins = append(plugins, newConfPlugin)
203 break
204 }
205 if !Equal(oldConfPlugin, newConfPlugin) {
206 m.logger.Info("plugin conf change", logger.NewLoggerPair("plugin", newConfPlugin.Name))
207 plugins = append(plugins, newConfPlugin)
208 break
209 }
210 }
211 }
212 }
213
214 if len(plugins) > 0 {
215 pluginsActivated = append(pluginsActivated, commandPluginActivation{command: cmd, plugins: plugins})
216 }
217 }
218 return pluginsActivated, nil
219}