cerrado @ 02614b3781f6acdfc6df0e7b07d856b2779c4ac7

feat: Per repository configuration
diff --git a/config.example.scfg b/config.example.scfg
index 1e3180f17834707170c018db8e31909eabda4cfe..3961e51ddb60a7deb021ad48db74525d2eae913e 100644
--- a/config.example.scfg
+++ b/config.example.scfg
@@ -3,6 +3,12 @@ scan /srv/git/ {
     public true
 }
 
+repository /srv/git/cerrado.git {
+    name cerrado
+    description "Self host single person forge"
+    public true
+}
+
 # TBD
 #user admin:iKlvHe1g0UoXE
 #
@@ -15,9 +21,3 @@ #    authentication plain
 #    default false
 #}
 #
-#repository cerrado {
-#    title Cerrado
-#    description "Self host single person readonly forge"
-#    list main
-#    public true
-#}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 419d49dac3a06f7357de72162885c2b822be8e07..3e539f7091a7b2b1e39ee96b7bd54136d052fd2b 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -6,6 +6,7 @@ 	"fmt"
 	"io"
 	"os"
 	"path"
+	"path/filepath"
 	"strconv"
 
 	"git.gabrielgio.me/cerrado/pkg/u"
@@ -13,8 +14,9 @@ 	"git.sr.ht/~emersion/go-scfg"
 )
 
 var (
-	ScanPathErr = errors.New("Scan path does not exist")
-	RepoPathErr = errors.New("Repository path does not exist")
+	ScanPathErr        = errors.New("Scan path does not exist")
+	RepoPathErr        = errors.New("Repository path does not exist")
+	InvalidPropertyErr = errors.New("Invalid property")
 )
 
 type (
@@ -26,16 +28,19 @@ 		Public bool
 	}
 
 	// configuration represents file configuration.
+	// fields needs to be exported to cmp to work
 	configuration struct {
-		Scan       *scan
-		RootReadme string
+		Scan         *scan
+		RootReadme   string
+		Repositories []*GitRepositoryConfiguration
 	}
 
 	// This is a per repository configuration.
 	GitRepositoryConfiguration struct {
-		Name   string
-		Path   string
-		Public bool
+		Name        string
+		Path        string
+		Description string
+		Public      bool
 	}
 
 	// ConfigurationRepository represents the configuration repository (as in
@@ -60,13 +65,17 @@ 		return nil, err
 	}
 
 	repo := &ConfigurationRepository{
-		rootReadme: config.RootReadme,
+		rootReadme:   config.RootReadme,
+		repositories: config.Repositories,
 	}
 
-	err = repo.expandOnScanPath(config.Scan.Path, config.Scan.Public)
-	if err != nil {
-		return nil, err
+	if config.Scan.Path != "" {
+		err = repo.expandOnScanPath(config.Scan.Path, config.Scan.Public)
+		if err != nil {
+			return nil, err
+		}
 	}
+
 	return repo, nil
 
 }
@@ -104,22 +113,32 @@ 	if err != nil {
 		return err
 	}
 
-	c.repositories = make([]*GitRepositoryConfiguration, 0)
 	for _, e := range entries {
 		if !e.IsDir() {
 			continue
 		}
 
 		fullPath := path.Join(scanPath, e.Name())
-		c.repositories = append(c.repositories, &GitRepositoryConfiguration{
-			Name:   e.Name(),
-			Path:   fullPath,
-			Public: public,
-		})
+		if !c.repoExits(fullPath) {
+			c.repositories = append(c.repositories, &GitRepositoryConfiguration{
+				Name:   e.Name(),
+				Path:   fullPath,
+				Public: public,
+			})
+		}
 	}
 	return nil
 }
 
+func (c *ConfigurationRepository) repoExits(path string) bool {
+	for _, r := range c.repositories {
+		if path == r.Path {
+			return true
+		}
+	}
+	return false
+}
+
 func parse(r io.Reader) (*configuration, error) {
 	block, err := scfg.Read(r)
 	if err != nil {
@@ -138,16 +157,82 @@ 	if err != nil {
 		return nil, err
 	}
 
+	err = setRepositories(block, &config.Repositories)
+	if err != nil {
+		return nil, err
+	}
+
 	return config, nil
 }
 
+func setRepositories(block scfg.Block, repositories *[]*GitRepositoryConfiguration) error {
+	blocks := block.GetAll("repository")
+
+	for _, r := range blocks {
+		if len(r.Params) != 1 {
+			return fmt.Errorf(
+				"Invlid number of params for repository: %w",
+				InvalidPropertyErr,
+			)
+		}
+
+		path := u.FirstOrZero(r.Params)
+		repository := defaultRepisotryConfiguration(path)
+
+		for _, d := range r.Children {
+			// under repository there is only single param properties
+			if len(d.Params) != 1 {
+				return fmt.Errorf(
+					"Invlid number of params for %s: %w",
+					d.Name,
+					InvalidPropertyErr,
+				)
+			}
+
+			switch d.Name {
+			case "name":
+				if err := setString(d, &repository.Name); err != nil {
+					return err
+				}
+			case "description":
+				if err := setString(d, &repository.Description); err != nil {
+					return err
+				}
+			case "public":
+				if err := setBool(d, &repository.Public); err != nil {
+					return err
+				}
+			}
+		}
+
+		*repositories = append(*repositories, repository)
+	}
+
+	return nil
+}
+
 func defaultConfiguration() *configuration {
 	return &configuration{
-		Scan: &scan{
-			Public: true,
-			Path:   "",
-		},
-		RootReadme: "",
+		Scan:         defaultScan(),
+		RootReadme:   "",
+		Repositories: make([]*GitRepositoryConfiguration, 0),
+	}
+}
+
+func defaultScan() *scan {
+	return &scan{
+		Public: false,
+		Path:   "",
+	}
+
+}
+
+func defaultRepisotryConfiguration(path string) *GitRepositoryConfiguration {
+	return &GitRepositoryConfiguration{
+		Path:        path,
+		Name:        filepath.Base(path),
+		Description: "",
+		Public:      false,
 	}
 }
 
@@ -158,6 +243,9 @@ }
 
 func setScan(block scfg.Block, scan *scan) error {
 	scanDir := block.Get("scan")
+	if scanDir == nil {
+		return nil
+	}
 	err := setString(scanDir, &scan.Path)
 	if err != nil {
 		return err
@@ -182,7 +270,7 @@ }
 
 func setString(dir *scfg.Directive, field *string) error {
 	if dir != nil {
-		*field, _ = u.First(dir.Params)
+		*field = u.FirstOrZero(dir.Params)
 	}
 	return nil
 }
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 7afbaef263f86b987b586cc5cb70cc6488a2d994..9109ecb47e242baa141860f478524b2299f33d8a 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -19,21 +19,94 @@ 			name:   "minimal scan",
 			config: `scan "/srv/git"`,
 			expectedConfig: &configuration{
 				Scan: &scan{
-					Public: true,
+					Public: false,
 					Path:   "/srv/git",
 				},
+				Repositories: []*GitRepositoryConfiguration{},
 			},
 		},
 		{
 			name: "complete scan",
-			config: `scan "/srv/git" {
-	public false
+			config: `
+scan "/srv/git" {
+	public true
 }`,
 			expectedConfig: &configuration{
 				Scan: &scan{
-					Public: false,
+					Public: true,
 					Path:   "/srv/git",
 				},
+				Repositories: []*GitRepositoryConfiguration{},
+			},
+		},
+		{
+			name:   "minimal repository",
+			config: `repository /srv/git/cerrado.git`,
+			expectedConfig: &configuration{
+				Scan: defaultScan(),
+				Repositories: []*GitRepositoryConfiguration{
+					{
+						Name:        "cerrado.git",
+						Path:        "/srv/git/cerrado.git",
+						Description: "",
+						Public:      false,
+					},
+				},
+			},
+		},
+		{
+			name: "complete repository",
+			config: `
+repository /srv/git/cerrado.git {
+	name cerrado
+	description "Single person forge"
+	public true
+}`,
+			expectedConfig: &configuration{
+				Scan: defaultScan(),
+				Repositories: []*GitRepositoryConfiguration{
+					{
+						Name:        "cerrado",
+						Path:        "/srv/git/cerrado.git",
+						Description: "Single person forge",
+						Public:      true,
+					},
+				},
+			},
+		},
+		{
+			name: "complete",
+			config: `
+scan "/srv/git" {
+	public true
+}
+
+repository /srv/git/linux.git
+
+repository /srv/git/cerrado.git {
+	name cerrado
+	description "Single person forge"
+	public true
+}`,
+			expectedConfig: &configuration{
+				Scan: &scan{
+					Public: true,
+					Path:   "/srv/git",
+				},
+				Repositories: []*GitRepositoryConfiguration{
+					{
+						Name:        "linux.git",
+						Path:        "/srv/git/linux.git",
+						Description: "",
+						Public:      false,
+					},
+					{
+						Name:        "cerrado",
+						Path:        "/srv/git/cerrado.git",
+						Description: "Single person forge",
+						Public:      true,
+					},
+				},
 			},
 		},
 	}
@@ -49,7 +122,6 @@
 			if diff := cmp.Diff(tc.expectedConfig, config); diff != "" {
 				t.Errorf("Wrong result given - wanted + got\n %s", diff)
 			}
-
 		})
 
 	}
diff --git a/pkg/service/git.go b/pkg/service/git.go
index 94e2adc5ab4433d9cc113f7a83d7c49aafd12787..7418d971dffc7b2ff26d9ab900e94d79bba2b75a 100644
--- a/pkg/service/git.go
+++ b/pkg/service/git.go
@@ -16,7 +16,6 @@
 type (
 	Repository struct {
 		Name           string
-		Title          string
 		Description    string
 		LastCommitDate string
 		Ref            string
@@ -48,8 +47,8 @@
 func (g *GitService) ListRepositories() ([]*Repository, error) {
 	rs := g.configRepo.List()
 
-	repos := make([]*Repository, len(rs))
-	for i, r := range rs {
+	repos := make([]*Repository, 0, len(rs))
+	for _, r := range rs {
 		repo, err := git.OpenRepository(r.Path)
 		if err != nil {
 			return nil, err
@@ -57,12 +56,14 @@ 		}
 
 		obj, err := repo.LastCommit()
 		if err != nil {
-			return nil, err
+			slog.Error("Error fetching last commit", "repository", r.Path, "error", err)
+			continue
 		}
 
 		head, err := repo.Head()
 		if err != nil {
-			return nil, err
+			slog.Error("Error fetching head", "repository", r.Path, "error", err)
+			continue
 		}
 
 		d := path.Join(r.Path, "description")
@@ -75,14 +76,12 @@ 				slog.Error("Error loading description file", "err", err)
 			}
 		}
 
-		baseName := path.Base(r.Path)
-		repos[i] = &Repository{
-			Name:           baseName,
-			Title:          baseName,
+		repos = append(repos, &Repository{
+			Name:           r.Name,
 			Description:    description,
 			LastCommitDate: obj.Author.When.Format(timeFormat),
 			Ref:            head.Name().Short(),
-		}
+		})
 	}
 
 	return repos, nil
diff --git a/pkg/u/list.go b/pkg/u/list.go
index cf71909e439d7726b4c6d9c6fcea771ad776a5bf..7271ef3783e892aad55ea49786515f491af30735 100644
--- a/pkg/u/list.go
+++ b/pkg/u/list.go
@@ -16,6 +16,14 @@ 	}
 	return v[0]
 }
 
+func Map[T any, V any](ts []T, fun func(T) V) []V {
+	rs := make([]V, len(ts))
+	for i := range ts {
+		rs[i] = fun(ts[i])
+	}
+	return rs
+}
+
 func ChunkBy[T any](items []T, chunkSize int) [][]T {
 	var chunks = make([][]T, 0, (len(items)/chunkSize)+1)
 	for chunkSize < len(items) {
diff --git a/pkg/u/list_test.go b/pkg/u/list_test.go
index 805a2091b59ce4c644f748b4b6c49a28aec50cdb..3a856b961818256ffdd0842824879169af645f7b 100644
--- a/pkg/u/list_test.go
+++ b/pkg/u/list_test.go
@@ -3,6 +3,7 @@
 package u
 
 import (
+	"strconv"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -129,3 +130,32 @@
 		})
 	}
 }
+
+func TestMap(t *testing.T) {
+	testCases := []struct {
+		name string
+		in   []int
+		out  []string
+	}{
+		{
+			name: "empty",
+			in:   []int{},
+			out:  []string{},
+		},
+		{
+			name: "not empty",
+			in:   []int{1, 2, 3},
+			out:  []string{"1", "2", "3"},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			out := Map(tc.in, func(v int) string { return strconv.Itoa(v) })
+
+			if diff := cmp.Diff(tc.out, out); diff != "" {
+				t.Errorf("Map error:\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/templates/base.qtpl b/templates/base.qtpl
index 9b0c4f57081d1861c77a7a75a4b25cafef57b6f4..ae9f7a6c50f072bfc14a2edf7c1af335f19e0a1b 100644
--- a/templates/base.qtpl
+++ b/templates/base.qtpl
@@ -42,7 +42,7 @@ <html lang="en">
     <head>
         <meta charset="utf-8">
         <link rel="icon" href="data:,">
-        <title>cerrado | {%= p.Title() %}</title> 
+        <title>{%= p.Title() %}</title> 
         <link rel="stylesheet" href="/static/main{%s Slug%}.css">
         <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
         <meta name="viewport" content="width=device-width, initial-scale=1" />
diff --git a/templates/base.qtpl.go b/templates/base.qtpl.go
index d2bcc7369a0587ee3edf2f641a333666d598e33e..bc40252cea03af8df732d1a1b29d11716260bca4 100644
--- a/templates/base.qtpl.go
+++ b/templates/base.qtpl.go
@@ -87,7 +87,7 @@ <html lang="en">
     <head>
         <meta charset="utf-8">
         <link rel="icon" href="data:,">
-        <title>cerrado | `)
+        <title>`)
 //line base.qtpl:45
 	p.StreamTitle(qw422016)
 //line base.qtpl:45