1diff --git a/.gitignore b/.gitignore
2new file mode 100644
3index 0000000000000000000000000000000000000000..e660fd93d3196215552065b1e63bf6a2f393ed86
4--- /dev/null
5+++ b/.gitignore
6@@ -0,0 +1 @@
7+bin/
8diff --git a/Makefile b/Makefile
9index 7a0e125baadd8ba25b560106a0b1bd91ec30df17..ecf165dce0342209fc2879c732b2eb8d7b06eba0 100644
10--- a/Makefile
11+++ b/Makefile
12@@ -3,3 +3,6 @@ go build -o bin/cerrado
13
14 run:
15 go run .
16+
17+test:
18+ go test -v --tags=unit ./...
19diff --git a/README.md b/README.md
20new file mode 100644
21index 0000000000000000000000000000000000000000..5a723e55161cbc997e4cbeec226fd7bd9555e258
22--- /dev/null
23+++ b/README.md
24@@ -0,0 +1,3 @@
25+# Cerrado
26+
27+Read only single user mail based forge for git.
28diff --git a/config.example.scfg b/config.example.scfg
29new file mode 100644
30index 0000000000000000000000000000000000000000..eda4f38d14d06608e6447089bbf13c5416092d65
31--- /dev/null
32+++ b/config.example.scfg
33@@ -0,0 +1,22 @@
34+scan /srv/git/ {
35+ public true
36+}
37+
38+# TBD
39+#user admin:iKlvHe1g0UoXE
40+#
41+#list main {
42+# server smtp.example.com
43+# user admin@admin.com
44+# password 1234567
45+# security tls
46+# authentication plain
47+# default false
48+#}
49+#
50+#repository cerrado {
51+# title Cerrado
52+# description "Self host single person readonly forge"
53+# list main
54+# public true
55+#}
56diff --git a/go.mod b/go.mod
57index cca006e89444dbdcf6c81c9f4f0f9acf6ed0bb6b..bfbe03ad8f3b8502d8d8f9ba9d3967552b1171e1 100644
58--- a/go.mod
59+++ b/go.mod
60@@ -2,4 +2,8 @@ module git.gabrielgio.me/cerrado
61
62 go 1.22.2
63
64-require golang.org/x/sync v0.7.0 // indirect
65+require (
66+ git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082
67+ github.com/google/go-cmp v0.6.0
68+ golang.org/x/sync v0.7.0
69+)
70diff --git a/go.sum b/go.sum
71index e8ef4a360a1437536e8a3053e4ffc82fc69ce9c5..a98204454c0ef47546d171a1dada140655d0b45e 100644
72--- a/go.sum
73+++ b/go.sum
74@@ -1,2 +1,8 @@
75+git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082 h1:9Udx5fm4vRtmgDIBjy2ef5QioHbzpw5oHabbhpAUyEw=
76+git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw=
77+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
78+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
79+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
80+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
81 golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
82 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
83diff --git a/main.go b/main.go
84index 7c80564f57dfa106d4a4074f6c4586bc0a9c18ba..ba441fe71b8c6c0ea4aff7a8805d2e4729059e27 100644
85--- a/main.go
86+++ b/main.go
87@@ -2,16 +2,20 @@ package main
88
89 import (
90 "context"
91+ "encoding/json"
92+ "flag"
93 "log/slog"
94 "net/http"
95 "os"
96 "os/signal"
97 "time"
98
99+ "git.gabrielgio.me/cerrado/pkg/config"
100 "git.gabrielgio.me/cerrado/pkg/worker"
101 )
102
103 func main() {
104+
105 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
106 defer stop()
107 if err := run(ctx); err != nil {
108@@ -20,10 +24,36 @@ }
109 }
110
111 func run(ctx context.Context) error {
112+ var (
113+ configPath = flag.String("config", "config.example.scfg", "File path for the configuration file")
114+ )
115+
116+ flag.Parse()
117+
118 mux := http.NewServeMux()
119 mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
120- if _, err := w.Write([]byte("Hello world!")); err != nil {
121+
122+ f, err := os.Open(*configPath)
123+ if err != nil {
124+ slog.Error("Error openning config file json", "error", err, "path", *configPath)
125+ return
126+ }
127+
128+ c, err := config.Parse(f)
129+ if err != nil {
130+ slog.Error("Error parsing config", "error", err, "path", *configPath)
131+ return
132+ }
133+
134+ b, err := json.MarshalIndent(c, "", " ")
135+ if err != nil {
136+ slog.Error("Error parsing json", "error", err)
137+ return
138+ }
139+
140+ if _, err := w.Write(b); err != nil {
141 slog.Error("Error handling index", "error", err)
142+ return
143 }
144 })
145 serverTask := worker.NewServerTask(&http.Server{Handler: mux, Addr: "0.0.0.0:8080"})
146diff --git a/pkg/config/config.go b/pkg/config/config.go
147new file mode 100644
148index 0000000000000000000000000000000000000000..ba1614f6fa318316db0b44a319846bf4a275bba6
149--- /dev/null
150+++ b/pkg/config/config.go
151@@ -0,0 +1,84 @@
152+package config
153+
154+import (
155+ "fmt"
156+ "io"
157+ "strconv"
158+
159+ "git.sr.ht/~emersion/go-scfg"
160+)
161+
162+type (
163+ Scan struct {
164+ Path string
165+ Public bool
166+ }
167+
168+ Configuration struct {
169+ Scan *Scan
170+ }
171+)
172+
173+func Parse(r io.Reader) (*Configuration, error) {
174+ block, err := scfg.Read(r)
175+ if err != nil {
176+ return nil, err
177+ }
178+
179+ config := defaultConfiguration()
180+
181+ err = setScan(block, config.Scan)
182+ if err != nil {
183+ return nil, err
184+ }
185+
186+ return config, nil
187+}
188+
189+func defaultConfiguration() *Configuration {
190+ return &Configuration{
191+ Scan: &Scan{
192+ Public: true,
193+ Path: "",
194+ },
195+ }
196+}
197+
198+func setScan(block scfg.Block, scan *Scan) error {
199+ scanDir := block.Get("scan")
200+ err := setString(scanDir, &scan.Path)
201+ if err != nil {
202+ return err
203+ }
204+
205+ public := scanDir.Children.Get("public")
206+ return setBool(public, &scan.Public)
207+}
208+
209+func setBool(dir *scfg.Directive, field *bool) error {
210+
211+ if dir != nil {
212+ p1 := first(dir.Params)
213+ v, err := strconv.ParseBool(p1)
214+ if err != nil {
215+ return fmt.Errorf("Error parsing bool param of %s: %w", dir.Name, err)
216+ }
217+ *field = v
218+ }
219+ return nil
220+}
221+
222+func setString(dir *scfg.Directive, field *string) error {
223+ if dir != nil {
224+ *field = first(dir.Params)
225+ }
226+ return nil
227+}
228+
229+func first[T any](v []T) T {
230+ if len(v) == 0 {
231+ var zero T
232+ return zero
233+ }
234+ return v[0]
235+}
236diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
237new file mode 100644
238index 0000000000000000000000000000000000000000..c8cd8876055e2cabe66e1ee574b5c36df358efcb
239--- /dev/null
240+++ b/pkg/config/config_test.go
241@@ -0,0 +1,56 @@
242+// go:build unit
243+package config
244+
245+import (
246+ "strings"
247+ "testing"
248+
249+ "github.com/google/go-cmp/cmp"
250+)
251+
252+func TestConfig(t *testing.T) {
253+ testCases := []struct {
254+ name string
255+ config string
256+ expectedConfig *Configuration
257+ }{
258+ {
259+ name: "minimal scan",
260+ config: `scan "/srv/git"`,
261+ expectedConfig: &Configuration{
262+ Scan: &Scan{
263+ Public: true,
264+ Path: "/srv/git",
265+ },
266+ },
267+ },
268+ {
269+ name: "complete scan",
270+ config: `scan "/srv/git" {
271+ public false
272+}`,
273+ expectedConfig: &Configuration{
274+ Scan: &Scan{
275+ Public: false,
276+ Path: "/srv/git",
277+ },
278+ },
279+ },
280+ }
281+
282+ for _, tc := range testCases {
283+ t.Run(tc.name, func(t *testing.T) {
284+ r := strings.NewReader(tc.config)
285+ config, err := Parse(r)
286+ if err != nil {
287+ t.Fatalf("Error parsing config %s", err.Error())
288+ }
289+
290+ if diff := cmp.Diff(tc.expectedConfig, config); diff != "" {
291+ t.Errorf("Wrong result given - wanted + got\n %s", diff)
292+ }
293+
294+ })
295+
296+ }
297+}