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