GitRoot

Craft your forge, Build your project, Grow your community freely
  1// SPDX-FileCopyrightText: 2026 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package plugin
  6
  7import (
  8	"context"
  9	"fmt"
 10	"io/fs"
 11	"slices"
 12
 13	"github.com/go-git/go-git/v6/plumbing"
 14	"github.com/go-git/go-git/v6/plumbing/protocol/packp"
 15	"github.com/samber/oops"
 16	"gitroot.dev/libs/golang/plugin/model"
 17	grfs "gitroot.dev/server/fs"
 18	"gitroot.dev/server/logger"
 19)
 20
 21type CmdToExec struct {
 22	commands     []*packp.Command
 23	commitsByRef map[plumbing.ReferenceName][]plumbing.Hash
 24}
 25
 26func (r *runtime) schedule(ctx context.Context, repoName string, plugin Plugin, pluginRun PluginRun) error {
 27	r.command = nil
 28	r.commit = nil
 29	r.plugin = plugin
 30
 31	timerStop := r.logger.Time(fmt.Sprintf("Timer %s", plugin.Log()))
 32
 33	repo, err := r.manager.repoManager.Open(logger.AddCaller(r.ctx, "runtime.schedule"), repoName)
 34	if err != nil {
 35		return oops.Wrapf(err, "can't open repo")
 36	}
 37	r.repo = repo
 38
 39	commandsToExec := []CmdToExec{}
 40
 41	defer func() {
 42		r.repo = nil
 43		r.repoWriter = nil
 44		r.command = nil
 45		r.commitHook = nil
 46		r.commit = nil
 47		r.logger.Debug("will close repo in schedule")
 48		repo.Close()
 49		r.logger.Debug("start next cmd for schedule")
 50		for _, cmd := range commandsToExec {
 51			r.manager.backgroundManager.PostPush(plugin.commiter.SimpleUser, repoName, cmd.commands, cmd.commitsByRef)
 52		}
 53	}()
 54
 55	branches, err := repo.Branches()
 56	if err != nil {
 57		return oops.Wrapf(err, "can't get branches")
 58	}
 59
 60	fs := grfs.NewMultiple(ctx, map[string]fs.FS{
 61		"webcontent": r.manager.conf.DataWeb(r.repo.Name()),
 62		"cache":      r.manager.conf.Cache(r.repo.Name(), plugin.NamespaceAndName()),
 63	})
 64	m, err := r.loadModule(ctx, plugin, fs)
 65	if err != nil {
 66		return oops.Wrapf(err, "loadModule for schedule error")
 67	}
 68	l := logger.NewLogger(logger.WASM)
 69	l.Debug("memory before schedule", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Log()))
 70
 71	branchesToRun := branchesToRunOn(pluginRun.Branch, branches)
 72
 73	for _, branch := range branchesToRun {
 74		r.logger.Debug("start branch plugin schedule", logger.NewLoggerPair("branch", branch), logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("name", plugin.Log()))
 75		repoWriter, err := repo.WillWrite(branch)
 76		if err != nil {
 77			r.logger.Error("can't WillWrite for schedule", err, logger.NewLoggerPair("name", plugin.Log()))
 78			continue
 79		}
 80		r.repoWriter = repoWriter
 81		fs.UpdateSubFs("worktree", r.repoWriter.ToFs(ctx))
 82
 83		refOld, err := repoWriter.Storer().Reference(branch)
 84		if err != nil {
 85			r.logger.Error("can't repoWriter.Storer().Reference for schedule", err, logger.NewLoggerPair("name", plugin.Log()))
 86			repo.StopWrite()
 87			continue
 88		}
 89
 90		newCommitsHash := []plumbing.Hash{}
 91		r.commitHook = func(hash plumbing.Hash) {
 92			newCommitsHash = append(newCommitsHash, hash)
 93		}
 94
 95		malloc := m.ExportedFunction("gitrootAlloc")
 96		if malloc == nil {
 97			malloc = m.ExportedFunction("malloc")
 98		}
 99
100		isAuthorized := checkBranch(pluginRun, branch)
101		if !isAuthorized {
102			repo.StopWrite()
103			continue
104		}
105		r.pluginRun = pluginRun
106		if init := m.ExportedFunction("init"); init != nil {
107			arg, err := pluginRun.Marshal()
108			if err != nil {
109				r.logger.Error("can't marshall pluginRun", err, logger.NewLoggerPair("name", plugin.Log()))
110				repo.StopWrite()
111				continue
112			}
113			r.logger.Debug("init plugin schedule", logger.NewLoggerPair("name", plugin.Log()), logger.NewLoggerPair("arg", arg))
114			if err := r.writeMemoryAndCall(m, init, malloc, repo.Name(), string(model.InitKindSchedule), string(arg)); err != nil {
115				r.logger.Error("can't writeMemoryAndCall init", err, logger.NewLoggerPair("name", plugin.Log()))
116				repo.StopWrite()
117				continue
118			}
119		}
120
121		//need to commit before generate next command
122		repo.StopWrite()
123
124		if len(newCommitsHash) > 0 {
125			packpCmd := []*packp.Command{{Name: branch, Old: refOld.Hash(), New: newCommitsHash[len(newCommitsHash)-1]}}
126			hashByRef := map[plumbing.ReferenceName][]plumbing.Hash{
127				branch: newCommitsHash,
128			}
129			commandsToExec = append(commandsToExec, CmdToExec{
130				commands:     packpCmd,
131				commitsByRef: hashByRef,
132			})
133		}
134	}
135
136	l.Debug("memory after schedule", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Log()))
137	r.logger.Debug("finish plugin schedule", logger.NewLoggerPair("name", plugin.Log()))
138	timerStop()
139	return nil
140}
141
142func branchesToRunOn(confBranches []string, availableBranches []plumbing.ReferenceName) []plumbing.ReferenceName {
143	hasStar := slices.Contains(confBranches, "*")
144	return slices.DeleteFunc(availableBranches, func(b plumbing.ReferenceName) bool {
145		// remove all branches marked has not
146		if ok := slices.Contains(confBranches, fmt.Sprintf("!%s", b.Short())); ok {
147			return true
148		}
149		// do not remove rest branches
150		if ok := slices.Contains(confBranches, fmt.Sprintf("%s", b.Short())); ok {
151			return false
152		}
153		// keep the rest if star
154		return !hasStar
155	})
156}