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