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}