diff options
Diffstat (limited to 'pkg')
| -rw-r--r-- | pkg/config/config.go | 60 | ||||
| -rw-r--r-- | pkg/config/config_test.go | 38 | ||||
| -rw-r--r-- | pkg/ext/auth.go | 22 | ||||
| -rw-r--r-- | pkg/ext/compression.go | 31 | ||||
| -rw-r--r-- | pkg/ext/log.go | 4 | ||||
| -rw-r--r-- | pkg/ext/request.go | 14 | ||||
| -rw-r--r-- | pkg/ext/router.go | 24 | ||||
| -rw-r--r-- | pkg/git/git.go | 101 | ||||
| -rw-r--r-- | pkg/handler/about/handler.go | 3 | ||||
| -rw-r--r-- | pkg/handler/auth/login.go | 4 | ||||
| -rw-r--r-- | pkg/handler/git/handler.go | 69 | ||||
| -rw-r--r-- | pkg/handler/router.go | 18 | ||||
| -rw-r--r-- | pkg/handler/static/handler.go | 63 | ||||
| -rw-r--r-- | pkg/service/git.go | 29 |
14 files changed, 426 insertions, 54 deletions
diff --git a/pkg/config/config.go b/pkg/config/config.go index 9dbf449..ff969ec 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -39,6 +39,11 @@ type ( Public bool } + SyntaxHighlight struct { + Dark string + Light string + } + // configuration represents file configuration. // fields needs to be exported to cmp to work configuration struct { @@ -50,7 +55,8 @@ type ( Repositories []*GitRepositoryConfiguration RootReadme string Scans []*scan - SyntaxHighlight string + SyntaxHighlight SyntaxHighlight + Hostname string } // This is a per repository configuration. @@ -74,7 +80,8 @@ type ( removeSuffix bool repositories []*GitRepositoryConfiguration rootReadme string - syntaxHighlight string + syntaxHighlight SyntaxHighlight + hostname string } ) @@ -95,6 +102,7 @@ func LoadConfigurationRepository(configPath string) (*ConfigurationRepository, e passphrase: []byte(config.Passphrase), repositories: config.Repositories, rootReadme: config.RootReadme, + hostname: config.Hostname, syntaxHighlight: config.SyntaxHighlight, removeSuffix: config.RemoveSuffix, orderBy: parseOrderBy(config.OrderBy), @@ -117,12 +125,20 @@ func (c *ConfigurationRepository) GetRootReadme() string { return c.rootReadme } +func (c *ConfigurationRepository) GetHostname() string { + return c.hostname +} + func (c *ConfigurationRepository) GetOrderBy() OrderBy { return c.orderBy } func (c *ConfigurationRepository) GetSyntaxHighlight() string { - return c.syntaxHighlight + return c.syntaxHighlight.Light +} + +func (c *ConfigurationRepository) GetSyntaxHighlightDark() string { + return c.syntaxHighlight.Dark } func (c *ConfigurationRepository) GetListenAddr() string { @@ -219,6 +235,11 @@ func parse(r io.Reader) (*configuration, error) { return nil, err } + err = setHostname(block, &config.Hostname) + if err != nil { + return nil, err + } + err = setListenAddr(block, &config.ListenAddr) if err != nil { return nil, err @@ -311,11 +332,16 @@ func defaultConfiguration() *configuration { return &configuration{ Scans: defaultScans(), RootReadme: "", + Hostname: defaultHostname(), ListenAddr: defaultAddr(), Repositories: make([]*GitRepositoryConfiguration, 0), } } +func defaultHostname() string { + return "https://localhost:8080" +} + func defaultScans() []*scan { return []*scan{} } @@ -339,6 +365,11 @@ func setRootReadme(block scfg.Block, readme *string) error { return setString(scanDir, readme) } +func setHostname(block scfg.Block, hostname *string) error { + scanDir := block.Get("hostname") + return setString(scanDir, hostname) +} + func setPassphrase(block scfg.Block, listenAddr *string) error { scanDir := block.Get("passphrase") return setString(scanDir, listenAddr) @@ -349,9 +380,26 @@ func setAESKey(block scfg.Block, listenAddr *string) error { return setString(scanDir, listenAddr) } -func setSyntaxHighlight(block scfg.Block, listenAddr *string) error { - scanDir := block.Get("syntax-highlight") - return setString(scanDir, listenAddr) +func setSyntaxHighlight(block scfg.Block, sh *SyntaxHighlight) error { + shDir := block.Get("syntax-highlight") + if shDir == nil { + return nil + } + + themes := shDir.Params + if len(themes) > 2 || len(themes) == 0 { + return errors.New("syntax-highlight must contains at most two params and at least one, light then dark theme name") + } + + sh.Light = themes[0] + if len(themes) > 1 { + sh.Dark = themes[1] + } else { + // if dark is not set use light + sh.Dark = sh.Light + } + + return nil } func setOrderby(block scfg.Block, orderBy *string) error { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 949238e..50744b5 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -25,6 +25,7 @@ func TestFileParsing(t *testing.T) { }, }, ListenAddr: defaultAddr(), + Hostname: defaultHostname(), Repositories: []*GitRepositoryConfiguration{}, }, }, @@ -42,15 +43,32 @@ scan "/srv/git" { }, }, ListenAddr: defaultAddr(), + Hostname: defaultHostname(), Repositories: []*GitRepositoryConfiguration{}, }, }, { + name: "themes", + config: ` +syntax-highlight light dark`, + expectedConfig: &configuration{ + Scans: defaultScans(), + ListenAddr: defaultAddr(), + Hostname: defaultHostname(), + Repositories: []*GitRepositoryConfiguration{}, + SyntaxHighlight: SyntaxHighlight{ + Light: "light", + Dark: "dark", + }, + }, + }, + { name: "minimal repository", config: `repository /srv/git/cerrado.git`, expectedConfig: &configuration{ Scans: defaultScans(), ListenAddr: defaultAddr(), + Hostname: defaultHostname(), Repositories: []*GitRepositoryConfiguration{ { Name: "cerrado.git", @@ -74,6 +92,7 @@ repository /srv/git/cerrado.git { expectedConfig: &configuration{ Scans: defaultScans(), ListenAddr: defaultAddr(), + Hostname: defaultHostname(), Repositories: []*GitRepositoryConfiguration{ { Name: "cerrado", @@ -91,6 +110,7 @@ repository /srv/git/cerrado.git { expectedConfig: &configuration{ Scans: defaultScans(), ListenAddr: defaultAddr(), + Hostname: defaultHostname(), Repositories: []*GitRepositoryConfiguration{}, }, }, @@ -99,6 +119,7 @@ repository /srv/git/cerrado.git { config: `listen-addr unix://var/run/cerrado/cerrado.sock`, expectedConfig: &configuration{ Scans: defaultScans(), + Hostname: defaultHostname(), ListenAddr: "unix://var/run/cerrado/cerrado.sock", Repositories: []*GitRepositoryConfiguration{}, }, @@ -112,6 +133,7 @@ aes-key 8XHptZxSWCGs1m7QzztX5zNQ7D9NiQevVX0DaUTNMbDpRwFzoJiB0U7K6O/kqIt01jJVgzBU syntax-highlight monokailight order-by lastcommit-desc remove-suffix true +hostname https://domain.tld scan "/srv/git" { public true @@ -132,12 +154,16 @@ repository /srv/git/cerrado.git { Path: "/srv/git", }, }, - ListenAddr: "unix://var/run/cerrado/cerrado.sock", - Passphrase: "$2a$14$VnB/ZcB1DUDkMnosRA6Y7.dj8h5eroslDxTeXlLwfQX/x86mh6WAq", - AESKey: "8XHptZxSWCGs1m7QzztX5zNQ7D9NiQevVX0DaUTNMbDpRwFzoJiB0U7K6O/kqIt01jJVgzBUfiR8ES46ZLLb4w==", - SyntaxHighlight: "monokailight", - OrderBy: "lastcommit-desc", - RemoveSuffix: true, + ListenAddr: "unix://var/run/cerrado/cerrado.sock", + Passphrase: "$2a$14$VnB/ZcB1DUDkMnosRA6Y7.dj8h5eroslDxTeXlLwfQX/x86mh6WAq", + AESKey: "8XHptZxSWCGs1m7QzztX5zNQ7D9NiQevVX0DaUTNMbDpRwFzoJiB0U7K6O/kqIt01jJVgzBUfiR8ES46ZLLb4w==", + SyntaxHighlight: SyntaxHighlight{ + Light: "monokailight", + Dark: "monokailight", + }, + OrderBy: "lastcommit-desc", + RemoveSuffix: true, + Hostname: "https://domain.tld", Repositories: []*GitRepositoryConfiguration{ { Name: "linux.git", diff --git a/pkg/ext/auth.go b/pkg/ext/auth.go index 5c3070e..ef126ec 100644 --- a/pkg/ext/auth.go +++ b/pkg/ext/auth.go @@ -14,19 +14,20 @@ type authService interface { ValidateToken(token []byte) (bool, error) } -func DisableAuthentication(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func DisableAuthentication(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *Request) { ctx := r.Context() ctx = context.WithValue(ctx, "disableAuthentication", true) - next(w, r.WithContext(ctx)) + r.Request = r.WithContext(ctx) + next(w, r) } } func VerifyRespository( config *serverconfig.ConfigurationRepository, -) func(next http.HandlerFunc) http.HandlerFunc { - return func(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +) func(next HandlerFunc) HandlerFunc { + return func(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *Request) { name := r.PathValue("name") if name != "" { repo := config.GetByName(name) @@ -41,9 +42,9 @@ func VerifyRespository( } } -func Authenticate(auth authService) func(next http.HandlerFunc) http.HandlerFunc { - return func(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func Authenticate(auth authService) func(next HandlerFunc) HandlerFunc { + return func(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *Request) { cookie, err := r.Cookie("auth") if err != nil { if !errors.Is(err, http.ErrNoCookie) { @@ -70,9 +71,10 @@ func Authenticate(auth authService) func(next http.HandlerFunc) http.HandlerFunc ctx := r.Context() ctx = context.WithValue(ctx, "logged", valid) + r.Request = r.WithContext(ctx) slog.Info("Validated token", "valid?", valid) - next(w, r.WithContext(ctx)) + next(w, r) } } } diff --git a/pkg/ext/compression.go b/pkg/ext/compression.go index 6c7a219..d3a3df1 100644 --- a/pkg/ext/compression.go +++ b/pkg/ext/compression.go @@ -15,18 +15,37 @@ import ( "github.com/klauspost/compress/zstd" ) -var ( - errInvalidParam = errors.New("Invalid weighted param") -) +var errInvalidParam = errors.New("Invalid weighted param") type CompressionResponseWriter struct { innerWriter http.ResponseWriter compressWriter io.Writer } -func Compress(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func Compress(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *Request) { + // TODO: hand this better + if strings.HasSuffix(r.URL.Path, ".tar.gz") { + next(w, r) + return + } + + if accept, ok := r.Header["Accept-Encoding"]; ok { + if compress, algo := GetCompressionWriter(u.FirstOrZero(accept), w); algo != "" { + defer compress.Close() + w.Header().Add("Content-Encoding", algo) + w = &CompressionResponseWriter{ + innerWriter: w, + compressWriter: compress, + } + } + } + next(w, r) + } +} +func Decompress(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { // TODO: hand this better if strings.HasSuffix(r.URL.Path, ".tar.gz") { next(w, r) @@ -61,12 +80,12 @@ func GetCompressionWriter(header string, inner io.Writer) (io.WriteCloser, strin default: return nil, "" } - } func (c *CompressionResponseWriter) Header() http.Header { return c.innerWriter.Header() } + func (c *CompressionResponseWriter) Write(b []byte) (int, error) { return c.compressWriter.Write(b) } diff --git a/pkg/ext/log.go b/pkg/ext/log.go index 8e68134..e0ad89f 100644 --- a/pkg/ext/log.go +++ b/pkg/ext/log.go @@ -39,8 +39,8 @@ func wrap(w http.ResponseWriter) *statusWraper { } } -func Log(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func Log(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *Request) { t := time.Now() s := wrap(w) next(s, r) diff --git a/pkg/ext/request.go b/pkg/ext/request.go new file mode 100644 index 0000000..d1593b2 --- /dev/null +++ b/pkg/ext/request.go @@ -0,0 +1,14 @@ +package ext + +import ( + "io" + "net/http" +) + +type Request struct { + *http.Request +} + +func (r *Request) ReadBody() io.ReadCloser { + return r.Body +} diff --git a/pkg/ext/router.go b/pkg/ext/router.go index ce4c126..bbbffa1 100644 --- a/pkg/ext/router.go +++ b/pkg/ext/router.go @@ -16,8 +16,9 @@ type ( middlewares []Middleware router *http.ServeMux } - Middleware func(next http.HandlerFunc) http.HandlerFunc - ErrorRequestHandler func(w http.ResponseWriter, r *http.Request) error + HandlerFunc func(http.ResponseWriter, *Request) + Middleware func(next HandlerFunc) HandlerFunc + ErrorRequestHandler func(w http.ResponseWriter, r *Request) error ) func NewRouter() *Router { @@ -34,15 +35,15 @@ func (r *Router) AddMiddleware(middleware Middleware) { r.middlewares = append(r.middlewares, middleware) } -func wrapError(next ErrorRequestHandler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func wrapError(next ErrorRequestHandler) HandlerFunc { + return func(w http.ResponseWriter, r *Request) { if err := next(w, r); err != nil { if errors.Is(err, service.ErrRepositoryNotFound) || errors.Is(err, plumbing.ErrReferenceNotFound) { NotFound(w, r) } else { slog.Error("Internal Server Error", "error", err) - InternalServerError(r, w, err) + InternalServerError(w, r, err) } } } @@ -54,7 +55,7 @@ func (r *Router) run(next ErrorRequestHandler) http.HandlerFunc { for _, r := range r.middlewares { req = r(req) } - req(w, re) + req(w, &Request{Request: re}) } } @@ -62,19 +63,26 @@ func (r *Router) HandleFunc(path string, handler ErrorRequestHandler) { r.router.HandleFunc(path, r.run(handler)) } -func NotFound(w http.ResponseWriter, r *http.Request) { +func NotFound(w http.ResponseWriter, r *Request) { w.WriteHeader(http.StatusNotFound) templates.WritePageTemplate(w, &templates.ErrorPage{ Message: "Not Found", }, r.Context()) } +func BadRequest(w http.ResponseWriter, r *Request, msg string) { + w.WriteHeader(http.StatusBadRequest) + templates.WritePageTemplate(w, &templates.ErrorPage{ + Message: msg, + }, r.Context()) +} + func Redirect(w http.ResponseWriter, location string) { w.Header().Add("location", location) w.WriteHeader(http.StatusTemporaryRedirect) } -func InternalServerError(r *http.Request, w http.ResponseWriter, err error) { +func InternalServerError(w http.ResponseWriter, r *Request, err error) { w.WriteHeader(http.StatusInternalServerError) templates.WritePageTemplate(w, &templates.ErrorPage{ Message: fmt.Sprintf("Internal Server Error:\n%s", err.Error()), diff --git a/pkg/git/git.go b/pkg/git/git.go index 64c721a..83f3f93 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -3,10 +3,13 @@ package git import ( "archive/tar" "bytes" + "context" "errors" "fmt" "io" "io/fs" + "log/slog" + "os/exec" "path" "sort" "time" @@ -432,6 +435,68 @@ func (g *GitRepository) FileContent(path string) ([]byte, error) { return buf.Bytes(), nil } +func (g *GitRepository) WriteInfoRefs(ctx context.Context, w io.Writer) error { + cmd := exec.CommandContext( + ctx, + "git-upload-pack", + "--stateless-rpc", + "--advertise-refs", + ".", + ) + + cmd.Dir = g.path + cmd.Env = []string{ + // TODO: get this from header. + "GIT_PROTOCOL=version=2", + } + + var errBuff bytes.Buffer + cmd.Stderr = &errBuff + cmd.Stdout = w + + err := packLine(w, "# service=git-upload-pack\n") + if err != nil { + return err + } + + err = packFlush(w) + if err != nil { + return err + } + + err = cmd.Run() + if err != nil { + slog.Error("Error upload pack refs", "message", errBuff.String()) + return err + } + return nil +} + +func (g *GitRepository) WriteUploadPack(ctx context.Context, r io.Reader, w io.Writer) error { + cmd := exec.CommandContext( + ctx, + "git-upload-pack", + "--stateless-rpc", + ".", + ) + cmd.Dir = g.Path() + cmd.Env = []string{ + // TODO: get this from header. + "GIT_PROTOCOL=version=2", + } + var errBuff bytes.Buffer + cmd.Stderr = &errBuff + cmd.Stdout = w + cmd.Stdin = r + + if err := cmd.Run(); err != nil { + slog.ErrorContext(ctx, "Git upload pack failed", "error", err, "message", errBuff.String()) + return err + } + + return nil +} + func (g *GitRepository) WriteTar(w io.Writer, prefix string) error { tw := tar.NewWriter(w) defer tw.Close() @@ -613,3 +678,39 @@ func (self *tagList) Less(i, j int) bool { return dateI.After(dateJ) } + +func packLine(w io.Writer, s string) error { + _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s) + return err +} + +func packFlush(w io.Writer) error { + _, err := fmt.Fprint(w, "0000") + return err +} + +type debugReader struct { + r io.Reader +} + +func (d *debugReader) Read(p []byte) (n int, err error) { + r, err := d.r.Read(p) + if err != nil { + if errors.Is(io.EOF, err) { + fmt.Printf("READ: EOF\n") + } + return r, err + } + + fmt.Printf("READ: %s\n", p[:r]) + return r, nil +} + +type debugWriter struct { + w io.Writer +} + +func (d *debugWriter) Write(p []byte) (n int, err error) { + fmt.Printf("WRITE: %s\n", p) + return d.w.Write(p) +} diff --git a/pkg/handler/about/handler.go b/pkg/handler/about/handler.go index ee084cd..b3a1593 100644 --- a/pkg/handler/about/handler.go +++ b/pkg/handler/about/handler.go @@ -9,6 +9,7 @@ import ( "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" + "git.gabrielgio.me/cerrado/pkg/ext" "git.gabrielgio.me/cerrado/templates" ) @@ -26,7 +27,7 @@ func NewAboutHandler(configRepo configurationRepository) *AboutHandler { return &AboutHandler{configRepo.GetRootReadme()} } -func (g *AboutHandler) About(w http.ResponseWriter, r *http.Request) error { +func (g *AboutHandler) About(w http.ResponseWriter, r *ext.Request) error { f, err := os.Open(g.readmePath) if err != nil { return err diff --git a/pkg/handler/auth/login.go b/pkg/handler/auth/login.go index 89fd87b..9cc13cc 100644 --- a/pkg/handler/auth/login.go +++ b/pkg/handler/auth/login.go @@ -26,7 +26,7 @@ func NewLoginHandler(auth authService) *LoginHandler { } } -func (g *LoginHandler) Logout(w http.ResponseWriter, r *http.Request) error { +func (g *LoginHandler) Logout(w http.ResponseWriter, r *ext.Request) error { cookie := &http.Cookie{ Name: "auth", Value: "", @@ -44,7 +44,7 @@ func (g *LoginHandler) Logout(w http.ResponseWriter, r *http.Request) error { return nil } -func (g *LoginHandler) Login(w http.ResponseWriter, r *http.Request) error { +func (g *LoginHandler) Login(w http.ResponseWriter, r *ext.Request) error { referer := r.URL.Query().Get("referer") // if query value is empty tries to get from header diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go index a9be54c..d046d19 100644 --- a/pkg/handler/git/handler.go +++ b/pkg/handler/git/handler.go @@ -2,6 +2,7 @@ package git import ( "bytes" + "compress/gzip" "errors" "fmt" "io" @@ -37,6 +38,7 @@ type ( GetRootReadme() string GetSyntaxHighlight() string GetOrderBy() config.OrderBy + GetHostname() string } ) @@ -47,7 +49,7 @@ func NewGitHandler(gitService *service.GitService, confRepo configurationReposit } } -func (g *GitHandler) List(w http.ResponseWriter, r *http.Request) error { +func (g *GitHandler) List(w http.ResponseWriter, r *ext.Request) error { // this is the only handler that needs to handle authentication itself. // everything else relay on name path parameter logged := ext.IsLoggedIn(r.Context()) @@ -89,7 +91,7 @@ func (g *GitHandler) List(w http.ResponseWriter, r *http.Request) error { return nil } -func (g *GitHandler) Archive(w http.ResponseWriter, r *http.Request) error { +func (g *GitHandler) Archive(w http.ResponseWriter, r *ext.Request) error { ext.SetGZip(w) name := r.PathValue("name") file := r.PathValue("file") @@ -115,7 +117,51 @@ func (g *GitHandler) Archive(w http.ResponseWriter, r *http.Request) error { return nil } -func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) error { +func (g *GitHandler) Multiplex(w http.ResponseWriter, r *ext.Request) error { + path := r.PathValue("rest") + name := r.PathValue("name") + + if r.URL.RawQuery == "service=git-receive-pack" { + ext.BadRequest(w, r, "no pushing allowed") + return nil + } + + if path == "info/refs" && r.URL.RawQuery == "service=git-upload-pack" && r.Method == "GET" { + w.Header().Set("content-type", "application/x-git-upload-pack-advertisement") + + err := g.gitService.WriteInfoRefs(r.Context(), name, w) + if err != nil { + slog.Error("Error WriteInfoRefs", "error", err) + } + } else if path == "git-upload-pack" && r.Method == "POST" { + w.Header().Set("content-type", "application/x-git-upload-pack-result") + w.Header().Set("Connection", "Keep-Alive") + w.Header().Set("Transfer-Encoding", "chunked") + w.WriteHeader(http.StatusOK) + + reader := r.Body + + if r.Header.Get("Content-Encoding") == "gzip" { + var err error + reader, err = gzip.NewReader(r.Body) + if err != nil { + return err + } + defer reader.Close() + } + + err := g.gitService.WriteUploadPack(r.Context(), name, reader, w) + if err != nil { + slog.Error("Error WriteUploadPack", "error", err) + } + } else if r.Method == "GET" { + return g.Summary(w, r) + } + + return nil +} + +func (g *GitHandler) Summary(w http.ResponseWriter, r *ext.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref, err := g.gitService.GetHead(name) @@ -149,13 +195,14 @@ func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) error { Tags: tags, Branches: branches, Commits: commits, + Hostname: g.config.GetHostname(), }, } templates.WritePageTemplate(w, gitList, r.Context()) return nil } -func (g *GitHandler) About(w http.ResponseWriter, r *http.Request) error { +func (g *GitHandler) About(w http.ResponseWriter, r *ext.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref, err := g.gitService.GetHead(name) @@ -199,7 +246,7 @@ func (g *GitHandler) About(w http.ResponseWriter, r *http.Request) error { return nil } -func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) error { +func (g *GitHandler) Refs(w http.ResponseWriter, r *ext.Request) error { ext.SetHTML(w) name := r.PathValue("name") @@ -230,7 +277,7 @@ func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) error { return nil } -func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) error { +func (g *GitHandler) Tree(w http.ResponseWriter, r *ext.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") @@ -259,7 +306,7 @@ func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) error { return nil } -func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) error { +func (g *GitHandler) Blob(w http.ResponseWriter, r *ext.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") @@ -302,6 +349,7 @@ func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) error { formatter := html.New( html.WithLineNumbers(true), html.WithLinkableLineNumbers(true, "L"), + html.WithClasses(true), ) iterator, err := lexer.Tokenise(nil, string(file)) @@ -327,7 +375,7 @@ func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) error { return nil } -func (g *GitHandler) Log(w http.ResponseWriter, r *http.Request) error { +func (g *GitHandler) Log(w http.ResponseWriter, r *ext.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") @@ -350,7 +398,7 @@ func (g *GitHandler) Log(w http.ResponseWriter, r *http.Request) error { return nil } -func (g *GitHandler) Ref(w http.ResponseWriter, r *http.Request) error { +func (g *GitHandler) Ref(w http.ResponseWriter, r *ext.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") @@ -372,7 +420,7 @@ func (g *GitHandler) Ref(w http.ResponseWriter, r *http.Request) error { return nil } -func (g *GitHandler) Commit(w http.ResponseWriter, r *http.Request) error { +func (g *GitHandler) Commit(w http.ResponseWriter, r *ext.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") @@ -393,6 +441,7 @@ func (g *GitHandler) Commit(w http.ResponseWriter, r *http.Request) error { formatter := html.New( html.WithLineNumbers(true), html.WithLinkableLineNumbers(true, "L"), + html.WithClasses(true), ) iterator, err := lexer.Tokenise(nil, diff) diff --git a/pkg/handler/router.go b/pkg/handler/router.go index e461922..1fbc4e3 100644 --- a/pkg/handler/router.go +++ b/pkg/handler/router.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "net/http" serverconfig "git.gabrielgio.me/cerrado/pkg/config" @@ -10,6 +11,7 @@ import ( "git.gabrielgio.me/cerrado/pkg/handler/git" "git.gabrielgio.me/cerrado/pkg/handler/static" "git.gabrielgio.me/cerrado/pkg/service" + "git.gabrielgio.me/cerrado/templates" ) // Mount handler gets the requires service and repository to build the handlers @@ -31,6 +33,14 @@ func MountHandler( return nil, err } + cssStaticHandler, err := static.ServeStaticCSSHandler( + configRepo.GetSyntaxHighlight(), + configRepo.GetSyntaxHighlightDark(), + ) + if err != nil { + return nil, err + } + mux := ext.NewRouter() mux.AddMiddleware(ext.Compress) mux.AddMiddleware(ext.Log) @@ -45,8 +55,14 @@ func MountHandler( } mux.HandleFunc("/static/{file}", staticHandler) + // add slug and session so css file can be cached forever. + // Slug follow commit id, which is update every new version + // Session is update every time server restarts, this allows the css to be + // cached forever but refresh if the admin updates the server configuration. + mux.HandleFunc(fmt.Sprintf("/static/theme.%s%s.css", templates.Session, templates.Slug), cssStaticHandler) mux.HandleFunc("/{name}/about/{$}", gitHandler.About) - mux.HandleFunc("/{name}/", gitHandler.Summary) + mux.HandleFunc("/{name}", gitHandler.Multiplex) + mux.HandleFunc("/{name}/{rest...}", gitHandler.Multiplex) mux.HandleFunc("/{name}/refs/{$}", gitHandler.Refs) mux.HandleFunc("/{name}/tree/{ref}/{rest...}", gitHandler.Tree) mux.HandleFunc("/{name}/blob/{ref}/{rest...}", gitHandler.Blob) diff --git a/pkg/handler/static/handler.go b/pkg/handler/static/handler.go index 361f690..6cc884e 100644 --- a/pkg/handler/static/handler.go +++ b/pkg/handler/static/handler.go @@ -1,6 +1,9 @@ package static import ( + "bytes" + "fmt" + "io" "io/fs" "mime" "net/http" @@ -8,6 +11,9 @@ import ( "git.gabrielgio.me/cerrado/pkg/ext" "git.gabrielgio.me/cerrado/static" + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/styles" ) func ServeStaticHandler() (ext.ErrorRequestHandler, error) { @@ -16,7 +22,7 @@ func ServeStaticHandler() (ext.ErrorRequestHandler, error) { return nil, err } - return func(w http.ResponseWriter, r *http.Request) error { + return func(w http.ResponseWriter, r *ext.Request) error { var ( f = r.PathValue("file") e = filepath.Ext(f) @@ -24,7 +30,60 @@ func ServeStaticHandler() (ext.ErrorRequestHandler, error) { ) ext.SetMIME(w, m) w.Header().Add("Cache-Control", "max-age=31536000") - http.ServeFileFS(w, r, staticFs, f) + http.ServeFileFS(w, r.Request, staticFs, f) return nil }, nil } + +func ServeStaticCSSHandler(lightTheme, darkTheme string) (ext.ErrorRequestHandler, error) { + var ( + lightStyle = styles.Get(lightTheme) + darkStyle = styles.Get(darkTheme) + formatter = html.New( + html.WithCSSComments(false), + ) + ) + + return func(w http.ResponseWriter, r *ext.Request) error { + ext.SetMIME(w, "text/css") + w.Header().Add("Cache-Control", "max-age=31536000") + + // use buffer so this function can fail before writing to http.ResponseWriter + var buffer bytes.Buffer + + var style *chroma.Style + style = darkStyle + buffer.Write([]byte("[data-bs-theme=\"dark\"] {\n")) + err := formatter.WriteCSS(&ws{&buffer}, style) + if err != nil { + return err + } + buffer.Write([]byte("}\n")) + + style = lightStyle + buffer.Write([]byte("[data-bs-theme=\"light\"] {\n")) + err = formatter.WriteCSS(&ws{&buffer}, style) + if err != nil { + return err + } + buffer.Write([]byte("}")) + + _, err = io.Copy(w, &buffer) + if err != nil { + return err + } + + return nil + }, nil +} + +type ws struct { + inner io.Writer +} + +// This is very cursed, and rely on the fact that it writes every css rule at time. +// it adds & to the begging so it can be nested by the ServeStaticCSSHandler. +// This will allow the follow bootstrap data-bs-theme. +func (w *ws) Write(p []byte) (n int, err error) { + return fmt.Fprintf(w.inner, "& %s", string(p)) +} diff --git a/pkg/service/git.go b/pkg/service/git.go index 5410d7a..6aa5cd6 100644 --- a/pkg/service/git.go +++ b/pkg/service/git.go @@ -2,6 +2,7 @@ package service import ( "compress/gzip" + "context" "errors" "io" "log/slog" @@ -299,3 +300,31 @@ func (g *GitService) GetHead(name string) (*plumbing.Reference, error) { return repo.Head() } + +func (g *GitService) WriteInfoRefs(ctx context.Context, name string, w io.Writer) error { + r := g.configRepo.GetByName(name) + if r == nil { + return ErrRepositoryNotFound + } + + repo, err := git.OpenRepository(r.Path) + if err != nil { + return err + } + + return repo.WriteInfoRefs(ctx, w) +} + +func (g *GitService) WriteUploadPack(ctx context.Context, name string, re io.Reader, w io.Writer) error { + r := g.configRepo.GetByName(name) + if r == nil { + return ErrRepositoryNotFound + } + + repo, err := git.OpenRepository(r.Path) + if err != nil { + return err + } + + return repo.WriteUploadPack(ctx, re, w) +} |
