// SPDX-FileCopyrightText: 2025 Romain Maneschi // // SPDX-License-Identifier: EUPL-1.2 package plugin import ( "context" "encoding/json" "fmt" "io/fs" "slices" "time" "github.com/go-git/go-git/v6/plumbing" "github.com/samber/oops" "github.com/tetratelabs/wazero/api" pluginLib "gitroot.dev/libs/golang/plugin/model" grfs "gitroot.dev/server/fs" "gitroot.dev/server/logger" "gitroot.dev/server/repository" "gitroot.dev/server/user" ) type callPlugin struct { manager *Manager plugin Plugin repo *repository.GitRootRepository repoWriter *repository.GitRootRepositoryWrite module api.Module logger *logger.Logger } func (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 { r.repo = repo r.repoWriter = repoWriter r.command = nil r.commit = nil toRunLater := []CommandForDiff{} r.commitHook = func(hash plumbing.Hash) { lastCom, err := repoWriter.GetLastCommit(hash) if err != nil { r.logger.Error("can't GetLastCommit in start runtime", err) return } newCmd, err := CommandForDiffFromCommitCmd(ctx, r.plugin.commiter.SimpleUser, lastCom, r.command.branch) if err != nil { r.logger.Error("can't CommandForDiffFromCommitCmd in start runtime", err) return } toRunLater = append(toRunLater, newCmd) } r.mergeHook = mergeHook defer func() { r.commitHook = nil r.mergeHook = nil }() r.runCmd(ctx, plugins, repo, repoWriter, command) for _, cmd := range toRunLater { r.runCmd(ctx, plugins, repo, repoWriter, cmd) } return nil } func (r *runtime) runCmd(ctx context.Context, plugins []Plugin, repo *repository.GitRootRepository, repoWriter *repository.GitRootRepositoryWrite, command CommandForDiff) { fs := grfs.NewMultiple(ctx, map[string]fs.FS{ "worktree": r.repoWriter.ToFs(ctx), "webcontent": r.manager.conf.DataWeb(r.repo.Name()), }) for _, plugin := range plugins { fs.UpdateSubFs("cache", r.manager.conf.Cache(r.repo.Name(), plugin.Name)) r.plugin = plugin timerStop := r.logger.Time(fmt.Sprintf("Timer %s", plugin.Name)) r.logger.Debug("start plugin", logger.NewLoggerPair("repo", repo.Name()), logger.NewLoggerPair("name", plugin.Name)) m, err := r.loadModule(ctx, plugin, fs) if err != nil { r.logger.Error("loadModule for start error", err, logger.NewLoggerPair("name", plugin.Name)) continue } l := logger.NewLogger(logger.WASM) l.Debug("memory before", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Name)) cp := callPlugin{ manager: r.manager, plugin: plugin, repo: repo, repoWriter: repoWriter, module: m, logger: r.logger.NewSubLogger(plugin.Name), } if err := cp.callPluginForDiff(ctx, r, command); err != nil { r.logger.Error("finish plugin with error", err, logger.NewLoggerPair("name", plugin.Name)) } l.Debug("memory after", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Name)) r.logger.Debug("finish plugin", logger.NewLoggerPair("name", plugin.Name)) timerStop() } } func (c callPlugin) callPluginForDiff(ctx context.Context, r *runtime, cmd CommandForDiff) error { startCommit := c.module.ExportedFunction("startCommit") addFile := c.module.ExportedFunction("addFile") modFile := c.module.ExportedFunction("modFile") delFile := c.module.ExportedFunction("delFile") endCommit := c.module.ExportedFunction("endCommit") malloc := c.module.ExportedFunction("gitrootAlloc") if malloc == nil { malloc = c.module.ExportedFunction("malloc") } r.command = &cmd for _, pluginRun := range r.plugin.Run { r.pluginRun = pluginRun callOnAdd := addFile != nil && slices.Contains(pluginRun.When, pluginLib.PluginRunWhenAdd) callOnMod := modFile != nil && slices.Contains(pluginRun.When, pluginLib.PluginRunWhenMod) callOnDel := delFile != nil && slices.Contains(pluginRun.When, pluginLib.PluginRunWhenDel) isAuthorized := checkBranch(pluginRun, cmd.branch) if !isAuthorized { continue } if cmd.branchAction == commitForDiffActionDel { c.logger.Info("delete branch", logger.NewLoggerPair("branch", cmd.branch)) continue } atLeastOneFile := slices.ContainsFunc(cmd.commits, func(com commitForDiffCommit) bool { return slices.ContainsFunc(com.files, func(f pluginLib.File) bool { return pluginRun.glob.Match(f.Path) }) }) if !atLeastOneFile { c.logger.Info("no file match, skip execution", logger.NewLoggerPair("plugin", r.plugin.Name)) continue } if init := c.module.ExportedFunction("init"); init != nil { arg, err := pluginRun.Marshal() if err != nil { c.logger.Error("can't Marshal pluginRun", err, logger.NewLoggerPair("branch", cmd.branch)) continue } c.logger.Debug("init plugin", logger.NewLoggerPair("name", c.plugin.Name), logger.NewLoggerPair("arg", arg)) if err := r.writeMemoryAndCall(c.module, init, malloc, c.repo.Name(), "false", string(arg)); err != nil { c.logger.Error("can't init plugin", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Name)) continue } } else { c.logger.Info("no init fn to call", logger.NewLoggerPair("plugin", r.plugin.Name)) } for _, com := range cmd.commits { r.commit = &com comMarshalled, err := MarshallOne(cmd.branch.Short(), com) if err != nil { c.logger.Error("can't marshall commit", err, logger.NewLoggerPair("branch", cmd.branch)) continue } c.logger.Debug("diff start commit", logger.NewLoggerPair("com", comMarshalled)) if startCommit != nil { 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))) if err := r.writeMemoryAndCall(c.module, startCommit, malloc, comMarshalled); err != nil { c.logger.Error("startCommit error", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Name)) continue } } for _, f := range com.files { c.logger.Debug("diff start commit file", logger.NewLoggerPair("file", f.Path), logger.NewLoggerPair("action", f.Action)) if pluginRun.glob.Match(f.Path) { jsonFile, err := json.Marshal(f) if err != nil { c.logger.Error("can marshal file", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Name)) continue } if f.Action == pluginLib.FileActionTypeAdd && callOnAdd { c.logger.Debug("add", logger.NewLoggerPair("confPath", pluginRun.Path), logger.NewLoggerPair("currentPath", f.Path)) // creation r.writeMemoryAndCall(c.module, addFile, malloc, string(jsonFile)) } else { if f.Action == pluginLib.FileActionTypeDel && callOnDel { // deletion r.writeMemoryAndCall(c.module, delFile, malloc, string(jsonFile)) } else if f.Action == pluginLib.FileActionTypeMod && callOnMod { //modification r.writeMemoryAndCall(c.module, modFile, malloc, string(jsonFile)) } } } } if endCommit != nil { if err := r.writeMemoryAndCall(c.module, endCommit, malloc, comMarshalled); err != nil { c.logger.Error("endCommit error", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Name)) continue } } else { c.logger.Info("no endCommit fn to call", logger.NewLoggerPair("plugin", r.plugin.Name)) } } if finish := c.module.ExportedFunction("finish"); finish != nil { if _, err := finish.Call(ctx); err != nil { c.logger.Error("finish error", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Name)) continue } } else { c.logger.Info("no finish fn to call", logger.NewLoggerPair("plugin", r.plugin.Name)) } } return nil } func (r *runtime) writeMemoryAndCall(module api.Module, toCall api.Function, malloc api.Function, message ...string) error { _, err := r.writeMemoryAndCallWithRes(module, toCall, malloc, message...) if err != nil { return err } return nil } func (r *runtime) writeMemoryAndCallWithRes(module api.Module, toCall api.Function, malloc api.Function, message ...string) (uint64, error) { params := make([]uint64, 0) for _, m := range message { size := uint64(len(m)) results, err := malloc.Call(r.ctx, size) if err != nil { return 0, oops.Wrapf(err, "can't malloc memory for %s with %s", toCall.Definition().Name(), m) } ptr := results[0] // The pointer is a linear memory offset, which is where we write the name. if !module.Memory().Write(uint32(ptr), []byte(m)) { return 0, oops.Wrapf(err, "can't write memory") } params = append(params, ptr, size) } defer func() { ptrSizes := make([]ptrSize, 0) for i, d := range params { if i%2 == 0 { ptrSizes = append(ptrSizes, ptrSize{ptr: d, size: params[i+1]}) } } r.free(module, ptrSizes) }() if res, err := toCall.Call(r.ctx, params...); err != nil { return 0, oops.With("method", toCall.Definition().ExportNames()).Wrapf(err, "can't call") } else if len(res) > 0 { return res[0], nil } return 0, nil }