ikurotime / gitgud

public
main / internal/interface/web/templates/helpers.go
5.1 KB · Go Raw
  1package templates
  2
  3import (
  4	"context"
  5	"fmt"
  6	"strconv"
  7	"strings"
  8	"time"
  9
 10	"gitgud/internal/domain"
 11	"gitgud/internal/interface/web/presenter"
 12)
 13
 14type ctxKey int
 15
 16const (
 17	flashKey ctxKey = iota
 18	csrfKey
 19)
 20
 21func WithFlash(ctx context.Context, msg string) context.Context {
 22	return context.WithValue(ctx, flashKey, msg)
 23}
 24
 25func flashOf(ctx context.Context) string {
 26	s, _ := ctx.Value(flashKey).(string)
 27	return s
 28}
 29
 30func WithCSRF(ctx context.Context, token string) context.Context {
 31	return context.WithValue(ctx, csrfKey, token)
 32}
 33
 34func csrfToken(ctx context.Context) string {
 35	s, _ := ctx.Value(csrfKey).(string)
 36	return s
 37}
 38
 39func markdown(s string) string {
 40	return presenter.RenderMarkdown([]byte(s))
 41}
 42
 43func fmtTime(t time.Time) string {
 44	return t.Format("2006-01-02 15:04")
 45}
 46
 47func stateAction(state domain.IssueState) string {
 48	if state == domain.IssueOpen {
 49		return "/close"
 50	}
 51	return "/reopen"
 52}
 53
 54func stateTabClass(current, want string) string {
 55	if current == want {
 56		return "font-semibold text-ink"
 57	}
 58	return "text-muted hover:text-ink"
 59}
 60
 61// baseURL is the external URL of this gitgud instance, used to build clone
 62// URLs shown in the UI. It defaults to the local dev address and is overridden
 63// at startup via SetBaseURL from config (GITGUD_BASE_URL).
 64var baseURL = "http://localhost:8080"
 65
 66// SetBaseURL configures the external base URL used in clone instructions.
 67func SetBaseURL(u string) {
 68	if u != "" {
 69		baseURL = strings.TrimRight(u, "/")
 70	}
 71}
 72
 73func cloneInstructions(repo *domain.Repository) string {
 74	url := baseURL + "/" + repo.OwnerName + "/" + repo.Name + ".git"
 75	return fmt.Sprintf(`git clone %s
 76cd %s
 77echo "# %s" > README.md
 78git add README.md
 79git commit -m "first commit"
 80git push -u origin %s`, url, repo.Name, repo.Name, repo.DefaultBranch)
 81}
 82
 83type Crumb struct {
 84	Name string
 85	Href string
 86}
 87
 88func treeCrumbs(repo *domain.Repository, ref, p string) []Crumb {
 89	base := "/" + repo.OwnerName + "/" + repo.Name + "/tree/" + ref
 90	crumbs := []Crumb{{Name: repo.Name, Href: base}}
 91	if p = strings.Trim(p, "/"); p != "" {
 92		acc := base
 93		for _, seg := range strings.Split(p, "/") {
 94			acc += "/" + seg
 95			crumbs = append(crumbs, Crumb{Name: seg, Href: acc})
 96		}
 97	}
 98	return crumbs
 99}
100
101func entryHref(repo *domain.Repository, ref string, e domain.TreeEntry) string {
102	kind := "blob"
103	if e.IsDir {
104		kind = "tree"
105	}
106	return "/" + repo.OwnerName + "/" + repo.Name + "/" + kind + "/" + ref + "/" + e.Path
107}
108
109func repoPath(repo *domain.Repository, sub string) string {
110	return "/" + repo.OwnerName + "/" + repo.Name + sub
111}
112
113func humanSize(n int64) string {
114	const unit = 1024
115	if n < unit {
116		return fmt.Sprintf("%d B", n)
117	}
118	div, exp := int64(unit), 0
119	for x := n / unit; x >= unit; x /= unit {
120		div *= unit
121		exp++
122	}
123	return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGTPE"[exp])
124}
125
126func itoa(n int) string {
127	return strconv.Itoa(n)
128}
129
130func plural(n int, one, many string) string {
131	if n == 1 {
132		return one
133	}
134	return many
135}
136
137func countVisibility(repos []*domain.Repository, private bool) int {
138	n := 0
139	for _, r := range repos {
140		if r.IsPrivate == private {
141			n++
142		}
143	}
144	return n
145}
146
147func initial(s string) string {
148	if s == "" {
149		return "?"
150	}
151	return strings.ToUpper(s[:1])
152}
153
154func firstLine(s string) string {
155	if i := strings.IndexByte(s, '\n'); i >= 0 {
156		return s[:i]
157	}
158	return s
159}
160
161func diffLineClass(line string) string {
162	switch {
163	case strings.HasPrefix(line, "@@"):
164		return "diff-hunk"
165	case strings.HasPrefix(line, "+"):
166		return "diff-add"
167	case strings.HasPrefix(line, "-"):
168		return "diff-del"
169	default:
170		return "text-muted"
171	}
172}
173
174// statBlocks renders a 5-square GitHub-style diffstat: green for additions,
175// red for deletions, proportional to the change, padded with neutral squares.
176func statBlocks(added, deleted int) []string {
177	blocks := make([]string, 5)
178	total := added + deleted
179	if total == 0 {
180		for i := range blocks {
181			blocks[i] = "none"
182		}
183		return blocks
184	}
185	greens := added * 5 / total
186	if added > 0 && greens == 0 {
187		greens = 1
188	}
189	reds := 5 - greens
190	if deleted > 0 && reds == 0 && greens > 0 {
191		greens--
192		reds = 1
193	}
194	for i := range blocks {
195		switch {
196		case i < greens:
197			blocks[i] = "add"
198		case i < greens+reds:
199			blocks[i] = "del"
200		default:
201			blocks[i] = "none"
202		}
203	}
204	return blocks
205}
206
207// langLabel maps a file path to a human language label for the blob header.
208func langLabel(path string) string {
209	ext := strings.ToLower(path)
210	if i := strings.LastIndexByte(ext, '.'); i >= 0 {
211		ext = ext[i+1:]
212	}
213	switch ext {
214	case "go":
215		return "Go"
216	case "js", "mjs", "cjs":
217		return "JavaScript"
218	case "ts", "tsx":
219		return "TypeScript"
220	case "py":
221		return "Python"
222	case "rs":
223		return "Rust"
224	case "rb":
225		return "Ruby"
226	case "java":
227		return "Java"
228	case "c", "h":
229		return "C"
230	case "cpp", "cc", "hpp":
231		return "C++"
232	case "sh", "bash", "zsh":
233		return "Shell"
234	case "html", "htm":
235		return "HTML"
236	case "css":
237		return "CSS"
238	case "json":
239		return "JSON"
240	case "yml", "yaml":
241		return "YAML"
242	case "md", "markdown":
243		return "Markdown"
244	case "sql":
245		return "SQL"
246	case "templ":
247		return "Templ"
248	case "":
249		return "Text"
250	default:
251		return strings.ToUpper(ext)
252	}
253}
254
255func diffLines(patch string) []string {
256	return strings.Split(strings.TrimRight(patch, "\n"), "\n")
257}