1diff --git a/config.example.scfg b/config.example.scfg
2index 1e3180f17834707170c018db8e31909eabda4cfe..3961e51ddb60a7deb021ad48db74525d2eae913e 100644
3--- a/config.example.scfg
4+++ b/config.example.scfg
5@@ -3,6 +3,12 @@ scan /srv/git/ {
6 public true
7 }
8
9+repository /srv/git/cerrado.git {
10+ name cerrado
11+ description "Self host single person forge"
12+ public true
13+}
14+
15 # TBD
16 #user admin:iKlvHe1g0UoXE
17 #
18@@ -15,9 +21,3 @@ # authentication plain
19 # default false
20 #}
21 #
22-#repository cerrado {
23-# title Cerrado
24-# description "Self host single person readonly forge"
25-# list main
26-# public true
27-#}
28diff --git a/pkg/config/config.go b/pkg/config/config.go
29index 419d49dac3a06f7357de72162885c2b822be8e07..3e539f7091a7b2b1e39ee96b7bd54136d052fd2b 100644
30--- a/pkg/config/config.go
31+++ b/pkg/config/config.go
32@@ -6,6 +6,7 @@ "fmt"
33 "io"
34 "os"
35 "path"
36+ "path/filepath"
37 "strconv"
38
39 "git.gabrielgio.me/cerrado/pkg/u"
40@@ -13,8 +14,9 @@ "git.sr.ht/~emersion/go-scfg"
41 )
42
43 var (
44- ScanPathErr = errors.New("Scan path does not exist")
45- RepoPathErr = errors.New("Repository path does not exist")
46+ ScanPathErr = errors.New("Scan path does not exist")
47+ RepoPathErr = errors.New("Repository path does not exist")
48+ InvalidPropertyErr = errors.New("Invalid property")
49 )
50
51 type (
52@@ -26,16 +28,19 @@ Public bool
53 }
54
55 // configuration represents file configuration.
56+ // fields needs to be exported to cmp to work
57 configuration struct {
58- Scan *scan
59- RootReadme string
60+ Scan *scan
61+ RootReadme string
62+ Repositories []*GitRepositoryConfiguration
63 }
64
65 // This is a per repository configuration.
66 GitRepositoryConfiguration struct {
67- Name string
68- Path string
69- Public bool
70+ Name string
71+ Path string
72+ Description string
73+ Public bool
74 }
75
76 // ConfigurationRepository represents the configuration repository (as in
77@@ -60,13 +65,17 @@ return nil, err
78 }
79
80 repo := &ConfigurationRepository{
81- rootReadme: config.RootReadme,
82+ rootReadme: config.RootReadme,
83+ repositories: config.Repositories,
84 }
85
86- err = repo.expandOnScanPath(config.Scan.Path, config.Scan.Public)
87- if err != nil {
88- return nil, err
89+ if config.Scan.Path != "" {
90+ err = repo.expandOnScanPath(config.Scan.Path, config.Scan.Public)
91+ if err != nil {
92+ return nil, err
93+ }
94 }
95+
96 return repo, nil
97
98 }
99@@ -104,22 +113,32 @@ if err != nil {
100 return err
101 }
102
103- c.repositories = make([]*GitRepositoryConfiguration, 0)
104 for _, e := range entries {
105 if !e.IsDir() {
106 continue
107 }
108
109 fullPath := path.Join(scanPath, e.Name())
110- c.repositories = append(c.repositories, &GitRepositoryConfiguration{
111- Name: e.Name(),
112- Path: fullPath,
113- Public: public,
114- })
115+ if !c.repoExits(fullPath) {
116+ c.repositories = append(c.repositories, &GitRepositoryConfiguration{
117+ Name: e.Name(),
118+ Path: fullPath,
119+ Public: public,
120+ })
121+ }
122 }
123 return nil
124 }
125
126+func (c *ConfigurationRepository) repoExits(path string) bool {
127+ for _, r := range c.repositories {
128+ if path == r.Path {
129+ return true
130+ }
131+ }
132+ return false
133+}
134+
135 func parse(r io.Reader) (*configuration, error) {
136 block, err := scfg.Read(r)
137 if err != nil {
138@@ -138,16 +157,82 @@ if err != nil {
139 return nil, err
140 }
141
142+ err = setRepositories(block, &config.Repositories)
143+ if err != nil {
144+ return nil, err
145+ }
146+
147 return config, nil
148 }
149
150+func setRepositories(block scfg.Block, repositories *[]*GitRepositoryConfiguration) error {
151+ blocks := block.GetAll("repository")
152+
153+ for _, r := range blocks {
154+ if len(r.Params) != 1 {
155+ return fmt.Errorf(
156+ "Invlid number of params for repository: %w",
157+ InvalidPropertyErr,
158+ )
159+ }
160+
161+ path := u.FirstOrZero(r.Params)
162+ repository := defaultRepisotryConfiguration(path)
163+
164+ for _, d := range r.Children {
165+ // under repository there is only single param properties
166+ if len(d.Params) != 1 {
167+ return fmt.Errorf(
168+ "Invlid number of params for %s: %w",
169+ d.Name,
170+ InvalidPropertyErr,
171+ )
172+ }
173+
174+ switch d.Name {
175+ case "name":
176+ if err := setString(d, &repository.Name); err != nil {
177+ return err
178+ }
179+ case "description":
180+ if err := setString(d, &repository.Description); err != nil {
181+ return err
182+ }
183+ case "public":
184+ if err := setBool(d, &repository.Public); err != nil {
185+ return err
186+ }
187+ }
188+ }
189+
190+ *repositories = append(*repositories, repository)
191+ }
192+
193+ return nil
194+}
195+
196 func defaultConfiguration() *configuration {
197 return &configuration{
198- Scan: &scan{
199- Public: true,
200- Path: "",
201- },
202- RootReadme: "",
203+ Scan: defaultScan(),
204+ RootReadme: "",
205+ Repositories: make([]*GitRepositoryConfiguration, 0),
206+ }
207+}
208+
209+func defaultScan() *scan {
210+ return &scan{
211+ Public: false,
212+ Path: "",
213+ }
214+
215+}
216+
217+func defaultRepisotryConfiguration(path string) *GitRepositoryConfiguration {
218+ return &GitRepositoryConfiguration{
219+ Path: path,
220+ Name: filepath.Base(path),
221+ Description: "",
222+ Public: false,
223 }
224 }
225
226@@ -158,6 +243,9 @@ }
227
228 func setScan(block scfg.Block, scan *scan) error {
229 scanDir := block.Get("scan")
230+ if scanDir == nil {
231+ return nil
232+ }
233 err := setString(scanDir, &scan.Path)
234 if err != nil {
235 return err
236@@ -182,7 +270,7 @@ }
237
238 func setString(dir *scfg.Directive, field *string) error {
239 if dir != nil {
240- *field, _ = u.First(dir.Params)
241+ *field = u.FirstOrZero(dir.Params)
242 }
243 return nil
244 }
245diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
246index 7afbaef263f86b987b586cc5cb70cc6488a2d994..9109ecb47e242baa141860f478524b2299f33d8a 100644
247--- a/pkg/config/config_test.go
248+++ b/pkg/config/config_test.go
249@@ -19,21 +19,94 @@ name: "minimal scan",
250 config: `scan "/srv/git"`,
251 expectedConfig: &configuration{
252 Scan: &scan{
253- Public: true,
254+ Public: false,
255 Path: "/srv/git",
256 },
257+ Repositories: []*GitRepositoryConfiguration{},
258 },
259 },
260 {
261 name: "complete scan",
262- config: `scan "/srv/git" {
263- public false
264+ config: `
265+scan "/srv/git" {
266+ public true
267 }`,
268 expectedConfig: &configuration{
269 Scan: &scan{
270- Public: false,
271+ Public: true,
272 Path: "/srv/git",
273 },
274+ Repositories: []*GitRepositoryConfiguration{},
275+ },
276+ },
277+ {
278+ name: "minimal repository",
279+ config: `repository /srv/git/cerrado.git`,
280+ expectedConfig: &configuration{
281+ Scan: defaultScan(),
282+ Repositories: []*GitRepositoryConfiguration{
283+ {
284+ Name: "cerrado.git",
285+ Path: "/srv/git/cerrado.git",
286+ Description: "",
287+ Public: false,
288+ },
289+ },
290+ },
291+ },
292+ {
293+ name: "complete repository",
294+ config: `
295+repository /srv/git/cerrado.git {
296+ name cerrado
297+ description "Single person forge"
298+ public true
299+}`,
300+ expectedConfig: &configuration{
301+ Scan: defaultScan(),
302+ Repositories: []*GitRepositoryConfiguration{
303+ {
304+ Name: "cerrado",
305+ Path: "/srv/git/cerrado.git",
306+ Description: "Single person forge",
307+ Public: true,
308+ },
309+ },
310+ },
311+ },
312+ {
313+ name: "complete",
314+ config: `
315+scan "/srv/git" {
316+ public true
317+}
318+
319+repository /srv/git/linux.git
320+
321+repository /srv/git/cerrado.git {
322+ name cerrado
323+ description "Single person forge"
324+ public true
325+}`,
326+ expectedConfig: &configuration{
327+ Scan: &scan{
328+ Public: true,
329+ Path: "/srv/git",
330+ },
331+ Repositories: []*GitRepositoryConfiguration{
332+ {
333+ Name: "linux.git",
334+ Path: "/srv/git/linux.git",
335+ Description: "",
336+ Public: false,
337+ },
338+ {
339+ Name: "cerrado",
340+ Path: "/srv/git/cerrado.git",
341+ Description: "Single person forge",
342+ Public: true,
343+ },
344+ },
345 },
346 },
347 }
348@@ -49,7 +122,6 @@
349 if diff := cmp.Diff(tc.expectedConfig, config); diff != "" {
350 t.Errorf("Wrong result given - wanted + got\n %s", diff)
351 }
352-
353 })
354
355 }
356diff --git a/pkg/service/git.go b/pkg/service/git.go
357index 94e2adc5ab4433d9cc113f7a83d7c49aafd12787..7418d971dffc7b2ff26d9ab900e94d79bba2b75a 100644
358--- a/pkg/service/git.go
359+++ b/pkg/service/git.go
360@@ -16,7 +16,6 @@
361 type (
362 Repository struct {
363 Name string
364- Title string
365 Description string
366 LastCommitDate string
367 Ref string
368@@ -48,8 +47,8 @@
369 func (g *GitService) ListRepositories() ([]*Repository, error) {
370 rs := g.configRepo.List()
371
372- repos := make([]*Repository, len(rs))
373- for i, r := range rs {
374+ repos := make([]*Repository, 0, len(rs))
375+ for _, r := range rs {
376 repo, err := git.OpenRepository(r.Path)
377 if err != nil {
378 return nil, err
379@@ -57,12 +56,14 @@ }
380
381 obj, err := repo.LastCommit()
382 if err != nil {
383- return nil, err
384+ slog.Error("Error fetching last commit", "repository", r.Path, "error", err)
385+ continue
386 }
387
388 head, err := repo.Head()
389 if err != nil {
390- return nil, err
391+ slog.Error("Error fetching head", "repository", r.Path, "error", err)
392+ continue
393 }
394
395 d := path.Join(r.Path, "description")
396@@ -75,14 +76,12 @@ slog.Error("Error loading description file", "err", err)
397 }
398 }
399
400- baseName := path.Base(r.Path)
401- repos[i] = &Repository{
402- Name: baseName,
403- Title: baseName,
404+ repos = append(repos, &Repository{
405+ Name: r.Name,
406 Description: description,
407 LastCommitDate: obj.Author.When.Format(timeFormat),
408 Ref: head.Name().Short(),
409- }
410+ })
411 }
412
413 return repos, nil
414diff --git a/pkg/u/list.go b/pkg/u/list.go
415index cf71909e439d7726b4c6d9c6fcea771ad776a5bf..7271ef3783e892aad55ea49786515f491af30735 100644
416--- a/pkg/u/list.go
417+++ b/pkg/u/list.go
418@@ -16,6 +16,14 @@ }
419 return v[0]
420 }
421
422+func Map[T any, V any](ts []T, fun func(T) V) []V {
423+ rs := make([]V, len(ts))
424+ for i := range ts {
425+ rs[i] = fun(ts[i])
426+ }
427+ return rs
428+}
429+
430 func ChunkBy[T any](items []T, chunkSize int) [][]T {
431 var chunks = make([][]T, 0, (len(items)/chunkSize)+1)
432 for chunkSize < len(items) {
433diff --git a/pkg/u/list_test.go b/pkg/u/list_test.go
434index 805a2091b59ce4c644f748b4b6c49a28aec50cdb..3a856b961818256ffdd0842824879169af645f7b 100644
435--- a/pkg/u/list_test.go
436+++ b/pkg/u/list_test.go
437@@ -3,6 +3,7 @@
438 package u
439
440 import (
441+ "strconv"
442 "testing"
443
444 "github.com/google/go-cmp/cmp"
445@@ -129,3 +130,32 @@
446 })
447 }
448 }
449+
450+func TestMap(t *testing.T) {
451+ testCases := []struct {
452+ name string
453+ in []int
454+ out []string
455+ }{
456+ {
457+ name: "empty",
458+ in: []int{},
459+ out: []string{},
460+ },
461+ {
462+ name: "not empty",
463+ in: []int{1, 2, 3},
464+ out: []string{"1", "2", "3"},
465+ },
466+ }
467+
468+ for _, tc := range testCases {
469+ t.Run(tc.name, func(t *testing.T) {
470+ out := Map(tc.in, func(v int) string { return strconv.Itoa(v) })
471+
472+ if diff := cmp.Diff(tc.out, out); diff != "" {
473+ t.Errorf("Map error:\n%s", diff)
474+ }
475+ })
476+ }
477+}
478diff --git a/templates/base.qtpl b/templates/base.qtpl
479index 9b0c4f57081d1861c77a7a75a4b25cafef57b6f4..ae9f7a6c50f072bfc14a2edf7c1af335f19e0a1b 100644
480--- a/templates/base.qtpl
481+++ b/templates/base.qtpl
482@@ -42,7 +42,7 @@ <html lang="en">
483 <head>
484 <meta charset="utf-8">
485 <link rel="icon" href="data:,">
486- <title>cerrado | {%= p.Title() %}</title>
487+ <title>{%= p.Title() %}</title>
488 <link rel="stylesheet" href="/static/main{%s Slug%}.css">
489 <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
490 <meta name="viewport" content="width=device-width, initial-scale=1" />
491diff --git a/templates/base.qtpl.go b/templates/base.qtpl.go
492index d2bcc7369a0587ee3edf2f641a333666d598e33e..bc40252cea03af8df732d1a1b29d11716260bca4 100644
493--- a/templates/base.qtpl.go
494+++ b/templates/base.qtpl.go
495@@ -87,7 +87,7 @@ <html lang="en">
496 <head>
497 <meta charset="utf-8">
498 <link rel="icon" href="data:,">
499- <title>cerrado | `)
500+ <title>`)
501 //line base.qtpl:45
502 p.StreamTitle(qw422016)
503 //line base.qtpl:45