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	"context"
  9	"encoding/json"
 10	"fmt"
 11	"io/fs"
 12	"slices"
 13	"time"
 14
 15	"github.com/go-git/go-git/v6/plumbing"
 16	"github.com/samber/oops"
 17	"github.com/tetratelabs/wazero/api"
 18	"gitroot.dev/libs/golang/plugin/model"
 19	pluginLib "gitroot.dev/libs/golang/plugin/model"
 20	grfs "gitroot.dev/server/fs"
 21	"gitroot.dev/server/logger"
 22	"gitroot.dev/server/repository"
 23	"gitroot.dev/server/user"
 24)
 25
 26type callPlugin struct {
 27	manager    *Manager
 28	plugin     Plugin
 29	repo       *repository.GitRootRepository
 30	repoWriter *repository.GitRootRepositoryWrite
 31	module     api.Module
 32	logger     *logger.Logger
 33}
 34
 35func (r *runtime) start(ctx context.Context, repo *repository.GitRootRepository, repoWriter *repository.GitRootRepositoryWrite, plugins []Plugin, command CommandForDiff, mergeHook func(cmd *repository.MergeRes, pusher user.SimpleUser, toDeleteBranchName string)) error {
 36	r.repo = repo
 37	r.repoWriter = repoWriter
 38	r.command = nil
 39	r.commit = nil
 40
 41	toRunLater := []CommandForDiff{}
 42
 43	r.commitHook = func(hash plumbing.Hash) {
 44		lastCom, err := repoWriter.GetLastCommit(hash)
 45		if err != nil {
 46			r.logger.Error("can't GetLastCommit in start runtime", err)
 47			return
 48		}
 49		newCmd, err := CommandForDiffFromCommitCmd(ctx, r.plugin.commiter.SimpleUser, lastCom, r.command.branch)
 50		if err != nil {
 51			r.logger.Error("can't CommandForDiffFromCommitCmd in start runtime", err)
 52			return
 53		}
 54		toRunLater = append(toRunLater, newCmd)
 55	}
 56
 57	r.mergeHook = mergeHook
 58	defer func() {
 59		r.commitHook = nil
 60		r.mergeHook = nil
 61	}()
 62
 63	r.runCmd(ctx, plugins, repo, repoWriter, command)
 64
 65	for _, cmd := range toRunLater {
 66		r.runCmd(ctx, plugins, repo, repoWriter, cmd)
 67	}
 68
 69	return nil
 70}
 71
 72func (r *runtime) runCmd(ctx context.Context, plugins []Plugin, repo *repository.GitRootRepository, repoWriter *repository.GitRootRepositoryWrite, command CommandForDiff) {
 73	fs := grfs.NewMultiple(ctx, map[string]fs.FS{
 74		"worktree":   r.repoWriter.ToFs(ctx),
 75		"webcontent": r.manager.conf.DataWeb(r.repo.Name()),
 76	})
 77	for _, plugin := range plugins {
 78		fs.UpdateSubFs("cache", r.manager.conf.Cache(r.repo.Name(), plugin.NamespaceAndName()))
 79		r.plugin = plugin
 80		timerStop := r.logger.Time(fmt.Sprintf("Timer %s", plugin.Log()))
 81		r.logger.Debug("start plugin", logger.NewLoggerPair("repo", repo.Name()), logger.NewLoggerPair("name", plugin.Log()))
 82		m, err := r.loadModule(ctx, plugin, fs)
 83		if err != nil {
 84			r.logger.Error("loadModule for start error", err, logger.NewLoggerPair("name", plugin.Log()))
 85			continue
 86		}
 87		l := logger.NewLogger(logger.WASM)
 88		l.Debug("memory before", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Log()))
 89		cp := callPlugin{
 90			manager:    r.manager,
 91			plugin:     plugin,
 92			repo:       repo,
 93			repoWriter: repoWriter,
 94			module:     m,
 95			logger:     r.logger.NewSubLogger(plugin.Log()),
 96		}
 97		if err := cp.callPluginForDiff(ctx, r, command); err != nil {
 98			r.logger.Error("finish plugin with error", err, logger.NewLoggerPair("name", plugin.Log()))
 99		}
100		l.Debug("memory after", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Log()))
101		r.logger.Debug("finish plugin", logger.NewLoggerPair("name", plugin.Log()))
102		timerStop()
103	}
104}
105
106func (c callPlugin) callPluginForDiff(ctx context.Context, r *runtime, cmd CommandForDiff) error {
107	startCommit := c.module.ExportedFunction("startCommit")
108	addFile := c.module.ExportedFunction("addFile")
109	modFile := c.module.ExportedFunction("modFile")
110	delFile := c.module.ExportedFunction("delFile")
111	endCommit := c.module.ExportedFunction("endCommit")
112	malloc := c.module.ExportedFunction("gitrootAlloc")
113	if malloc == nil {
114		malloc = c.module.ExportedFunction("malloc")
115	}
116
117	r.command = &cmd
118
119	for _, pluginRun := range r.plugin.Run {
120		r.pluginRun = pluginRun
121		callOnAdd := addFile != nil && slices.Contains(pluginRun.When, pluginLib.PluginRunWhenAdd)
122		callOnMod := modFile != nil && slices.Contains(pluginRun.When, pluginLib.PluginRunWhenMod)
123		callOnDel := delFile != nil && slices.Contains(pluginRun.When, pluginLib.PluginRunWhenDel)
124		atLeastOneCall := callOnAdd || callOnMod || callOnDel
125		if !atLeastOneCall {
126			c.logger.Info("no call given, skip execution", logger.NewLoggerPair("plugin", r.plugin.Log()))
127			continue
128		}
129
130		isAuthorized := checkBranch(pluginRun, cmd.branch)
131		if !isAuthorized {
132			continue
133		}
134
135		if cmd.branchAction == commitForDiffActionDel {
136			c.logger.Info("delete branch", logger.NewLoggerPair("branch", cmd.branch))
137			continue
138		}
139
140		atLeastOneFile := slices.ContainsFunc(cmd.commits, func(com commitForDiffCommit) bool {
141			return slices.ContainsFunc(com.files, func(f pluginLib.File) bool {
142				return pluginRun.glob.Match(f.Path)
143			})
144		})
145		if !atLeastOneFile {
146			c.logger.Info("no file match, skip execution", logger.NewLoggerPair("plugin", r.plugin.Log()))
147			continue
148		}
149
150		if init := c.module.ExportedFunction("init"); init != nil {
151			arg, err := pluginRun.Marshal()
152			if err != nil {
153				c.logger.Error("can't Marshal pluginRun", err, logger.NewLoggerPair("branch", cmd.branch))
154				continue
155			}
156			c.logger.Debug("init plugin", logger.NewLoggerPair("name", c.plugin.Log()), logger.NewLoggerPair("arg", arg))
157			if err := r.writeMemoryAndCall(c.module, init, malloc, c.repo.Name(), string(model.InitKindDiff), string(arg)); err != nil {
158				c.logger.Error("can't init plugin", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Log()))
159				continue
160			}
161		} else {
162			c.logger.Info("no init fn to call", logger.NewLoggerPair("plugin", r.plugin.Log()))
163		}
164
165		for _, com := range cmd.commits {
166			r.commit = &com
167			comMarshalled, err := MarshallOne(cmd.branch.Short(), com)
168			if err != nil {
169				c.logger.Error("can't marshall commit", err, logger.NewLoggerPair("branch", cmd.branch))
170				continue
171			}
172			c.logger.Debug("diff start commit", logger.NewLoggerPair("com", comMarshalled))
173			if startCommit != nil {
174				c.logger.Debug("startCommit", logger.NewLoggerPair("branch", cmd.branch.Short()), logger.NewLoggerPair("hash", com.hash.String()), logger.NewLoggerPair("message", com.message), logger.NewLoggerPair("date", com.date.Format(time.RFC3339)))
175				if err := r.writeMemoryAndCall(c.module, startCommit, malloc, comMarshalled); err != nil {
176					c.logger.Error("startCommit error", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Log()))
177					continue
178				}
179			}
180
181			for _, f := range com.files {
182				c.logger.Debug("diff start commit file", logger.NewLoggerPair("file", f.Path), logger.NewLoggerPair("action", f.Action))
183				if pluginRun.glob.Match(f.Path) {
184					jsonFile, err := json.Marshal(f)
185					if err != nil {
186						c.logger.Error("can marshal file", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Log()))
187						continue
188					}
189					if f.Action == pluginLib.FileActionTypeAdd && callOnAdd {
190						c.logger.Debug("add", logger.NewLoggerPair("confPath", pluginRun.Path), logger.NewLoggerPair("currentPath", f.Path))
191						// creation
192						r.writeMemoryAndCall(c.module, addFile, malloc, string(jsonFile))
193					} else {
194						if f.Action == pluginLib.FileActionTypeDel && callOnDel {
195							// deletion
196							r.writeMemoryAndCall(c.module, delFile, malloc, string(jsonFile))
197						} else if f.Action == pluginLib.FileActionTypeMod && callOnMod {
198							//modification
199							r.writeMemoryAndCall(c.module, modFile, malloc, string(jsonFile))
200						}
201					}
202				}
203			}
204
205			if endCommit != nil {
206				if err := r.writeMemoryAndCall(c.module, endCommit, malloc, comMarshalled); err != nil {
207					c.logger.Error("endCommit error", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Log()))
208					continue
209				}
210			} else {
211				c.logger.Info("no endCommit fn to call", logger.NewLoggerPair("plugin", r.plugin.Log()))
212			}
213		}
214
215		if finish := c.module.ExportedFunction("finish"); finish != nil {
216			if _, err := finish.Call(ctx); err != nil {
217				c.logger.Error("finish error", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Log()))
218				continue
219			}
220		} else {
221			c.logger.Info("no finish fn to call", logger.NewLoggerPair("plugin", r.plugin.Log()))
222		}
223	}
224	return nil
225}
226
227func (r *runtime) writeMemoryAndCall(module api.Module, toCall api.Function, malloc api.Function, message ...string) error {
228	_, err := r.writeMemoryAndCallWithRes(module, toCall, malloc, message...)
229	if err != nil {
230		return err
231	}
232	return nil
233}
234
235func (r *runtime) writeMemoryAndCallWithRes(module api.Module, toCall api.Function, malloc api.Function, message ...string) (uint64, error) {
236	params := make([]uint64, 0)
237	for _, m := range message {
238		size := uint64(len(m))
239
240		results, err := malloc.Call(r.ctx, size)
241		if err != nil {
242			return 0, oops.Wrapf(err, "can't malloc memory for %s with %s", toCall.Definition().Name(), m)
243		}
244		ptr := results[0]
245
246		// The pointer is a linear memory offset, which is where we write the name.
247		if !module.Memory().Write(uint32(ptr), []byte(m)) {
248			return 0, oops.Wrapf(err, "can't write memory")
249		}
250
251		params = append(params, ptr, size)
252	}
253
254	defer func() {
255		ptrSizes := make([]ptrSize, 0)
256		for i, d := range params {
257			if i%2 == 0 {
258				ptrSizes = append(ptrSizes, ptrSize{ptr: d, size: params[i+1]})
259			}
260		}
261		r.free(module, ptrSizes)
262	}()
263
264	if res, err := toCall.Call(r.ctx, params...); err != nil {
265		return 0, oops.With("method", toCall.Definition().ExportNames()).Wrapf(err, "can't call")
266	} else if len(res) > 0 {
267		return res[0], nil
268	}
269
270	return 0, nil
271}