Polish: flash messages, error pages, CSRF, README
DDavid committed on 2026-06-28 01:50 commit f41aa56
README.md +56 −1
# gitgud
-WIP - a github clone. for a video.+A small, self-hosted GitHub clone written in Go. It serves real git repositories over
+HTTP (clone / push / pull) and provides a web UI for browsing code, opening issues, and
+reviewing and merging pull requests.
+
+## Features
+
+- User registration, login, and session auth
+- Public and private repositories (private repos are invisible to others — 404, not 403)
+- Real git over HTTP via `git http-backend` (clone, push, pull) with HTTP Basic auth
+- Code browsing: file tree, syntax-highlighted files, rendered README, branch switcher,
+ commit log, and commit diffs (powered by go-git)
+- Issues: per-repo numbering, comments, open/close, markdown bodies
+- Pull requests: compare two branches, view the diff and commits, comment, and merge
+- Flash messages, friendly 404/403/500 pages, and CSRF-protected forms
+
+## Run locally
+
+Requirements: **Go 1.25+** and the **`git`** binary on your `PATH`.
+
+```bash
+go run ./cmd/server # serves http://localhost:8080
+# data (SQLite db + bare repos) lands in ./data
+```
+
+Configuration is read from the environment (a local `.env` file is loaded automatically):
+
+| Variable | Example | Purpose |
+| -------------------- | ----------------- | --------------------------------------- |
+| `GITGUD_ADDR` | `:8080` | Listen address |
+| `GITGUD_DATA_DIR` | `./data` | Where the database and repos are stored |
+| `GITGUD_SESSION_KEY` | `a-random-secret` | Session secret (use a random value) |
+
+Then register a user, create a repository, and follow the on-screen clone instructions:
+
+```bash
+git clone http://localhost:8080/<you>/<repo>.git
+cd <repo>
+echo "# <repo>" > README.md
+git add README.md && git commit -m "first commit"
+git push -u origin main # prompts for your gitgud username/password
+```
+
+## How it works
+
+gitgud has two git surfaces — a smart-HTTP server (`git http-backend` via CGI) for
+clone/push/pull, and a go-git reader for the web browsing UI — sitting on top of a clean,
+layered architecture (domain / application / interface / infrastructure). See
+[docs/00-overview.md](docs/00-overview.md) for the full design, and the numbered files in
+[docs/](docs/) for how each milestone was built.
+
+## Known limitations (v1)
+
+- Git over HTTP only (no SSH)
+- Pull requests are between two branches of the **same** repo (no forks)
+- Single-node; SQLite storage; bare repos on the local filesystem
+- The clone URL shown in the UI is hardcoded to `http://localhost:8080`
cmd/server/main.go +11 −1
package main
import (
"log"
"net/http"
+ "time"
"gitgud/internal/app"
"gitgud/internal/infra/config"
"gitgud/internal/infra/git"
"gitgud/internal/infra/persistence/sqlite"
"gitgud/internal/infra/security"
"gitgud/internal/infra/session"
"gitgud/internal/interface/web"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
db, err := sqlite.Open(cfg.DBPath())
if err != nil {
log.Fatal(err)
}
defer db.Close()
hasher := security.NewBcryptHasher()
userRepo := sqlite.NewUserRepo(db)
userService := app.NewUserService(userRepo, hasher)
gitSvc := git.NewCLIGit(cfg.ReposDir())
gitReader := git.NewGoGitReader(cfg.ReposDir())
repoRepo := sqlite.NewRepoRepo(db)
repoService := app.NewRepoService(repoRepo, gitSvc)
browseService := app.NewBrowseService(repoRepo, gitReader)
issueRepo := sqlite.NewIssueRepo(db)
issueService := app.NewIssueService(issueRepo)
prRepo := sqlite.NewPRRepo(db)
pullService := app.NewPullService(prRepo, gitReader, gitSvc)
gitAccess := app.NewGitAccessService(repoRepo)
gitBackend, err := git.NewBackend(cfg.ReposDir())
if err != nil {
log.Fatal(err)
}
sm := session.NewSessionManager(db)
handlers := web.NewHandlers(userService, repoService, browseService, issueService, pullService, gitAccess, gitBackend, sm)
handler := web.NewRouter(cfg, handlers)
+ srv := &http.Server{
+ Addr: cfg.Addr,
+ Handler: handler,
+ ReadHeaderTimeout: 10 * time.Second,
+ ReadTimeout: 60 * time.Second,
+ WriteTimeout: 120 * time.Second,
+ IdleTimeout: 120 * time.Second,
+ }
+
log.Printf("listening on %s", cfg.Addr)
- log.Fatal(http.ListenAndServe(cfg.Addr, handler))
+ log.Fatal(srv.ListenAndServe())
}
go.mod +1 −0
module gitgud
go 1.25.0
require (
github.com/go-chi/chi v1.5.5
github.com/joho/godotenv v1.5.1
)
require (
github.com/mattn/go-sqlite3 v1.14.47
golang.org/x/crypto v0.53.0
)
require (
github.com/a-h/templ v0.3.1020
github.com/alecthomas/chroma/v2 v2.27.0
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de
github.com/alexedwards/scs/v2 v2.9.0
github.com/go-git/go-git/v5 v5.19.1
github.com/yuin/goldmark v1.8.2
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/justinas/nosurf v1.2.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.46.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
go.sum +2 −0
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de h1:c72K9HLu6K442et0j3BUL/9HEYaUJouLkkVANdmqTOo=
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/justinas/nosurf v1.2.0 h1:yMs1bSRrNiwXk4AS6n8vL2Ssgpb9CB25T/4xrixaK0s=
+github.com/justinas/nosurf v1.2.0/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
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/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=
internal/interface/web/auth_handler.go +1 −1
package web
import (
"errors"
"net/http"
"strings"
"gitgud/internal/domain"
"gitgud/internal/interface/web/templates"
)
func (h *Handlers) showRegister(w http.ResponseWriter, r *http.Request) {
render(w, r, http.StatusOK, templates.Register(currentUser(r.Context()), "", "", ""))
}
func (h *Handlers) doRegister(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
email := r.FormValue("email")
password := r.FormValue("password")
u, err := h.users.Register(r.Context(), username, email, password)
if err != nil {
status := http.StatusBadRequest
if errors.Is(err, domain.ErrConflict) {
status = http.StatusConflict
}
render(w, r, status, templates.Register(currentUser(r.Context()), username, email, userMessage(err)))
return
}
h.startSession(w, r, u)
}
func (h *Handlers) showLogin(w http.ResponseWriter, r *http.Request) {
render(w, r, http.StatusOK, templates.Login(currentUser(r.Context()), "", ""))
}
func (h *Handlers) doLogin(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
u, err := h.users.Authenticate(r.Context(), username, password)
if err != nil {
render(w, r, http.StatusUnauthorized, templates.Login(currentUser(r.Context()), username, "invalid credentials"))
return
}
h.startSession(w, r, u)
}
func (h *Handlers) doLogout(w http.ResponseWriter, r *http.Request) {
if err := h.sm.Destroy(r.Context()); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (h *Handlers) startSession(w http.ResponseWriter, r *http.Request, u *domain.User) {
h.sm.Put(r.Context(), sessionUserIDKey, u.ID)
h.sm.RenewToken(r.Context())
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func userMessage(err error) string {
switch {
case errors.Is(err, domain.ErrUnauthorized):
return "invalid credentials"
case errors.Is(err, domain.ErrConflict):
return "that username is already taken"
case errors.Is(err, domain.ErrValidation):
return strings.TrimSuffix(err.Error(), ": "+domain.ErrValidation.Error())
default:
return "something went wrong"
}
}
internal/interface/web/browse_handler.go +16 −11
package web
import (
"errors"
+ "log"
"net/http"
"strings"
"github.com/go-chi/chi"
"gitgud/internal/domain"
"gitgud/internal/interface/web/presenter"
"gitgud/internal/interface/web/templates"
)
func (h *Handlers) repoTreeRoot(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
h.renderTree(w, r, repo, "", "")
}
func (h *Handlers) repoTree(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
h.renderTree(w, r, repo, chi.URLParam(r, "ref"), chi.URLParam(r, "*"))
}
func (h *Handlers) renderTree(w http.ResponseWriter, r *http.Request, repo *domain.Repository, ref, path string) {
ctx := r.Context()
empty, err := h.browse.IsEmpty(ctx, repo)
if err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
if empty {
render(w, r, http.StatusOK, templates.RepoHome(currentUser(ctx), repo))
return
}
if ref == "" {
ref = repo.DefaultBranch
}
branches, err := h.browse.Branches(ctx, repo)
if err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
entries, err := h.browse.Tree(ctx, repo, ref, path)
if err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
readme := ""
for _, e := range entries {
if !e.IsDir && strings.EqualFold(e.Name, "README.md") {
if blob, err := h.browse.Blob(ctx, repo, ref, e.Path); err == nil && !blob.IsBinary {
readme = presenter.RenderMarkdown(blob.Content)
}
break
}
}
render(w, r, http.StatusOK, templates.BrowseTree(currentUser(ctx), repo, ref, path, branches, entries, readme))
}
func (h *Handlers) repoBlob(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
ref := chi.URLParam(r, "ref")
path := chi.URLParam(r, "*")
blob, err := h.browse.Blob(r.Context(), repo, ref, path)
if err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
if ref == "" {
ref = repo.DefaultBranch
}
highlighted := ""
if !blob.IsBinary {
highlighted = presenter.Highlight(string(blob.Content), blob.Path)
}
render(w, r, http.StatusOK, templates.BrowseBlob(currentUser(r.Context()), repo, ref, blob, highlighted))
}
func (h *Handlers) repoRaw(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
blob, err := h.browse.Blob(r.Context(), repo, chi.URLParam(r, "ref"), chi.URLParam(r, "*"))
if err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
ctype := http.DetectContentType(blob.Content)
if !blob.IsBinary {
ctype = "text/plain; charset=utf-8"
}
w.Header().Set("Content-Type", ctype)
w.Write(blob.Content)
}
func (h *Handlers) repoCommits(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
ref := chi.URLParam(r, "ref")
commits, err := h.browse.Log(r.Context(), repo, ref, 50, 0)
if err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
if ref == "" {
ref = repo.DefaultBranch
}
render(w, r, http.StatusOK, templates.Commits(currentUser(r.Context()), repo, ref, commits))
}
func (h *Handlers) repoCommit(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
commit, diffs, err := h.browse.CommitDiff(r.Context(), repo, chi.URLParam(r, "hash"))
if err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
render(w, r, http.StatusOK, templates.CommitDetail(currentUser(r.Context()), repo, commit, diffs))
}
func (h *Handlers) viewRepo(w http.ResponseWriter, r *http.Request) *domain.Repository {
repo, err := h.browse.Repo(r.Context(), chi.URLParam(r, "owner"), chi.URLParam(r, "repo"), currentUser(r.Context()))
if err != nil {
h.notFound(w, r)
return nil
}
return repo
}
-func (h *Handlers) gitError(w http.ResponseWriter, r *http.Request, err error) {
+func (h *Handlers) writeError(w http.ResponseWriter, r *http.Request, err error) {
+ user := currentUser(r.Context())
switch {
case errors.Is(err, domain.ErrNotFound):
- h.notFound(w, r)
+ render(w, r, http.StatusNotFound, templates.NotFound(user))
case errors.Is(err, domain.ErrPermission):
- http.Error(w, "forbidden", http.StatusForbidden)
+ render(w, r, http.StatusForbidden, templates.Forbidden(user))
+ case errors.Is(err, domain.ErrUnauthorized):
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
default:
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ log.Printf("server error: %v", err)
+ render(w, r, http.StatusInternalServerError, templates.ServerError(user))
}
}
internal/interface/web/issue_handler.go +8 −8
package web
import (
"errors"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi"
"gitgud/internal/app"
"gitgud/internal/domain"
"gitgud/internal/interface/web/templates"
)
func (h *Handlers) issuesList(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
state := r.URL.Query().Get("state")
var filter domain.IssueState
switch state {
case "closed":
filter = domain.IssueClosed
case "all":
filter = ""
default:
state = "open"
filter = domain.IssueOpen
}
issues, err := h.issues.List(r.Context(), repo.ID, filter)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
open, closed, err := h.issues.Counts(r.Context(), repo.ID)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
render(w, r, http.StatusOK, templates.IssuesList(currentUser(r.Context()), repo, state, issues, open, closed))
}
func (h *Handlers) newIssue(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
render(w, r, http.StatusOK, templates.IssueNew(currentUser(r.Context()), repo, "", "", ""))
}
func (h *Handlers) createIssue(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
title := r.FormValue("title")
body := r.FormValue("body")
issue, err := h.issues.Open(r.Context(), repo, currentUser(r.Context()), title, body)
if err != nil {
if errors.Is(err, domain.ErrValidation) {
msg := strings.TrimSuffix(err.Error(), ": "+domain.ErrValidation.Error())
render(w, r, http.StatusBadRequest, templates.IssueNew(currentUser(r.Context()), repo, title, body, msg))
return
}
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
http.Redirect(w, r, repoURL(repo, "/issues/"+strconv.Itoa(issue.Number)), http.StatusSeeOther)
}
func (h *Handlers) issueDetail(w http.ResponseWriter, r *http.Request) {
repo, issue := h.loadIssue(w, r)
if issue == nil {
return
}
comments, err := h.issues.Comments(r.Context(), issue.ID)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
canModify := app.CanModifyIssue(repo, issue, currentUser(r.Context()))
render(w, r, http.StatusOK, templates.IssueDetail(currentUser(r.Context()), repo, issue, comments, canModify))
}
func (h *Handlers) addIssueComment(w http.ResponseWriter, r *http.Request) {
repo, issue := h.loadIssue(w, r)
if issue == nil {
return
}
if err := h.issues.Comment(r.Context(), issue, currentUser(r.Context()), r.FormValue("body")); err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
http.Redirect(w, r, repoURL(repo, "/issues/"+strconv.Itoa(issue.Number)), http.StatusSeeOther)
}
func (h *Handlers) closeIssue(w http.ResponseWriter, r *http.Request) {
repo, issue := h.loadIssue(w, r)
if issue == nil {
return
}
if err := h.issues.Close(r.Context(), repo, issue, currentUser(r.Context())); err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
http.Redirect(w, r, repoURL(repo, "/issues/"+strconv.Itoa(issue.Number)), http.StatusSeeOther)
}
func (h *Handlers) reopenIssue(w http.ResponseWriter, r *http.Request) {
repo, issue := h.loadIssue(w, r)
if issue == nil {
return
}
if err := h.issues.Reopen(r.Context(), repo, issue, currentUser(r.Context())); err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
http.Redirect(w, r, repoURL(repo, "/issues/"+strconv.Itoa(issue.Number)), http.StatusSeeOther)
}
func (h *Handlers) loadIssue(w http.ResponseWriter, r *http.Request) (*domain.Repository, *domain.Issue) {
repo := h.viewRepo(w, r)
if repo == nil {
return nil, nil
}
number, err := strconv.Atoi(chi.URLParam(r, "number"))
if err != nil {
h.notFound(w, r)
return nil, nil
}
issue, _, err := h.issues.Get(r.Context(), repo.ID, number)
if err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return nil, nil
}
return repo, issue
}
func repoURL(repo *domain.Repository, sub string) string {
return "/" + repo.OwnerName + "/" + repo.Name + sub
}
internal/interface/web/middleware.go +30 −0
package web
import (
"context"
"net/http"
"strconv"
"github.com/alexedwards/scs/v2"
+ "github.com/justinas/nosurf"
"gitgud/internal/app"
"gitgud/internal/domain"
"gitgud/internal/infra/git"
+ "gitgud/internal/interface/web/templates"
)
const sessionUserIDKey = "user_id"
type ctxKey int
const userCtxKey ctxKey = iota
type Handlers struct {
users *app.UserService
repos *app.RepoService
browse *app.BrowseService
issues *app.IssueService
pulls *app.PullService
gitAccess *app.GitAccessService
gitBackend *git.Backend
sm *scs.SessionManager
}
func NewHandlers(users *app.UserService, repos *app.RepoService, browse *app.BrowseService, issues *app.IssueService, pulls *app.PullService, gitAccess *app.GitAccessService, gitBackend *git.Backend, sm *scs.SessionManager) *Handlers {
return &Handlers{users: users, repos: repos, browse: browse, issues: issues, pulls: pulls, gitAccess: gitAccess, gitBackend: gitBackend, sm: sm}
}
func (h *Handlers) withUser(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if id := h.sm.GetInt64(r.Context(), sessionUserIDKey); id != 0 {
if u, err := h.users.ByID(r.Context(), strconv.FormatInt(id, 10)); err == nil {
r = r.WithContext(context.WithValue(r.Context(), userCtxKey, u))
}
}
next.ServeHTTP(w, r)
})
}
func (h *Handlers) requireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if currentUser(r.Context()) == nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
})
}
func currentUser(ctx context.Context) *domain.User {
u, _ := ctx.Value(userCtxKey).(*domain.User)
return u
}
+
+func (h *Handlers) withFlash(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if msg := h.sm.PopString(r.Context(), "flash"); msg != "" {
+ r = r.WithContext(templates.WithFlash(r.Context(), msg))
+ }
+ next.ServeHTTP(w, r)
+ })
+}
+
+func (h *Handlers) flash(r *http.Request, msg string) {
+ h.sm.Put(r.Context(), "flash", msg)
+}
+
+func csrf(next http.Handler) http.Handler {
+ s := nosurf.New(injectCSRFToken(next))
+ s.ExemptRegexp(`/(git-upload-pack|git-receive-pack)$`)
+ s.SetFailureHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "CSRF token invalid", http.StatusBadRequest)
+ }))
+ return s
+}
+
+func injectCSRFToken(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ next.ServeHTTP(w, r.WithContext(templates.WithCSRF(r.Context(), nosurf.Token(r))))
+ })
+}
internal/interface/web/pull_handler.go +10 −9
package web
import (
"errors"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi"
"gitgud/internal/app"
"gitgud/internal/domain"
"gitgud/internal/interface/web/templates"
)
func (h *Handlers) pullsList(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
state := r.URL.Query().Get("state")
var filter domain.PRState
switch state {
case "merged":
filter = domain.PRMerged
case "closed":
filter = domain.PRClosed
case "all":
filter = ""
default:
state = "open"
filter = domain.PROpen
}
prs, err := h.pulls.List(r.Context(), repo.ID, filter)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
open, merged, closed, err := h.pulls.Counts(r.Context(), repo.ID)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
render(w, r, http.StatusOK, templates.PullsList(currentUser(r.Context()), repo, state, prs, open, merged, closed))
}
func (h *Handlers) comparePull(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
branches, err := h.browse.Branches(r.Context(), repo)
if err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
base := r.URL.Query().Get("base")
if base == "" {
base = repo.DefaultBranch
}
head := r.URL.Query().Get("head")
var cmp *domain.Comparison
errMsg := ""
if head != "" && head != base {
cmp, err = h.pulls.Compare(r.Context(), repo, base, head)
if err != nil {
errMsg = "could not compare those branches"
}
}
render(w, r, http.StatusOK, templates.Compare(currentUser(r.Context()), repo, base, head, branches, cmp, errMsg))
}
func (h *Handlers) createPull(w http.ResponseWriter, r *http.Request) {
repo := h.viewRepo(w, r)
if repo == nil {
return
}
title := r.FormValue("title")
body := r.FormValue("body")
base := r.FormValue("base")
head := r.FormValue("head")
pr, err := h.pulls.Open(r.Context(), repo, currentUser(r.Context()), title, body, base, head)
if err != nil {
if errors.Is(err, domain.ErrValidation) {
branches, _ := h.browse.Branches(r.Context(), repo)
cmp, _ := h.pulls.Compare(r.Context(), repo, base, head)
msg := strings.TrimSuffix(err.Error(), ": "+domain.ErrValidation.Error())
render(w, r, http.StatusBadRequest, templates.Compare(currentUser(r.Context()), repo, base, head, branches, cmp, msg))
return
}
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
http.Redirect(w, r, repoURL(repo, "/pulls/"+strconv.Itoa(pr.Number)), http.StatusSeeOther)
}
func (h *Handlers) pullDetail(w http.ResponseWriter, r *http.Request) {
repo, pr := h.loadPull(w, r)
if pr == nil {
return
}
h.renderPull(w, r, repo, pr, "", http.StatusOK)
}
func (h *Handlers) addPullComment(w http.ResponseWriter, r *http.Request) {
repo, pr := h.loadPull(w, r)
if pr == nil {
return
}
if err := h.pulls.Comment(r.Context(), pr, currentUser(r.Context()), r.FormValue("body")); err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
http.Redirect(w, r, repoURL(repo, "/pulls/"+strconv.Itoa(pr.Number)), http.StatusSeeOther)
}
func (h *Handlers) mergePull(w http.ResponseWriter, r *http.Request) {
repo, pr := h.loadPull(w, r)
if pr == nil {
return
}
err := h.pulls.Merge(r.Context(), repo, pr, currentUser(r.Context()))
if err == nil {
+ h.flash(r, "Pull request merged.")
http.Redirect(w, r, repoURL(repo, "/pulls/"+strconv.Itoa(pr.Number)), http.StatusSeeOther)
return
}
if errors.Is(err, domain.ErrConflict) {
h.renderPull(w, r, repo, pr, "Cannot merge automatically — the branches conflict.", http.StatusConflict)
return
}
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
}
func (h *Handlers) closePull(w http.ResponseWriter, r *http.Request) {
repo, pr := h.loadPull(w, r)
if pr == nil {
return
}
if err := h.pulls.Close(r.Context(), repo, pr, currentUser(r.Context())); err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return
}
http.Redirect(w, r, repoURL(repo, "/pulls/"+strconv.Itoa(pr.Number)), http.StatusSeeOther)
}
func (h *Handlers) renderPull(w http.ResponseWriter, r *http.Request, repo *domain.Repository, pr *domain.PullRequest, errMsg string, status int) {
cmp, _ := h.pulls.Compare(r.Context(), repo, pr.BaseBranch, pr.HeadBranch)
comments, err := h.pulls.Comments(r.Context(), pr.ID)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
canMerge := pr.State == domain.PROpen && app.CanMergePR(repo, currentUser(r.Context())) && cmp != nil && cmp.Mergeable
render(w, r, status, templates.PullDetail(currentUser(r.Context()), repo, pr, cmp, comments, canMerge, errMsg))
}
func (h *Handlers) loadPull(w http.ResponseWriter, r *http.Request) (*domain.Repository, *domain.PullRequest) {
repo := h.viewRepo(w, r)
if repo == nil {
return nil, nil
}
number, err := strconv.Atoi(chi.URLParam(r, "number"))
if err != nil {
h.notFound(w, r)
return nil, nil
}
pr, err := h.pulls.Get(r.Context(), repo.ID, number)
if err != nil {
- h.gitError(w, r, err)
+ h.writeError(w, r, err)
return nil, nil
}
return repo, pr
}
internal/interface/web/repo_handler.go +3 −2
package web
import (
"errors"
"net/http"
"strings"
"github.com/go-chi/chi"
"gitgud/internal/app"
"gitgud/internal/domain"
"gitgud/internal/interface/web/templates"
)
func (h *Handlers) dashboard(w http.ResponseWriter, r *http.Request) {
user := currentUser(r.Context())
if user == nil {
render(w, r, http.StatusOK, templates.Home(nil))
return
}
repos, err := h.repos.ListByOwner(r.Context(), user.ID)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
render(w, r, http.StatusOK, templates.Dashboard(user, repos))
}
func (h *Handlers) showNewRepo(w http.ResponseWriter, r *http.Request) {
render(w, r, http.StatusOK, templates.NewRepo(currentUser(r.Context()), "", "", false, ""))
}
func (h *Handlers) createRepo(w http.ResponseWriter, r *http.Request) {
user := currentUser(r.Context())
name := r.FormValue("name")
description := r.FormValue("description")
private := r.FormValue("private") != ""
repo, err := h.repos.CreateRepo(r.Context(), user, name, description, private)
if err != nil {
status := http.StatusBadRequest
msg := "could not create repository"
switch {
case errors.Is(err, domain.ErrConflict):
status = http.StatusConflict
msg = "a repository with that name already exists"
case errors.Is(err, domain.ErrValidation):
msg = strings.TrimSuffix(err.Error(), ": "+domain.ErrValidation.Error())
}
render(w, r, status, templates.NewRepo(user, name, description, private, msg))
return
}
+ h.flash(r, "Repository created.")
http.Redirect(w, r, "/"+repo.OwnerName+"/"+repo.Name, http.StatusSeeOther)
}
func (h *Handlers) profile(w http.ResponseWriter, r *http.Request) {
viewer := currentUser(r.Context())
ownerName := chi.URLParam(r, "owner")
owner, err := h.users.ByUsername(r.Context(), ownerName)
if err != nil {
h.notFound(w, r)
return
}
repos, err := h.repos.ListByOwner(r.Context(), owner.ID)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ h.writeError(w, r, err)
return
}
visible := repos[:0]
for _, repo := range repos {
if app.CanView(repo, viewer) == nil {
visible = append(visible, repo)
}
}
render(w, r, http.StatusOK, templates.Profile(viewer, owner.Username, visible))
}
func (h *Handlers) notFound(w http.ResponseWriter, r *http.Request) {
render(w, r, http.StatusNotFound, templates.NotFound(currentUser(r.Context())))
}
internal/interface/web/router.go +2 −0
package web
import (
"embed"
"net/http"
"github.com/a-h/templ"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"gitgud/internal/infra/config"
)
var staticFS embed.FS
func NewRouter(cfg config.Config, h *Handlers) http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger, middleware.Recoverer)
r.Use(h.sm.LoadAndSave)
+ r.Use(csrf)
+ r.Use(h.withFlash)
r.Use(h.withUser)
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
r.Handle("/static/*", http.StripPrefix("/static", http.FileServer((http.FS(staticFS)))))
r.Get("/register", h.showRegister)
r.Post("/register", h.doRegister)
r.Get("/login", h.showLogin)
r.Post("/login", h.doLogin)
r.Post("/logout", h.doLogout)
r.With(h.requireAuth).Get("/new", h.showNewRepo)
r.With(h.requireAuth).Post("/new", h.createRepo)
r.Get("/{owner}/{repo}/info/refs", h.gitHTTP)
r.Post("/{owner}/{repo}/git-upload-pack", h.gitHTTP)
r.Post("/{owner}/{repo}/git-receive-pack", h.gitHTTP)
r.Get("/", h.dashboard)
r.Get("/{owner}", h.profile)
r.Get("/{owner}/{repo}", h.repoTreeRoot)
r.Get("/{owner}/{repo}/tree/{ref}", h.repoTree)
r.Get("/{owner}/{repo}/tree/{ref}/*", h.repoTree)
r.Get("/{owner}/{repo}/blob/{ref}/*", h.repoBlob)
r.Get("/{owner}/{repo}/raw/{ref}/*", h.repoRaw)
r.Get("/{owner}/{repo}/commits/{ref}", h.repoCommits)
r.Get("/{owner}/{repo}/commit/{hash}", h.repoCommit)
r.Get("/{owner}/{repo}/issues", h.issuesList)
r.With(h.requireAuth).Get("/{owner}/{repo}/issues/new", h.newIssue)
r.With(h.requireAuth).Post("/{owner}/{repo}/issues", h.createIssue)
r.Get("/{owner}/{repo}/issues/{number}", h.issueDetail)
r.With(h.requireAuth).Post("/{owner}/{repo}/issues/{number}/comments", h.addIssueComment)
r.With(h.requireAuth).Post("/{owner}/{repo}/issues/{number}/close", h.closeIssue)
r.With(h.requireAuth).Post("/{owner}/{repo}/issues/{number}/reopen", h.reopenIssue)
r.Get("/{owner}/{repo}/pulls", h.pullsList)
r.Get("/{owner}/{repo}/compare", h.comparePull)
r.With(h.requireAuth).Get("/{owner}/{repo}/pulls/new", h.comparePull)
r.With(h.requireAuth).Post("/{owner}/{repo}/pulls", h.createPull)
r.Get("/{owner}/{repo}/pulls/{number}", h.pullDetail)
r.With(h.requireAuth).Post("/{owner}/{repo}/pulls/{number}/comments", h.addPullComment)
r.With(h.requireAuth).Post("/{owner}/{repo}/pulls/{number}/merge", h.mergePull)
r.With(h.requireAuth).Post("/{owner}/{repo}/pulls/{number}/close", h.closePull)
return r
}
func render(w http.ResponseWriter, r *http.Request, status int, c templ.Component) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
if err := c.Render(r.Context(), w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
internal/interface/web/templates/compare.templ +1 −0
package templates
import "gitgud/internal/domain"
templ Compare(user *domain.User, repo *domain.Repository, base, head string, branches []string, cmp *domain.Comparison, errMsg string) {
@Layout("Compare · "+repo.Name, user) {
<div class="max-w-3xl mx-auto">
@repoHeader(repo, "pulls")
<h2 class="text-lg mb-3">Compare changes</h2>
if errMsg != "" {
<p class="text-red-600 mb-3">{ errMsg }</p>
}
<form method="get" action={ templ.SafeURL(repoPath(repo, "/compare")) } class="flex items-center gap-2 mb-4 text-sm">
<span>base</span>
<select name="base" class="border px-2 py-1">
@branchOptions(branches, base)
</select>
<span>←</span>
<span>compare</span>
<select name="head" class="border px-2 py-1">
<option value="">select branch</option>
@branchOptions(branches, head)
</select>
<button class="border px-3 py-1" type="submit">Compare</button>
</form>
if cmp != nil {
<div class="mb-4 text-sm text-gray-600">{ itoa(len(cmp.Commits)) } commit(s), { itoa(len(cmp.Files)) } file(s) changed</div>
if user != nil && cmp.Mergeable {
<form method="post" action={ templ.SafeURL(repoPath(repo, "/pulls")) } class="flex flex-col gap-3 mb-6 border p-3">
+ @csrfField()
<input type="hidden" name="base" value={ base }/>
<input type="hidden" name="head" value={ head }/>
<input class="border px-2 py-1" type="text" name="title" placeholder="Title" value={ head }/>
<textarea class="border px-2 py-1" name="body" rows="5" placeholder="Description (markdown supported)"></textarea>
<div><button class="border px-3 py-1" type="submit">Create pull request</button></div>
</form>
}
@commitsList(repo, cmp.Commits)
@diffFiles(cmp.Files)
}
</div>
}
}
templ branchOptions(branches []string, selected string) {
for _, b := range branches {
if b == selected {
<option value={ b } selected>{ b }</option>
} else {
<option value={ b }>{ b }</option>
}
}
}
internal/interface/web/templates/compare_templ.go +28 −20
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.865
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "gitgud/internal/domain"
func Compare(user *domain.User, repo *domain.Repository, base, head string, branches []string, cmp *domain.Comparison, errMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-3xl mx-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = repoHeader(repo, "pulls").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"text-lg mb-3\">Compare changes</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errMsg != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<p class=\"text-red-600 mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 11, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<form method=\"get\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(repoPath(repo, "/compare"))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"flex items-center gap-2 mb-4 text-sm\"><span>base</span> <select name=\"base\" class=\"border px-2 py-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = branchOptions(branches, base).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</select> <span>←</span> <span>compare</span> <select name=\"head\" class=\"border px-2 py-1\"><option value=\"\">select branch</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = branchOptions(branches, head).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</select> <button class=\"border px-3 py-1\" type=\"submit\">Compare</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cmp != nil {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"mb-4 text-sm text-gray-600\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(cmp.Commits)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 27, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " commit(s), ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(cmp.Files)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 27, Col: 104}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " file(s) changed</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if user != nil && cmp.Mergeable {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<form method=\"post\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 templ.SafeURL = templ.SafeURL(repoPath(repo, "/pulls"))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"flex flex-col gap-3 mb-6 border p-3\"><input type=\"hidden\" name=\"base\" value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"flex flex-col gap-3 mb-6 border p-3\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = csrfField().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<input type=\"hidden\" name=\"base\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(base)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 30, Col: 51}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 31, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"> <input type=\"hidden\" name=\"head\" value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"> <input type=\"hidden\" name=\"head\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(head)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 31, Col: 51}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 32, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"> <input class=\"border px-2 py-1\" type=\"text\" name=\"title\" placeholder=\"Title\" value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"> <input class=\"border px-2 py-1\" type=\"text\" name=\"title\" placeholder=\"Title\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(head)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 32, Col: 95}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 33, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"> <textarea class=\"border px-2 py-1\" name=\"body\" rows=\"5\" placeholder=\"Description (markdown supported)\"></textarea><div><button class=\"border px-3 py-1\" type=\"submit\">Create pull request</button></div></form>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"> <textarea class=\"border px-2 py-1\" name=\"body\" rows=\"5\" placeholder=\"Description (markdown supported)\"></textarea><div><button class=\"border px-3 py-1\" type=\"submit\">Create pull request</button></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = commitsList(repo, cmp.Commits).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = diffFiles(cmp.Files).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout("Compare · "+repo.Name, user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func branchOptions(branches []string, selected string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var11 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
for _, b := range branches {
if b == selected {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<option value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(b)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 47, Col: 20}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 48, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" selected>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" selected>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(b)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 47, Col: 35}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 48, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</option>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<option value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(b)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 49, Col: 20}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 50, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(b)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 49, Col: 26}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/compare.templ`, Line: 50, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</option>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
internal/interface/web/templates/errors.templ +21 −0
+package templates
+
+import "gitgud/internal/domain"
+
+templ Forbidden(user *domain.User) {
+ @Layout("Forbidden · gitgud", user) {
+ <div class="max-w-2xl mx-auto">
+ <h1 class="text-2xl mb-2">403</h1>
+ <p>You don't have permission to do that.</p>
+ </div>
+ }
+}
+
+templ ServerError(user *domain.User) {
+ @Layout("Error · gitgud", user) {
+ <div class="max-w-2xl mx-auto">
+ <h1 class="text-2xl mb-2">500</h1>
+ <p>Something went wrong. Please try again.</p>
+ </div>
+ }
+}
internal/interface/web/templates/errors_templ.go +107 −0
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.865
+package templates
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import "gitgud/internal/domain"
+
+func Forbidden(user *domain.User) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-2xl mx-auto\"><h1 class=\"text-2xl mb-2\">403</h1><p>You don't have permission to do that.</p></div>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = Layout("Forbidden · gitgud", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func ServerError(user *domain.User) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var3 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var3 == nil {
+ templ_7745c5c3_Var3 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"max-w-2xl mx-auto\"><h1 class=\"text-2xl mb-2\">500</h1><p>Something went wrong. Please try again.</p></div>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = Layout("Error · gitgud", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
internal/interface/web/templates/helpers.go +26 −0
package templates
import (
+ "context"
"fmt"
"strconv"
"strings"
"time"
"gitgud/internal/domain"
"gitgud/internal/interface/web/presenter"
)
+
+type ctxKey int
+
+const (
+ flashKey ctxKey = iota
+ csrfKey
+)
+
+func WithFlash(ctx context.Context, msg string) context.Context {
+ return context.WithValue(ctx, flashKey, msg)
+}
+
+func flashOf(ctx context.Context) string {
+ s, _ := ctx.Value(flashKey).(string)
+ return s
+}
+
+func WithCSRF(ctx context.Context, token string) context.Context {
+ return context.WithValue(ctx, csrfKey, token)
+}
+
+func csrfToken(ctx context.Context) string {
+ s, _ := ctx.Value(csrfKey).(string)
+ return s
+}
func markdown(s string) string {
return presenter.RenderMarkdown([]byte(s))
}
func fmtTime(t time.Time) string {
return t.Format("2006-01-02 15:04")
}
func stateAction(state domain.IssueState) string {
if state == domain.IssueOpen {
return "/close"
}
return "/reopen"
}
func stateTabClass(current, want string) string {
if current == want {
return "font-semibold"
}
return "text-gray-600"
}
func cloneInstructions(repo *domain.Repository) string {
url := "http://localhost:8080/" + repo.OwnerName + "/" + repo.Name + ".git"
return fmt.Sprintf(`git clone %s
cd %s
echo "# %s" > README.md
git add README.md
git commit -m "first commit"
git push -u origin %s`, url, repo.Name, repo.Name, repo.DefaultBranch)
}
type Crumb struct {
Name string
Href string
}
func treeCrumbs(repo *domain.Repository, ref, p string) []Crumb {
base := "/" + repo.OwnerName + "/" + repo.Name + "/tree/" + ref
crumbs := []Crumb{{Name: repo.Name, Href: base}}
if p = strings.Trim(p, "/"); p != "" {
acc := base
for _, seg := range strings.Split(p, "/") {
acc += "/" + seg
crumbs = append(crumbs, Crumb{Name: seg, Href: acc})
}
}
return crumbs
}
func entryHref(repo *domain.Repository, ref string, e domain.TreeEntry) string {
kind := "blob"
if e.IsDir {
kind = "tree"
}
return "/" + repo.OwnerName + "/" + repo.Name + "/" + kind + "/" + ref + "/" + e.Path
}
func repoPath(repo *domain.Repository, sub string) string {
return "/" + repo.OwnerName + "/" + repo.Name + sub
}
func humanSize(n int64) string {
const unit = 1024
if n < unit {
return fmt.Sprintf("%d B", n)
}
div, exp := int64(unit), 0
for x := n / unit; x >= unit; x /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGTPE"[exp])
}
func itoa(n int) string {
return strconv.Itoa(n)
}
func firstLine(s string) string {
if i := strings.IndexByte(s, '\n'); i >= 0 {
return s[:i]
}
return s
}
func diffLineClass(line string) string {
switch {
case strings.HasPrefix(line, "+"):
return "bg-green-100"
case strings.HasPrefix(line, "-"):
return "bg-red-100"
default:
return ""
}
}
func diffLines(patch string) []string {
return strings.Split(strings.TrimRight(patch, "\n"), "\n")
}
internal/interface/web/templates/issue_detail.templ +2 −0
package templates
import "gitgud/internal/domain"
templ IssueDetail(user *domain.User, repo *domain.Repository, issue *domain.Issue, comments []*domain.IssueComment, canModify bool) {
@Layout(issue.Title+" · "+repo.Name, user) {
<div class="max-w-3xl mx-auto">
@repoHeader(repo, "issues")
<div class="flex items-center gap-2 mb-1">
<h2 class="text-xl">{ issue.Title } <span class="text-gray-400">#{ itoa(issue.Number) }</span></h2>
<span class="text-xs border px-2 py-0.5">{ string(issue.State) }</span>
</div>
<div class="text-xs text-gray-500 mb-4">opened by { issue.AuthorName } on { fmtTime(issue.CreatedAt) }</div>
@commentCard(issue.AuthorName, fmtTime(issue.CreatedAt), issue.Body)
for _, c := range comments {
@commentCard(c.AuthorName, fmtTime(c.CreatedAt), c.Body)
}
if canModify {
<form method="post" action={ templ.SafeURL(repoPath(repo, "/issues/"+itoa(issue.Number)+stateAction(issue.State))) } class="mb-4">
+ @csrfField()
if issue.State == domain.IssueOpen {
<button class="border px-3 py-1 text-sm" type="submit">Close issue</button>
} else {
<button class="border px-3 py-1 text-sm" type="submit">Reopen issue</button>
}
</form>
}
if user != nil {
<form method="post" action={ templ.SafeURL(repoPath(repo, "/issues/"+itoa(issue.Number)+"/comments")) } class="flex flex-col gap-2">
+ @csrfField()
<textarea class="border px-2 py-1" name="body" rows="4" placeholder="Leave a comment (markdown supported)"></textarea>
<div>
<button class="border px-3 py-1" type="submit">Comment</button>
</div>
</form>
} else {
<p class="text-sm text-gray-500"><a href="/login">Sign in</a> to comment.</p>
}
</div>
}
}
templ commentCard(author, when, body string) {
<div class="border mb-3">
<div class="bg-gray-100 px-3 py-1 text-xs text-gray-600">{ author } commented on { when }</div>
<div class="px-3 py-2">
@templ.Raw(markdown(body))
</div>
</div>
}
internal/interface/web/templates/issue_detail_templ.go +21 −9
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.865
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "gitgud/internal/domain"
func IssueDetail(user *domain.User, repo *domain.Repository, issue *domain.Issue, comments []*domain.IssueComment, canModify bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-3xl mx-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = repoHeader(repo, "issues").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex items-center gap-2 mb-1\"><h2 class=\"text-xl\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_detail.templ`, Line: 10, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " <span class=\"text-gray-400\">#")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(issue.Number))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_detail.templ`, Line: 10, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span></h2><span class=\"text-xs border px-2 py-0.5\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(string(issue.State))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_detail.templ`, Line: 11, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span></div><div class=\"text-xs text-gray-500 mb-4\">opened by ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(issue.AuthorName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_detail.templ`, Line: 13, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " on ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmtTime(issue.CreatedAt))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_detail.templ`, Line: 13, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = commentCard(issue.AuthorName, fmtTime(issue.CreatedAt), issue.Body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, c := range comments {
templ_7745c5c3_Err = commentCard(c.AuthorName, fmtTime(c.CreatedAt), c.Body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if canModify {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<form method=\"post\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL = templ.SafeURL(repoPath(repo, "/issues/"+itoa(issue.Number)+stateAction(issue.State)))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var8)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" class=\"mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+ templ_7745c5c3_Err = csrfField().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
if issue.State == domain.IssueOpen {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<button class=\"border px-3 py-1 text-sm\" type=\"submit\">Close issue</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<button class=\"border px-3 py-1 text-sm\" type=\"submit\">Reopen issue</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if user != nil {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<form method=\"post\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 templ.SafeURL = templ.SafeURL(repoPath(repo, "/issues/"+itoa(issue.Number)+"/comments"))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var9)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"flex flex-col gap-2\"><textarea class=\"border px-2 py-1\" name=\"body\" rows=\"4\" placeholder=\"Leave a comment (markdown supported)\"></textarea><div><button class=\"border px-3 py-1\" type=\"submit\">Comment</button></div></form>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"flex flex-col gap-2\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = csrfField().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<textarea class=\"border px-2 py-1\" name=\"body\" rows=\"4\" placeholder=\"Leave a comment (markdown supported)\"></textarea><div><button class=\"border px-3 py-1\" type=\"submit\">Comment</button></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<p class=\"text-sm text-gray-500\"><a href=\"/login\">Sign in</a> to comment.</p>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<p class=\"text-sm text-gray-500\"><a href=\"/login\">Sign in</a> to comment.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout(issue.Title+" · "+repo.Name, user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func commentCard(author, when, body string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"border mb-3\"><div class=\"bg-gray-100 px-3 py-1 text-xs text-gray-600\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"border mb-3\"><div class=\"bg-gray-100 px-3 py-1 text-xs text-gray-600\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(author)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_detail.templ`, Line: 43, Col: 67}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_detail.templ`, Line: 45, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " commented on ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " commented on ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(when)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_detail.templ`, Line: 43, Col: 89}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_detail.templ`, Line: 45, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div><div class=\"px-3 py-2\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div><div class=\"px-3 py-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(markdown(body)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
internal/interface/web/templates/issue_new.templ +1 −0
package templates
import "gitgud/internal/domain"
templ IssueNew(user *domain.User, repo *domain.Repository, title, body, errMsg string) {
@Layout("New issue · "+repo.Name, user) {
<div class="max-w-3xl mx-auto">
@repoHeader(repo, "issues")
<h2 class="text-lg mb-3">New issue</h2>
if errMsg != "" {
<p class="text-red-600 mb-3">{ errMsg }</p>
}
<form method="post" action={ templ.SafeURL(repoPath(repo, "/issues")) } class="flex flex-col gap-3">
+ @csrfField()
<input class="border px-2 py-1" type="text" name="title" placeholder="Title" value={ title } autofocus/>
<textarea class="border px-2 py-1" name="body" rows="8" placeholder="Leave a description (markdown supported)">{ body }</textarea>
<div>
<button class="border px-3 py-1" type="submit">Create issue</button>
</div>
</form>
</div>
}
}
internal/interface/web/templates/issue_new_templ.go +13 −5
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.865
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "gitgud/internal/domain"
func IssueNew(user *domain.User, repo *domain.Repository, title, body, errMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-3xl mx-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = repoHeader(repo, "issues").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"text-lg mb-3\">New issue</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errMsg != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<p class=\"text-red-600 mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_new.templ`, Line: 11, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<form method=\"post\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(repoPath(repo, "/issues"))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"flex flex-col gap-3\"><input class=\"border px-2 py-1\" type=\"text\" name=\"title\" placeholder=\"Title\" value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"flex flex-col gap-3\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = csrfField().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<input class=\"border px-2 py-1\" type=\"text\" name=\"title\" placeholder=\"Title\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_new.templ`, Line: 14, Col: 94}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_new.templ`, Line: 15, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" autofocus> <textarea class=\"border px-2 py-1\" name=\"body\" rows=\"8\" placeholder=\"Leave a description (markdown supported)\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" autofocus> <textarea class=\"border px-2 py-1\" name=\"body\" rows=\"8\" placeholder=\"Leave a description (markdown supported)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(body)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_new.templ`, Line: 15, Col: 121}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/issue_new.templ`, Line: 16, Col: 121}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</textarea><div><button class=\"border px-3 py-1\" type=\"submit\">Create issue</button></div></form></div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</textarea><div><button class=\"border px-3 py-1\" type=\"submit\">Create issue</button></div></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout("New issue · "+repo.Name, user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
internal/interface/web/templates/layout.templ +11 −2
package templates
import "gitgud/internal/domain"
templ Layout(title string, user *domain.User) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title }</title>
<link rel="stylesheet" href="/static/css/main.css"/>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body>
<nav class="flex items-center justify-between px-6 py-3 border-b">
<a href="/" class="font-bold">gitgud</a>
<div class="flex items-center gap-4">
if user != nil {
+ <a href="/new">New</a>
<span>{ user.Username }</span>
<form method="post" action="/logout">
- <button type="submit">Log out</button>
+ @csrfField()
+ <button type="submit">Sign out</button>
</form>
} else {
- <a href="/login">Log in</a>
+ <a href="/login">Sign in</a>
<a href="/register">Register</a>
}
</div>
</nav>
+ if flashOf(ctx) != "" {
+ <div class="px-6 py-2 bg-blue-50 border-b border-blue-200 text-blue-800 text-sm">{ flashOf(ctx) }</div>
+ }
<main class="p-6">
{ children... }
</main>
</body>
</html>
}
+
+templ csrfField() {
+ <input type="hidden" name="csrf_token" value={ csrfToken(ctx) }/>
+}
internal/interface/web/templates/layout_templ.go +79 −6
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.865
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "gitgud/internal/domain"
func Layout(title string, user *domain.User) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/layout.templ`, Line: 11, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/static/css/main.css\"><script src=\"https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4\"></script></head><body><nav class=\"flex items-center justify-between px-6 py-3 border-b\"><a href=\"/\" class=\"font-bold\">gitgud</a><div class=\"flex items-center gap-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if user != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<span>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"/new\">New</a> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/layout.templ`, Line: 20, Col: 27}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/layout.templ`, Line: 21, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span><form method=\"post\" action=\"/logout\"><button type=\"submit\">Log out</button></form>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span><form method=\"post\" action=\"/logout\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = csrfField().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button type=\"submit\">Sign out</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"/login\">Log in</a> <a href=\"/register\">Register</a>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"/login\">Sign in</a> <a href=\"/register\">Register</a>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></nav>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if flashOf(ctx) != "" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"px-6 py-2 bg-blue-50 border-b border-blue-200 text-blue-800 text-sm\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(flashOf(ctx))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/layout.templ`, Line: 33, Col: 99}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></nav><main class=\"p-6\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<main class=\"p-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</main></body></html>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</main></body></html>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func csrfField() templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var5 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var5 == nil {
+ templ_7745c5c3_Var5 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"hidden\" name=\"csrf_token\" value=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken(ctx))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/layout.templ`, Line: 43, Col: 62}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
internal/interface/web/templates/login.templ +1 −0
package templates
import "gitgud/internal/domain"
templ Login(user *domain.User, username, errMsg string) {
@Layout("Log in · gitgud", user) {
<div class="max-w-sm mx-auto">
<h1 class="text-2xl mb-4">Log in</h1>
if errMsg != "" {
<p class="text-red-600 mb-3">{ errMsg }</p>
}
<form method="post" action="/login" class="flex flex-col gap-3">
+ @csrfField()
<label class="flex flex-col gap-1">
<span>Username</span>
<input class="border px-2 py-1" type="text" name="username" value={ username } autofocus/>
</label>
<label class="flex flex-col gap-1">
<span>Password</span>
<input class="border px-2 py-1" type="password" name="password"/>
</label>
<button class="border px-3 py-1" type="submit">Log in</button>
</form>
<p class="mt-3 text-sm">No account? <a href="/register">Register</a></p>
</div>
}
}
internal/interface/web/templates/login_templ.go +11 −3
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.865
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "gitgud/internal/domain"
func Login(user *domain.User, username, errMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-sm mx-auto\"><h1 class=\"text-2xl mb-4\">Log in</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errMsg != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"text-red-600 mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/login.templ`, Line: 10, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"post\" action=\"/login\" class=\"flex flex-col gap-3\"><label class=\"flex flex-col gap-1\"><span>Username</span> <input class=\"border px-2 py-1\" type=\"text\" name=\"username\" value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"post\" action=\"/login\" class=\"flex flex-col gap-3\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = csrfField().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<label class=\"flex flex-col gap-1\"><span>Username</span> <input class=\"border px-2 py-1\" type=\"text\" name=\"username\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(username)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/login.templ`, Line: 15, Col: 81}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/login.templ`, Line: 16, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" autofocus></label> <label class=\"flex flex-col gap-1\"><span>Password</span> <input class=\"border px-2 py-1\" type=\"password\" name=\"password\"></label> <button class=\"border px-3 py-1\" type=\"submit\">Log in</button></form><p class=\"mt-3 text-sm\">No account? <a href=\"/register\">Register</a></p></div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" autofocus></label> <label class=\"flex flex-col gap-1\"><span>Password</span> <input class=\"border px-2 py-1\" type=\"password\" name=\"password\"></label> <button class=\"border px-3 py-1\" type=\"submit\">Log in</button></form><p class=\"mt-3 text-sm\">No account? <a href=\"/register\">Register</a></p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout("Log in · gitgud", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
internal/interface/web/templates/new_repo.templ +1 −0
package templates
import "gitgud/internal/domain"
templ NewRepo(user *domain.User, name, description string, private bool, errMsg string) {
@Layout("New repository · gitgud", user) {
<div class="max-w-md mx-auto">
<h1 class="text-2xl mb-4">New repository</h1>
if errMsg != "" {
<p class="text-red-600 mb-3">{ errMsg }</p>
}
<form method="post" action="/new" class="flex flex-col gap-3">
+ @csrfField()
<label class="flex flex-col gap-1">
<span>Name</span>
<input class="border px-2 py-1" type="text" name="name" value={ name } autofocus/>
</label>
<label class="flex flex-col gap-1">
<span>Description</span>
<input class="border px-2 py-1" type="text" name="description" value={ description }/>
</label>
<label class="flex items-center gap-2">
if private {
<input type="checkbox" name="private" checked/>
} else {
<input type="checkbox" name="private"/>
}
<span>Private</span>
</label>
<button class="border px-3 py-1" type="submit">Create repository</button>
</form>
</div>
}
}
internal/interface/web/templates/new_repo_templ.go +16 −8
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.865
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "gitgud/internal/domain"
func NewRepo(user *domain.User, name, description string, private bool, errMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-md mx-auto\"><h1 class=\"text-2xl mb-4\">New repository</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errMsg != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"text-red-600 mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/new_repo.templ`, Line: 10, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"post\" action=\"/new\" class=\"flex flex-col gap-3\"><label class=\"flex flex-col gap-1\"><span>Name</span> <input class=\"border px-2 py-1\" type=\"text\" name=\"name\" value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"post\" action=\"/new\" class=\"flex flex-col gap-3\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = csrfField().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<label class=\"flex flex-col gap-1\"><span>Name</span> <input class=\"border px-2 py-1\" type=\"text\" name=\"name\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/new_repo.templ`, Line: 15, Col: 73}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/new_repo.templ`, Line: 16, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" autofocus></label> <label class=\"flex flex-col gap-1\"><span>Description</span> <input class=\"border px-2 py-1\" type=\"text\" name=\"description\" value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" autofocus></label> <label class=\"flex flex-col gap-1\"><span>Description</span> <input class=\"border px-2 py-1\" type=\"text\" name=\"description\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(description)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/new_repo.templ`, Line: 19, Col: 87}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/new_repo.templ`, Line: 20, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></label> <label class=\"flex items-center gap-2\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"></label> <label class=\"flex items-center gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if private {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<input type=\"checkbox\" name=\"private\" checked> ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"checkbox\" name=\"private\" checked> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"checkbox\" name=\"private\"> ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<input type=\"checkbox\" name=\"private\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span>Private</span></label> <button class=\"border px-3 py-1\" type=\"submit\">Create repository</button></form></div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span>Private</span></label> <button class=\"border px-3 py-1\" type=\"submit\">Create repository</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout("New repository · gitgud", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
internal/interface/web/templates/pull_detail.templ +2 −0
package templates
import "gitgud/internal/domain"
templ PullDetail(user *domain.User, repo *domain.Repository, pr *domain.PullRequest, cmp *domain.Comparison, comments []*domain.PRComment, canMerge bool, errMsg string) {
@Layout(pr.Title+" · "+repo.Name, user) {
<div class="max-w-3xl mx-auto">
@repoHeader(repo, "pulls")
<div class="flex items-center gap-2 mb-1">
<h2 class="text-xl">{ pr.Title } <span class="text-gray-400">#{ itoa(pr.Number) }</span></h2>
<span class="text-xs border px-2 py-0.5">{ string(pr.State) }</span>
</div>
<div class="text-xs text-gray-500 mb-4">
{ pr.AuthorName } wants to merge <code>{ pr.HeadBranch }</code> into <code>{ pr.BaseBranch }</code>
</div>
if errMsg != "" {
<p class="text-red-600 mb-3">{ errMsg }</p>
}
@commentCard(pr.AuthorName, fmtTime(pr.CreatedAt), pr.Body)
for _, c := range comments {
@commentCard(c.AuthorName, fmtTime(c.CreatedAt), c.Body)
}
<div class="border p-3 mb-4">
if pr.State == domain.PRMerged {
<div class="text-purple-700">This pull request was merged.</div>
} else if pr.State == domain.PRClosed {
<div class="text-gray-600">This pull request was closed.</div>
} else if canMerge {
<form method="post" action={ templ.SafeURL(repoPath(repo, "/pulls/"+itoa(pr.Number)+"/merge")) }>
+ @csrfField()
<button class="border px-3 py-1 bg-green-700 text-white" type="submit">Merge pull request</button>
</form>
} else if user != nil {
<div class="text-gray-600">You cannot merge this pull request.</div>
} else {
<div class="text-gray-600"><a href="/login">Sign in</a> to act on this pull request.</div>
}
</div>
if cmp != nil {
<h3 class="text-md mb-2">Commits</h3>
@commitsList(repo, cmp.Commits)
<h3 class="text-md mb-2">Files changed</h3>
@diffFiles(cmp.Files)
}
if user != nil {
<form method="post" action={ templ.SafeURL(repoPath(repo, "/pulls/"+itoa(pr.Number)+"/comments")) } class="flex flex-col gap-2 mt-4">
+ @csrfField()
<textarea class="border px-2 py-1" name="body" rows="4" placeholder="Leave a comment (markdown supported)"></textarea>
<div><button class="border px-3 py-1" type="submit">Comment</button></div>
</form>
}
</div>
}
}
internal/interface/web/templates/pull_detail_templ.go +25 −9
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.865
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "gitgud/internal/domain"
func PullDetail(user *domain.User, repo *domain.Repository, pr *domain.PullRequest, cmp *domain.Comparison, comments []*domain.PRComment, canMerge bool, errMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-3xl mx-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = repoHeader(repo, "pulls").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex items-center gap-2 mb-1\"><h2 class=\"text-xl\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(pr.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/pull_detail.templ`, Line: 10, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " <span class=\"text-gray-400\">#")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(pr.Number))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/pull_detail.templ`, Line: 10, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span></h2><span class=\"text-xs border px-2 py-0.5\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(string(pr.State))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/pull_detail.templ`, Line: 11, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span></div><div class=\"text-xs text-gray-500 mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(pr.AuthorName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/pull_detail.templ`, Line: 14, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " wants to merge <code>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(pr.HeadBranch)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/pull_detail.templ`, Line: 14, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</code> into <code>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(pr.BaseBranch)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/pull_detail.templ`, Line: 14, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</code></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errMsg != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"text-red-600 mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/pull_detail.templ`, Line: 17, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = commentCard(pr.AuthorName, fmtTime(pr.CreatedAt), pr.Body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, c := range comments {
templ_7745c5c3_Err = commentCard(c.AuthorName, fmtTime(c.CreatedAt), c.Body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"border p-3 mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if pr.State == domain.PRMerged {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"text-purple-700\">This pull request was merged.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if pr.State == domain.PRClosed {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"text-gray-600\">This pull request was closed.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if canMerge {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<form method=\"post\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 templ.SafeURL = templ.SafeURL(repoPath(repo, "/pulls/"+itoa(pr.Number)+"/merge"))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var10)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"><button class=\"border px-3 py-1 bg-green-700 text-white\" type=\"submit\">Merge pull request</button></form>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = csrfField().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<button class=\"border px-3 py-1 bg-green-700 text-white\" type=\"submit\">Merge pull request</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if user != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"text-gray-600\">You cannot merge this pull request.</div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"text-gray-600\">You cannot merge this pull request.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"text-gray-600\"><a href=\"/login\">Sign in</a> to act on this pull request.</div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"text-gray-600\"><a href=\"/login\">Sign in</a> to act on this pull request.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cmp != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<h3 class=\"text-md mb-2\">Commits</h3>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<h3 class=\"text-md mb-2\">Commits</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = commitsList(repo, cmp.Commits).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " <h3 class=\"text-md mb-2\">Files changed</h3>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " <h3 class=\"text-md mb-2\">Files changed</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = diffFiles(cmp.Files).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if user != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<form method=\"post\" action=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<form method=\"post\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 templ.SafeURL = templ.SafeURL(repoPath(repo, "/pulls/"+itoa(pr.Number)+"/comments"))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var11)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" class=\"flex flex-col gap-2 mt-4\"><textarea class=\"border px-2 py-1\" name=\"body\" rows=\"4\" placeholder=\"Leave a comment (markdown supported)\"></textarea><div><button class=\"border px-3 py-1\" type=\"submit\">Comment</button></div></form>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"flex flex-col gap-2 mt-4\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = csrfField().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<textarea class=\"border px-2 py-1\" name=\"body\" rows=\"4\" placeholder=\"Leave a comment (markdown supported)\"></textarea><div><button class=\"border px-3 py-1\" type=\"submit\">Comment</button></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout(pr.Title+" · "+repo.Name, user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
internal/interface/web/templates/register.templ +1 −0
package templates
import "gitgud/internal/domain"
templ Register(user *domain.User, username, email, errMsg string) {
@Layout("Register · gitgud", user) {
<div class="max-w-sm mx-auto">
<h1 class="text-2xl mb-4">Register</h1>
if errMsg != "" {
<p class="text-red-600 mb-3">{ errMsg }</p>
}
<form method="post" action="/register" class="flex flex-col gap-3">
+ @csrfField()
<label class="flex flex-col gap-1">
<span>Username</span>
<input class="border px-2 py-1" type="text" name="username" value={ username } autofocus/>
</label>
<label class="flex flex-col gap-1">
<span>Email</span>
<input class="border px-2 py-1" type="email" name="email" value={ email }/>
</label>
<label class="flex flex-col gap-1">
<span>Password</span>
<input class="border px-2 py-1" type="password" name="password"/>
</label>
<button class="border px-3 py-1" type="submit">Create account</button>
</form>
<p class="mt-3 text-sm">Already have an account? <a href="/login">Log in</a></p>
</div>
}
}
internal/interface/web/templates/register_templ.go +13 −5
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.865
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "gitgud/internal/domain"
func Register(user *domain.User, username, email, errMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-sm mx-auto\"><h1 class=\"text-2xl mb-4\">Register</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errMsg != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"text-red-600 mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/register.templ`, Line: 10, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"post\" action=\"/register\" class=\"flex flex-col gap-3\"><label class=\"flex flex-col gap-1\"><span>Username</span> <input class=\"border px-2 py-1\" type=\"text\" name=\"username\" value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"post\" action=\"/register\" class=\"flex flex-col gap-3\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = csrfField().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<label class=\"flex flex-col gap-1\"><span>Username</span> <input class=\"border px-2 py-1\" type=\"text\" name=\"username\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(username)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/register.templ`, Line: 15, Col: 81}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/register.templ`, Line: 16, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" autofocus></label> <label class=\"flex flex-col gap-1\"><span>Email</span> <input class=\"border px-2 py-1\" type=\"email\" name=\"email\" value=\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" autofocus></label> <label class=\"flex flex-col gap-1\"><span>Email</span> <input class=\"border px-2 py-1\" type=\"email\" name=\"email\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(email)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/register.templ`, Line: 19, Col: 76}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/interface/web/templates/register.templ`, Line: 20, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></label> <label class=\"flex flex-col gap-1\"><span>Password</span> <input class=\"border px-2 py-1\" type=\"password\" name=\"password\"></label> <button class=\"border px-3 py-1\" type=\"submit\">Create account</button></form><p class=\"mt-3 text-sm\">Already have an account? <a href=\"/login\">Log in</a></p></div>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"></label> <label class=\"flex flex-col gap-1\"><span>Password</span> <input class=\"border px-2 py-1\" type=\"password\" name=\"password\"></label> <button class=\"border px-3 py-1\" type=\"submit\">Create account</button></form><p class=\"mt-3 text-sm\">Already have an account? <a href=\"/login\">Log in</a></p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout("Register · gitgud", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate