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
  7import (
  8	"fmt"
  9	"path/filepath"
 10	"regexp"
 11	"strings"
 12
 13	"github.com/tidwall/gjson"
 14	"gitroot.dev/libs/golang/glob"
 15	"gitroot.dev/libs/golang/plugin/model"
 16)
 17
 18type Conf struct {
 19	Title        string `json:"title"`
 20	Description  string `json:"description"`
 21	Format       string `json:"format"`
 22	TableHeader  string `json:"tableHeader"`
 23	For          string `json:"for"`
 24	glob         *glob.Glob
 25	Where        string   `json:"where"`
 26	Selects      []string `json:"selects"`
 27	selections   []*regexp.Regexp
 28	To           string            `json:"to"`
 29	Paginator    int64             `json:"paginator"`
 30	Sort         string            `json:"sort"`
 31	SortOrder    string            `json:"sortOrder"`
 32	Fs           string            `json:"filesystem"`
 33	GroupBy      string            `json:"groupBy"`
 34	Metadata     map[string]string `json:"metadata"`
 35	CustomLayout string            `json:"customLayout"`
 36}
 37
 38const roadmapDescription = `This file provides an overview of the direction this project is heading.
 39
 40> When it is obvious that the goals cannot be reached, don’t adjust the goals, adjust the action steps.
 41
 42_[Confucius](https://en.wikipedia.org/wiki/Confucius)_
 43
 44## List`
 45
 46var defaultConf = map[string]any{"boards": []Conf{
 47	{Title: "Roadmaps", Description: roadmapDescription, Format: "table", TableHeader: "| | priority |", For: "**/*.md", Where: "kind: 'roadmap'", Selects: []string{"priority: (\\d+)"}, To: "boards/roadmap.md", Paginator: 0, Sort: "title", SortOrder: "asc", Fs: "git"},
 48	{Title: "Todo is lie", Description: "", Format: "embed", TableHeader: "", For: "**/*|!.gitroot/**", Where: "// TODO", Selects: []string{}, To: "boards/todos.md", Paginator: 0, Sort: "file", SortOrder: "asc", Fs: "git"},
 49	{Title: "All Issues", Description: "All\n[New](./triage.md)", Format: "table", TableHeader: "| | status | priority |", For: "**/*.md", Where: "kind: issue", Selects: []string{"status: (.*)", "priority: (\\d+)"}, To: "boards/issues.md", Paginator: 10, Sort: "select[1]", SortOrder: "desc", Fs: "git"},
 50	{Title: "New Issues", Description: "[All](./issues.md)\nNew", Format: "list", TableHeader: "", For: "**/*.md", Where: "status: triage", Selects: []string{}, To: "boards/triage.md", Paginator: 10, Sort: "", SortOrder: "asc", Fs: "git"},
 51}}
 52
 53var defaultRun = []model.PluginRun{{
 54	Path:   "**/*",
 55	Branch: []string{"main"},
 56	When:   model.PluginRunWhenAll,
 57	Func:   []model.PluginFunc{},
 58	Write: model.PluginWrite{
 59		Git: []model.PluginWriteRight{{
 60			Path: "boards/*.md",
 61			Can:  model.PluginWriteRightCanAll,
 62		}},
 63		Web:  []model.PluginWriteRight{},
 64		Exec: []model.PluginExecRight{},
 65		CallFunc: []model.PluginCallFuncRight{
 66			{PluginPURL: PLUGIN_APEX, FuncName: PLUGIN_APEX_FUNC, Configuration: map[string]any{}},
 67			{PluginPURL: PLUGIN_MARKDOWN, FuncName: PLUGIN_MARKDOWN_FUNC, Configuration: map[string]any{}},
 68		},
 69	},
 70	Configuration: defaultConf,
 71}}
 72
 73func (p *Plugin) unmarshalConf(serializedConf string) []Conf {
 74	c := []Conf{}
 75	for _, meta := range gjson.Parse(serializedConf).Get("boards").Array() {
 76		selects := make([]string, 0)
 77		for _, s := range meta.Get("selects").Array() {
 78			selects = append(selects, s.String())
 79		}
 80		metadatas := map[string]string{}
 81		meta.Get("metadata").ForEach(func(key, value gjson.Result) bool {
 82			if key.String() != "" && value.String() != "" {
 83				metadatas[key.String()] = value.String()
 84			}
 85			return true
 86		})
 87		myConf := Conf{
 88			Title:        meta.Get("title").String(),
 89			Description:  meta.Get("description").String(),
 90			Format:       meta.Get("format").String(),
 91			TableHeader:  meta.Get("tableHeader").String(),
 92			For:          meta.Get("for").String(),
 93			Where:        meta.Get("where").String(),
 94			Selects:      selects,
 95			To:           meta.Get("to").String(),
 96			Paginator:    meta.Get("paginator").Int(),
 97			Sort:         meta.Get("sort").String(),
 98			SortOrder:    meta.Get("sortOrder").String(),
 99			Fs:           meta.Get("filesystem").String(),
100			GroupBy:      meta.Get("groupBy").String(),
101			Metadata:     metadatas,
102			CustomLayout: meta.Get("customLayout").String(),
103		}
104		//default format
105		if myConf.Format == "" {
106			myConf.Format = "list"
107		}
108		// default fs
109		if myConf.Fs == "" {
110			myConf.Fs = "git"
111		}
112		//select to selections
113		selections := make([]*regexp.Regexp, 0)
114		for _, se := range myConf.Selects {
115			if r, err := regexp.Compile(se); err != nil {
116				p.server.LogError("conf regexp.Compile select="+se, err)
117			} else {
118				selections = append(selections, r)
119			}
120		}
121		myConf.selections = selections
122		//default paginator
123		if myConf.Paginator == 0 {
124			myConf.Paginator = -1
125		}
126		//for glob
127		glob, err := glob.NewGlob(myConf.For)
128		if err != nil {
129			p.server.LogError("error glob", err)
130		}
131		myConf.glob = glob
132
133		c = append(c, myConf)
134	}
135	return c
136}
137
138func (c Conf) relativePath(toPath string) string {
139	absFromPath, _ := filepath.Abs(c.To)
140	dirFromPath, _ := filepath.Split(absFromPath)
141	absToPath, _ := filepath.Abs(toPath)
142	dirToPath, fileToPath := filepath.Split(absToPath)
143	path, err := filepath.Rel(dirFromPath, dirToPath)
144	if err != nil {
145		return toPath
146	}
147	return fmt.Sprintf("%s/%s", path, fileToPath)
148}
149
150func findTitle(path string, content []byte) string {
151	if strings.HasSuffix(path, ".md") { //special case, search first title
152		re := regexp.MustCompile(`(?m)^#\s(.*)$`)
153		t := re.FindSubmatch(content)
154		if len(t) > 1 {
155			return string(t[1])
156		}
157	}
158	return path
159}