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 main
  6
  7// compile cmd
  8// tinygo build -o grafter-0.0.1.wasm -scheduler=none --no-debug -target=wasi ./
  9
 10import (
 11	_ "embed"
 12	"fmt"
 13	"io"
 14	"os"
 15	"path/filepath"
 16	"regexp"
 17	"strings"
 18
 19	"github.com/tidwall/gjson"
 20	gitroot "gitroot.dev/libs/golang/plugin"
 21)
 22
 23type Plugin struct {
 24	server   gitroot.Server
 25	conf     pluginConf
 26	newGraft *graft
 27}
 28
 29type pluginConf struct {
 30	defaultTargetBranch string
 31}
 32
 33type graft struct {
 34	isCreation         bool
 35	filename           string
 36	branch             string
 37	commitsLines       []string
 38	pushContent        []string
 39	content            string
 40	isCommitReview     bool
 41	isPostCommitReview bool
 42	currentHash        string
 43}
 44
 45var defaultRun = []gitroot.PluginRun{{
 46	Path:   "**/*",
 47	Branch: []string{"*"},
 48	When:   gitroot.PluginRunWhenAll,
 49	Write: gitroot.PluginWrite{
 50		Git: []gitroot.PluginWriteRight{{
 51			Path: "**/*",
 52			Can:  gitroot.PluginWriteRightCanAll,
 53		}},
 54		Web: []gitroot.PluginWriteRight{},
 55	},
 56	Configuration: map[string]any{"defaultTargetBranch": "main"},
 57}}
 58
 59var defaultGraft = `---
 60target: %s
 61status: draft
 62---
 63
 64# Want to merge %s
 65
 66Please add description of your work here.
 67
 68When your changes are ready to be reviewed, add at the end of this file a comment with:
 69
 70` + strings.Repeat("`", 3) + `markdown
 71---
 72
 73/review
 74` + strings.Repeat("`", 3) + "\n"
 75
 76var defaultReviewMessage = `
 77---
 78
 79Please review changes with:
 80
 81` + strings.Repeat("`", 3) + `shell
 82git fetch origin %s
 83git checkout %s
 84git diff origin/HEAD..HEAD
 85` + strings.Repeat("`", 3) + `
 86
 87Make your changes about the code inside the code directly (remarks, questions can be made in the form of comments). General remarks on the why can be made here. When done commit and push directly on this branch. Graft plugin will make the diff and display it here.
 88`
 89
 90var targetRegexp = regexp.MustCompile(`- target:\s(.*)`)
 91
 92func (p *Plugin) Init(repoName string, confHasChanged bool, serializedConf string) error {
 93	defaultTargetBranch := gjson.Parse(serializedConf).Get("defaultTargetBranch").String()
 94	if defaultTargetBranch == "" {
 95		defaultTargetBranch = "main"
 96	}
 97	p.conf = pluginConf{
 98		defaultTargetBranch: defaultTargetBranch,
 99	}
100	p.newGraft = nil
101	return nil
102}
103
104func (p *Plugin) StartCommit(commit gitroot.Commit) error {
105	//does nothing if user push on main
106	if commit.Branch == p.conf.defaultTargetBranch {
107		return nil
108	}
109	commitLine := fmt.Sprintf("### %s\n%s\n", commit.Message, commit.Hash)
110	if p.newGraft == nil {
111		graftFilepath := fmt.Sprintf("grafts/%s.md", commit.Branch)
112		file, err := p.server.Worktree().Open(graftFilepath)
113		if err != nil && !os.IsNotExist(err) {
114			return err
115		}
116		if file == nil {
117			p.newGraft = &graft{
118				isCreation:   true,
119				filename:     graftFilepath,
120				branch:       commit.Branch,
121				commitsLines: []string{commitLine},
122				content:      fmt.Sprintf(defaultGraft, p.conf.defaultTargetBranch, commit.Branch),
123			}
124		} else {
125			filecontent, err := io.ReadAll(file)
126			if err != nil {
127				return err
128			}
129			p.newGraft = &graft{
130				isCreation:   false,
131				filename:     graftFilepath,
132				branch:       commit.Branch,
133				commitsLines: []string{commitLine},
134				content:      string(filecontent),
135			}
136		}
137	} else {
138		p.newGraft.commitsLines = append(p.newGraft.commitsLines, commitLine)
139	}
140	p.newGraft.pushContent = []string{}
141	reviews := strings.Split(p.newGraft.content, "/review")
142	somethingAfterUserReview := len(reviews) >= 3 && strings.Contains(reviews[len(reviews)-1], "---")
143	p.newGraft.isCommitReview = len(reviews) >= 3 && !somethingAfterUserReview
144	p.newGraft.isPostCommitReview = strings.Contains(p.newGraft.content, "status: review")
145	p.newGraft.currentHash = commit.Hash
146	return nil
147}
148
149func (p *Plugin) AddFile(path string) error {
150	//does nothing if user push on main
151	if p.newGraft == nil {
152		return nil
153	}
154	if path != p.newGraft.filename {
155		from, _ := filepath.Abs(path)
156		relPath, _ := filepath.Rel("/grafts/", from)
157		p.newGraft.pushContent = append(p.newGraft.pushContent, fmt.Sprintf("- ++ [%s](%s)", path, filepath.Clean(relPath)))
158	}
159	return nil
160}
161
162func (p *Plugin) ModFile(fromPath string, toPath string) error {
163	//does nothing if user push on main
164	if p.newGraft == nil {
165		return nil
166	}
167
168	if toPath != p.newGraft.filename {
169		from, _ := filepath.Abs(toPath)
170		relPath, _ := filepath.Rel("/grafts/", from)
171		if fromPath != toPath {
172			p.newGraft.pushContent = append(p.newGraft.pushContent, fmt.Sprintf("- %s -> [%s](%s)", fromPath, toPath, filepath.Clean(relPath)))
173		}
174		p.newGraft.pushContent = append(p.newGraft.pushContent, fmt.Sprintf("- +- [%s](%s)", toPath, filepath.Clean(relPath)))
175		if p.newGraft.isPostCommitReview {
176			if diff, err := p.server.DiffWithParent(toPath, p.newGraft.currentHash); err == nil {
177				p.newGraft.pushContent = append(p.newGraft.pushContent, diff)
178			} else {
179				p.server.LogError("can't get diff", err)
180			}
181		}
182	}
183	return nil
184}
185
186func (p *Plugin) DelFile(path string) error {
187	//does nothing if user push on main
188	if p.newGraft == nil {
189		return nil
190	}
191	if path != p.newGraft.filename {
192		from, _ := filepath.Abs(path)
193		relPath, _ := filepath.Rel("/grafts/", from)
194		p.newGraft.pushContent = append(p.newGraft.pushContent, fmt.Sprintf("- -- [%s](%s)", path, filepath.Clean(relPath)))
195	}
196	return nil
197}
198
199func (p *Plugin) EndCommit(commit gitroot.Commit) error {
200	//does nothing if user push on main
201	if p.newGraft == nil {
202		return nil
203	}
204	if len(p.newGraft.pushContent) > 0 {
205		p.newGraft.commitsLines[len(p.newGraft.commitsLines)-1] = fmt.Sprintf("%s\n%s\n", p.newGraft.commitsLines[len(p.newGraft.commitsLines)-1], strings.Join(p.newGraft.pushContent, "\n"))
206	} else {
207		// a commit which touch others files or only graft file is ignored
208		p.newGraft.commitsLines = p.newGraft.commitsLines[:len(p.newGraft.commitsLines)-1]
209	}
210	return nil
211}
212
213func (p *Plugin) Finish() error {
214	// does nothing if user push on main
215	if p.newGraft == nil {
216		return nil
217	}
218	if len(p.newGraft.commitsLines) > 0 {
219		commit := "commit"
220		if len(p.newGraft.commitsLines) > 1 {
221			commit = "commits"
222		}
223		content := fmt.Sprintf("%s\n---\n\n## Push %d %s\n\n%s", p.newGraft.content, len(p.newGraft.commitsLines), commit, strings.Join(p.newGraft.commitsLines, "\n"))
224		p.server.ModifyContent(p.newGraft.filename, content)
225	}
226	if p.newGraft.isCommitReview {
227		content := strings.Replace(p.newGraft.content, "status: draft", "status: review\nreviewers: []", 1)
228		content = content + fmt.Sprintf(defaultReviewMessage, p.newGraft.branch, p.newGraft.branch)
229		p.server.ModifyContent(p.newGraft.filename, content)
230	}
231	needMerge := strings.Contains(p.newGraft.content, "/merge")
232	if needMerge {
233		content := strings.Replace(p.newGraft.content, "status: review", "status: done", 1)
234		p.server.ModifyContent(p.newGraft.filename, content)
235	}
236	message := "graft updated"
237	if p.newGraft.isCreation {
238		message = "graft created"
239	}
240	p.server.CommitAllIfNeeded(message)
241	if needMerge {
242		all := targetRegexp.FindStringSubmatch(p.newGraft.content)
243		if len(all) > 1 {
244			p.server.Merge(all[1], p.newGraft.branch)
245		} else {
246			p.server.Merge(p.conf.defaultTargetBranch, p.newGraft.branch)
247		}
248	}
249	return nil
250}
251
252func Build(server gitroot.Server) gitroot.Plugin {
253	return &Plugin{
254		server: server,
255	}
256}
257
258//go:wasmexport install
259func main() {
260	gitroot.Register(defaultRun, Build)
261}