parent:
fad7f9b26c9d8c6063d141f15ef9368f60bf16df
nmyk <nick@nmyk.io>
2026-02-17T20:27:29-05:00
make server configurable; update readme
diff --git a/README.md b/README.md
index 22f9c05fe56b12f25efdfde1893429518d8712ab..9d379d48417fba5990e6c37673ad58c0a0a58593 100644
--- a/README.md
+++ b/README.md
@@ -3,4 +3,25 @@
Bidet is a simple web frontend for git.
It is hooked up to the plumbing, but it's all output, no input.
-Run bidet in the same directory as your bare repos to serve their contents over HTTP.
\ No newline at end of file
+## Usage
+
+```
+NAME
+ bidet
+
+FLAGS
+ -p, --port INT port for the server (default: 8080)
+ -n, --name STRING name for index page header
+ -g, --greeting STRING greeting for index page
+ -d, --directory STRING directory containing your bare git repositories (default: .)
+ -s, --css STRING path or URL to a CSS stylesheet
+```
+
+Bidet can also be configured with environment variables:
+```
+BIDET_PORT
+BIDET_HOSTNAME
+BIDET_GREETING
+BIDET_DIRECTORY
+BIDET_CSS
+```
\ No newline at end of file
diff --git a/cmd/bidet/main.go b/cmd/bidet/main.go
index a3da711b81909251c33d6d225ad55e41a0c92679..47399948cd7eb60bd4d9a5c697611cc24b5e3f38 100644
--- a/cmd/bidet/main.go
+++ b/cmd/bidet/main.go
@@ -1,15 +1,46 @@
package main
import (
+ "errors"
+ "fmt"
"log"
"net/http"
+ "os"
+ "github.com/peterbourgon/ff/v4"
+ "github.com/peterbourgon/ff/v4/ffhelp"
"nmyk.io/bidet/internal/handlers"
)
-const addr = ":8080"
+func main() {
+ fs := ff.NewFlagSet("bidet")
+ var (
+ port = fs.Int('p', "port", 8080, "port for the server")
+ hostname = fs.String('n', "name", "", "name for index page header")
+ greeting = fs.String('g', "greeting", "", "greeting for index page")
+ dir = fs.String('d', "directory", ".", "directory containing your bare git repositories")
+ cssLoc = fs.String('s', "css", "", "path or URL to a CSS stylesheet")
+ )
+
+ if err := ff.Parse(fs, os.Args[1:],
+ ff.WithEnvVarPrefix("BIDET"),
+ ); err != nil {
+ fmt.Println(ffhelp.Flags(fs))
+ if errors.Is(err, ff.ErrHelp) {
+ os.Exit(0)
+ }
+ os.Exit(1)
+ }
-func main() {
- log.Printf("Serving on %s\n", addr)
- log.Fatal(http.ListenAndServe(addr, handlers.Routes()))
+ cfg := handlers.Config{
+ Port: *port,
+ Name: *hostname,
+ Greeting: *greeting,
+ Dir: *dir,
+ CSSLocation: *cssLoc,
+ }
+ srv := handlers.NewServer(&cfg)
+ addr := fmt.Sprintf(":%d", srv.Port)
+ fmt.Printf("listening on port %d\n", *port)
+ log.Fatal(http.ListenAndServe(addr, srv.Routes()))
}
diff --git a/go.mod b/go.mod
index 45053c62ff6d84549a834f84ba40827a56d0be76..f38f6e4fe43c66a02b1096a7a9831d6a3d8a9b79 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,10 @@ module nmyk.io/bidet
go 1.25.0
-require github.com/go-git/go-git/v5 v5.16.5
+require (
+ github.com/go-git/go-git/v5 v5.16.5
+ github.com/peterbourgon/ff/v4 v4.0.0-beta.1
+)
require (
dario.cat/mergo v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 095dd6191aa7a2676c274595c40a7024bebbb0cd..4f9ab652ae88ccf6eed718fe2ccb646ca41037d1 100644
--- a/go.sum
+++ b/go.sum
@@ -47,6 +47,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
+github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
+github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/peterbourgon/ff/v4 v4.0.0-beta.1 h1:hV8qRu3V7YfiSMsBSfPfdcznAvPQd3jI5zDddSrDoUc=
+github.com/peterbourgon/ff/v4 v4.0.0-beta.1/go.mod h1:onQJUKipvCyFmZ1rIYwFAh1BhPOvftb1uhvSI7krNLc=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -97,6 +101,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/handlers/formatting.go b/internal/handlers/formatting.go
new file mode 100644
index 0000000000000000000000000000000000000000..37cc7e2f529e5310c33143f7ebcbb0f9e108274f
--- /dev/null
+++ b/internal/handlers/formatting.go
@@ -0,0 +1,41 @@
+package handlers
+
+import (
+ "path"
+ "slices"
+)
+
+func fmtAuthor(name string, email string) string {
+ return name + " <" + email + ">"
+}
+
+type Crumb struct {
+ Name string
+ URL string
+ IsLast bool
+}
+
+func breadcrumbs(repoName string, refLabel string, p string) []Crumb {
+ var crumbs []Crumb
+ last := true
+ cur := p
+ for {
+ base := path.Base(cur)
+ if base == "." || base == "/" {
+ break
+ }
+ crumbs = append(crumbs, Crumb{
+ Name: base,
+ URL: path.Join(repoName, "tree", refLabel, cur),
+ IsLast: last,
+ })
+ last = false
+ next := path.Dir(cur)
+ if next == cur {
+ break
+ }
+ cur = next
+ }
+ slices.Reverse(crumbs)
+ return crumbs
+}
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 30d441a267daf1c079f95ecef36cc6b954f48a71..1ec7c84bb91c28be9f6884733d2e2cd119dac9ee 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -17,16 +17,40 @@ "github.com/go-git/go-git/v5/plumbing/object"
"nmyk.io/bidet/internal/core"
)
-func Routes() *http.ServeMux {
+type Config struct {
+ Port int
+ Name string
+ Greeting string
+ Dir string
+ CSSLocation string
+}
+
+func NewServer(c *Config) Server {
+ css := getCSS(c.CSSLocation)
+ if css == nil {
+ css = defaultCSS
+ }
+ if c.Name == "" {
+ c.Name = "repositories"
+ }
+ return Server{Config: c, CSS: css}
+}
+
+type Server struct {
+ *Config
+ CSS []byte
+}
+
+func (s Server) Routes() *http.ServeMux {
mux := http.NewServeMux()
- mux.HandleFunc("GET /", ListRepos)
- mux.HandleFunc("GET /{name}", Repo)
- mux.HandleFunc("GET /{name}/refs", Refs)
- mux.HandleFunc("GET /{name}/refs/{type}", Refs)
- mux.HandleFunc("GET /{name}/tree/{path...}", Tree)
- mux.HandleFunc("GET /{name}/blob/{path...}", Blob)
- mux.HandleFunc("GET /{name}/commits/{path...}", Commits)
- mux.HandleFunc("GET /{name}/commit/{hash}", Commit)
+ mux.HandleFunc("GET /", s.ListRepos)
+ mux.HandleFunc("GET /{name}", s.Repo)
+ mux.HandleFunc("GET /{name}/refs", s.Refs)
+ mux.HandleFunc("GET /{name}/refs/{type}", s.Refs)
+ mux.HandleFunc("GET /{name}/tree/{path...}", s.Tree)
+ mux.HandleFunc("GET /{name}/blob/{path...}", s.Blob)
+ mux.HandleFunc("GET /{name}/commits/{path...}", s.Commits)
+ mux.HandleFunc("GET /{name}/commit/{hash}", s.Commit)
return mux
}
@@ -34,13 +58,13 @@ //go:embed templates/*.tmpl
var Templates embed.FS
//go:embed static/style.css
-var styleCSS []byte
+var defaultCSS []byte
-func Serve(w http.ResponseWriter, tmplName string, data any) {
+func (s Server) Serve(w http.ResponseWriter, tmplName string, data any) {
tmpl := template.Must(template.New("").
Funcs(template.FuncMap{
"css": func() template.CSS {
- return template.CSS(styleCSS)
+ return template.CSS(s.CSS)
},
}).
ParseFS(
@@ -49,24 +73,33 @@ "templates/base.tmpl",
"templates/"+tmplName+".tmpl",
),
)
-
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, tmplName, data); err != nil {
http.Error(w, err.Error(), 500)
return
}
-
w.WriteHeader(http.StatusOK)
buf.WriteTo(w)
}
-func ListRepos(w http.ResponseWriter, _ *http.Request) {
- repos, err := core.List(".")
+type IndexData struct {
+ Title string
+ Header string
+ Repos []string
+}
+
+func (s Server) ListRepos(w http.ResponseWriter, _ *http.Request) {
+ repos, err := core.List(s.Dir)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
- Serve(w, "repos", repos)
+ data := IndexData{
+ Title: s.Name,
+ Header: s.Greeting,
+ Repos: repos,
+ }
+ s.Serve(w, "repos", data)
}
type BranchMeta struct {
@@ -84,7 +117,7 @@ Branches []BranchMeta
Tags []TagMeta
}
-func Refs(w http.ResponseWriter, r *http.Request) {
+func (s Server) Refs(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
@@ -125,7 +158,7 @@ Type: refType,
Branches: branches,
Tags: tags,
}
- Serve(w, "refs", data)
+ s.Serve(w, "refs", data)
}
type CommitMeta struct {
@@ -141,11 +174,7 @@ Ref string
Commits []CommitMeta
}
-func fmtAuthor(name string, email string) string {
- return name + " <" + email + ">"
-}
-
-func Commits(w http.ResponseWriter, r *http.Request) {
+func (s Server) Commits(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
@@ -177,7 +206,7 @@ Repo: repoName,
Ref: commit.RefName,
Commits: commits,
}
- Serve(w, "commits", data)
+ s.Serve(w, "commits", data)
}
type CommitData struct {
@@ -187,7 +216,7 @@ Parents []string
Patch string
}
-func Commit(w http.ResponseWriter, r *http.Request) {
+func (s Server) Commit(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
@@ -232,7 +261,7 @@ Repo: repoName,
Parents: parents,
Patch: patch,
}
- Serve(w, "commit", data)
+ s.Serve(w, "commit", data)
}
type BlobData struct {
@@ -243,7 +272,7 @@ Crumbs []Crumb
Content string
}
-func Blob(w http.ResponseWriter, r *http.Request) {
+func (s Server) Blob(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
@@ -278,7 +307,7 @@ Path: filePath,
Crumbs: breadcrumbs(repoName, refLabel, filePath),
Content: content,
}
- Serve(w, "blob", data)
+ s.Serve(w, "blob", data)
}
type TreeData struct {
@@ -300,7 +329,7 @@ Path string
IsDir bool
}
-func Tree(w http.ResponseWriter, r *http.Request) {
+func (s Server) Tree(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
@@ -315,7 +344,7 @@ http.Error(w, "Cannot resolve HEAD state", 500)
}
headRef := head.Name().Short()
if commit.RefName == headRef && treePath == "" {
- Repo(w, r)
+ s.Repo(w, r)
return
}
rootTree, err := commit.Tree()
@@ -382,41 +411,10 @@ Entries: entries,
ReadmeName: readmeName,
Readme: readme,
}
- Serve(w, "tree", data)
-}
-
-type Crumb struct {
- Name string
- URL string
- IsLast bool
-}
-
-func breadcrumbs(repoName string, refLabel string, p string) []Crumb {
- var crumbs []Crumb
- last := true
- cur := p
- for {
- base := path.Base(cur)
- if base == "." || base == "/" {
- break
- }
- crumbs = append(crumbs, Crumb{
- Name: base,
- URL: path.Join(repoName, "tree", refLabel, cur),
- IsLast: last,
- })
- last = false
- next := path.Dir(cur)
- if next == cur {
- break
- }
- cur = next
- }
- slices.Reverse(crumbs)
- return crumbs
+ s.Serve(w, "tree", data)
}
-func Repo(w http.ResponseWriter, r *http.Request) {
+func (s Server) Repo(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
@@ -464,5 +462,5 @@ Entries: entries,
ReadmeName: readmeName,
Readme: readme,
}
- Serve(w, "repo", data)
+ s.Serve(w, "repo", data)
}
diff --git a/internal/handlers/style.go b/internal/handlers/style.go
new file mode 100644
index 0000000000000000000000000000000000000000..ad361c02d8ea709e658f976ef9ce738995870185
--- /dev/null
+++ b/internal/handlers/style.go
@@ -0,0 +1,33 @@
+package handlers
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+)
+
+func getCSS(loc string) []byte {
+ if loc == "" {
+ return nil
+ }
+ if u, err := url.Parse(loc); err == nil && (u.Scheme == "http" || u.Scheme == "https") {
+ if resp, err := http.Get(loc); err == nil {
+ defer resp.Body.Close()
+ if resp.StatusCode == http.StatusOK {
+ if data, err := io.ReadAll(resp.Body); err == nil {
+ return data
+ }
+ }
+ }
+ fmt.Printf("could not find stylesheet at %s; using default\n", loc)
+ return nil
+ }
+
+ if data, err := os.ReadFile(loc); err == nil {
+ return data
+ }
+ fmt.Printf("error reading stylesheet at %s; using default\n")
+ return nil
+}
diff --git a/internal/handlers/templates/repo.tmpl b/internal/handlers/templates/repo.tmpl
index c741b6262e9cfa1d6a57731c297413a2bec68df6..ff4eb3c663555ae23dbbef4f3e8e9cb9d1ca972b 100644
--- a/internal/handlers/templates/repo.tmpl
+++ b/internal/handlers/templates/repo.tmpl
@@ -7,7 +7,7 @@ <ul>
{{range .Entries}}
<li>
{{- if .IsDir -}}
- 📁 <a href="/{{$.Repo}}/tree/{{$.Ref}}/{{.Path}}">{{.Name}}</a>
+ 📂 <a href="/{{$.Repo}}/tree/{{$.Ref}}/{{.Path}}">{{.Name}}</a>
{{- else -}}
📄 <a href="/{{$.Repo}}/blob/{{$.Ref}}/{{.Path}}">{{.Name}}</a>
{{- end -}}
diff --git a/internal/handlers/templates/repos.tmpl b/internal/handlers/templates/repos.tmpl
index 107486974cb6551ed511399f6f03780b10f7065d..28fb5e11c198177f67558eea8af2563c37090e0b 100644
--- a/internal/handlers/templates/repos.tmpl
+++ b/internal/handlers/templates/repos.tmpl
@@ -1,8 +1,9 @@
-{{define "title"}}repositories{{end}}
+{{define "title"}}{{.Title}}{{end}}
{{define "content"}}
-<h1>repositories</h1>
+<h1>{{.Title}}</h1>
+<h4>{{.Header}}</h4>
<ul>
-{{range .}}
+{{range .Repos}}
<li><a href="/{{.}}">{{.}}</a></li>
{{end}}
</ul>
diff --git a/internal/handlers/templates/tree.tmpl b/internal/handlers/templates/tree.tmpl
index 8ae54adaba821181839da5f0a682f2caa5f9fc0c..cc7c430f07820c36decf10e3537641b907b5607a 100644
--- a/internal/handlers/templates/tree.tmpl
+++ b/internal/handlers/templates/tree.tmpl
@@ -20,7 +20,7 @@ <ul>
{{range .Entries}}
<li>
{{- if .IsDir -}}
- 📁 <a href="/{{$.Repo}}/tree/{{$.RefLabel}}/{{.Path}}">{{.Name}}</a>
+ 📂 <a href="/{{$.Repo}}/tree/{{$.RefLabel}}/{{.Path}}">{{.Name}}</a>
{{- else -}}
📄 <a href="/{{$.Repo}}/blob/{{$.RefLabel}}/{{.Path}}">{{.Name}}</a>
{{- end -}}