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}