feat: initial commit
This commit is contained in:
parent
ee4a65b345
commit
e6e5c9b04d
@ -6,14 +6,13 @@ CLEARCASE_LOGGER_LEVEL=-4
|
|||||||
# Gitea auth provider (disabled if empty)
|
# Gitea auth provider (disabled if empty)
|
||||||
CLEARCASE_AUTH_PROVIDERS_GITEA_KEY=
|
CLEARCASE_AUTH_PROVIDERS_GITEA_KEY=
|
||||||
CLEARCASE_AUTH_PROVIDERS_GITEA_SECRET=
|
CLEARCASE_AUTH_PROVIDERS_GITEA_SECRET=
|
||||||
CLEARCASE_AUTH_PROVIDERS_GITEA_SCOPES="user:email"
|
CLEARCASE_AUTH_PROVIDERS_GITEA_AUTH_URL=https://forge.cadoles.com/login/oauth/authorize
|
||||||
|
CLEARCASE_AUTH_PROVIDERS_GITEA_TOKEN_URL=https://forge.cadoles.com/login/oauth/access_token
|
||||||
|
CLEARCASE_AUTH_PROVIDERS_GITEA_PROFILE_URL=https://forge.cadoles.com/login/oauth/userinfo
|
||||||
|
|
||||||
# HTTP session keys (list of 32 characters strings, should be modified in production)
|
# HTTP session keys (list of 32 characters strings, should be modified in production)
|
||||||
CLEARCASE_HTTP_SESSION_KEYS=abcdefghijklmnopqrstuvwxyz000000
|
CLEARCASE_HTTP_SESSION_KEYS=abcdefghijklmnopqrstuvwxyz000000
|
||||||
|
|
||||||
# Base URL, used in templates and link generation
|
|
||||||
CLEARCASE_HTTP_BASE_URL=http://localhost:3001
|
|
||||||
|
|
||||||
# LLM Provider
|
# LLM Provider
|
||||||
# Example with ollama - llama3.1:8b :
|
# Example with ollama - llama3.1:8b :
|
||||||
CLEARCASE_LLM_PROVIDER_BASE_URL="http://localhost:11434/api/"
|
CLEARCASE_LLM_PROVIDER_BASE_URL="http://localhost:11434/api/"
|
||||||
|
11
.gitea/issue_template.md
Normal file
11
.gitea/issue_template.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
> La description du besoin.
|
||||||
|
|
||||||
|
## Détails d'implémentation
|
||||||
|
|
||||||
|
> Liste des actions à réaliser pour remplir le besoin.
|
||||||
|
|
||||||
|
## Tests d'acceptance
|
||||||
|
|
||||||
|
> Liste des critères d'acception de réalisation du ticket.
|
@ -1,4 +1,4 @@
|
|||||||
# Rkvst
|
# ClearCase
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@ -13,4 +13,4 @@ vim .env
|
|||||||
make watch
|
make watch
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open http://localhost:3000 in your browser.
|
Then open http://localhost:3001 in your browser.
|
||||||
|
29
go.mod
29
go.mod
@ -1,13 +1,19 @@
|
|||||||
module forge.cadoles.com/wpetit/clearcase
|
module forge.cadoles.com/wpetit/clearcase
|
||||||
|
|
||||||
go 1.23.1
|
go 1.23.4
|
||||||
|
|
||||||
|
toolchain go1.23.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
code.gitea.io/sdk/gitea v0.20.0
|
||||||
github.com/a-h/templ v0.3.819
|
github.com/a-h/templ v0.3.819
|
||||||
|
github.com/bornholm/genai v0.0.0-20250222092500-1076426da67c
|
||||||
github.com/caarlos0/env/v11 v11.2.2
|
github.com/caarlos0/env/v11 v11.2.2
|
||||||
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/gabriel-vasile/mimetype v1.4.7
|
github.com/gabriel-vasile/mimetype v1.4.7
|
||||||
github.com/gorilla/sessions v1.1.1
|
github.com/gorilla/sessions v1.1.1
|
||||||
github.com/markbates/goth v1.80.0
|
github.com/markbates/goth v1.80.0
|
||||||
|
github.com/num30/go-cache v1.0.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/samber/slog-http v1.4.4
|
github.com/samber/slog-http v1.4.4
|
||||||
)
|
)
|
||||||
@ -15,15 +21,34 @@ require (
|
|||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute v1.20.1 // indirect
|
cloud.google.com/go/compute v1.20.1 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
|
github.com/42wim/httpsig v1.2.1 // indirect
|
||||||
|
github.com/BurntSushi/toml v1.0.0 // indirect
|
||||||
|
github.com/RealAlexandreAI/json-repair v0.0.14 // indirect
|
||||||
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
github.com/gorilla/mux v1.6.2 // indirect
|
github.com/gorilla/mux v1.6.2 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
|
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||||
|
github.com/openai/openai-go v0.1.0-alpha.59 // indirect
|
||||||
|
github.com/revrost/go-openrouter v0.0.0-20250128091643-3d014d57014d // indirect
|
||||||
|
github.com/tidwall/gjson v1.14.4 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
go.opentelemetry.io/otel v1.29.0 // indirect
|
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
|
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect
|
||||||
|
golang.org/x/mod v0.20.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
golang.org/x/oauth2 v0.17.0 // indirect
|
golang.org/x/oauth2 v0.17.0 // indirect
|
||||||
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/tools v0.24.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
|
honnef.co/go/tools v0.3.1 // indirect
|
||||||
)
|
)
|
||||||
|
56
go.sum
56
go.sum
@ -2,14 +2,28 @@ cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZN
|
|||||||
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||||
|
code.gitea.io/sdk/gitea v0.20.0 h1:Zm/QDwwZK1awoM4AxdjeAQbxolzx2rIP8dDfmKu+KoU=
|
||||||
|
code.gitea.io/sdk/gitea v0.20.0/go.mod h1:faouBHC/zyx5wLgjmRKR62ydyvMzwWf3QnU0bH7Cw6U=
|
||||||
|
github.com/42wim/httpsig v1.2.1 h1:oLBxptMe9U4ZmSGtkosT8Dlfg31P3VQnAGq6psXv82Y=
|
||||||
|
github.com/42wim/httpsig v1.2.1/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY=
|
||||||
|
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
|
||||||
|
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/RealAlexandreAI/json-repair v0.0.14 h1:4kTqotVonDVTio5n2yweRUELVcNe2x518wl0bCsw0t0=
|
||||||
|
github.com/RealAlexandreAI/json-repair v0.0.14/go.mod h1:GKJi5borR78O8c7HCVbgqjhoiVibZ6hJldxbc6dGrAI=
|
||||||
github.com/a-h/templ v0.3.819 h1:KDJ5jTFN15FyJnmSmo2gNirIqt7hfvBD2VXVDTySckM=
|
github.com/a-h/templ v0.3.819 h1:KDJ5jTFN15FyJnmSmo2gNirIqt7hfvBD2VXVDTySckM=
|
||||||
github.com/a-h/templ v0.3.819/go.mod h1:iDJKJktpttVKdWoTkRNNLcllRI+BlpopJc+8au3gOUo=
|
github.com/a-h/templ v0.3.819/go.mod h1:iDJKJktpttVKdWoTkRNNLcllRI+BlpopJc+8au3gOUo=
|
||||||
|
github.com/bornholm/genai v0.0.0-20250222092500-1076426da67c h1:bI0ebsgO1/7Jx6+ZQdDF/I6tTZxyB5hODYz7x/XxwK8=
|
||||||
|
github.com/bornholm/genai v0.0.0-20250222092500-1076426da67c/go.mod h1:MnuvwSsBEWv/joeK/WgUyfZfOLcLTpd81NJdWoRpRfI=
|
||||||
github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg=
|
github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg=
|
||||||
github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc=
|
github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||||
|
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||||
|
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
@ -27,40 +41,74 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
|
|||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
|
github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
|
||||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||||
|
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||||
|
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
|
github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
|
||||||
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
|
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
|
||||||
|
github.com/num30/go-cache v1.0.0 h1:uvXf3X4xEm+AB8b2MtMouFpgevR/sFCOOxXKaofzReg=
|
||||||
|
github.com/num30/go-cache v1.0.0/go.mod h1:YxYzvWbR26wa2xojzZ8W3c+NSSwBzYtrorHSkeRaJWw=
|
||||||
|
github.com/openai/openai-go v0.1.0-alpha.59 h1:T3IYwKSCezfIlL9Oi+CGvU03fq0RoH33775S78Ti48Y=
|
||||||
|
github.com/openai/openai-go v0.1.0-alpha.59/go.mod h1:3SdE6BffOX9HPEQv8IL/fi3LYZ5TUpRYaqGQZbyk11A=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/revrost/go-openrouter v0.0.0-20250128091643-3d014d57014d h1:3PCl9WWy1S3YpCzlXrDoBJqPJ59IsiqKSf8d3tHDYn0=
|
||||||
|
github.com/revrost/go-openrouter v0.0.0-20250128091643-3d014d57014d/go.mod h1:UIrJIZBygNz1DygZPImp56zCjv5IJNNkdp2hNUgn9H4=
|
||||||
github.com/samber/slog-http v1.4.4 h1:NuENLy39Lk6b7wfj9cG9R5C/JLZR4t6pb9cwlyroybI=
|
github.com/samber/slog-http v1.4.4 h1:NuENLy39Lk6b7wfj9cG9R5C/JLZR4t6pb9cwlyroybI=
|
||||||
github.com/samber/slog-http v1.4.4/go.mod h1:PAcQQrYFo5KM7Qbk50gNNwKEAMGCyfsw6GN5dI0iv9g=
|
github.com/samber/slog-http v1.4.4/go.mod h1:PAcQQrYFo5KM7Qbk50gNNwKEAMGCyfsw6GN5dI0iv9g=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||||
|
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||||
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM=
|
||||||
|
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||||
|
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
@ -68,6 +116,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
|||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||||
|
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||||
@ -78,3 +128,5 @@ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFW
|
|||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.3.1 h1:1kJlrWJLkaGXgcaeosRXViwviqjI7nkBvU2+sZW0AYc=
|
||||||
|
honnef.co/go/tools v0.3.1/go.mod h1:vlRD9XErLMGT+mDuofSr0mMMquscM/1nQqtRSsh6m70=
|
||||||
|
90
internal/adapter/gitea/forge.go
Normal file
90
internal/adapter/gitea/forge.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package gitea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/port"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Forge struct {
|
||||||
|
client *gitea.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateIssue implements port.Forge.
|
||||||
|
func (f *Forge) CreateIssue(ctx context.Context, projectID string, title string, content string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIssueTemplate implements port.Forge.
|
||||||
|
func (f *Forge) GetIssueTemplate(ctx context.Context, rawProjectID string) (string, error) {
|
||||||
|
projectID, err := strconv.ParseInt(rawProjectID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
project, _, err := f.client.GetRepoByID(projectID)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _, err := f.client.GetFile(project.Owner.UserName, project.Name, project.DefaultBranch, ".gitea/issue_template.md")
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIssues implements port.Forge.
|
||||||
|
func (f *Forge) GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListProjects implements port.Forge.
|
||||||
|
func (f *Forge) GetProjects(ctx context.Context) ([]*model.Project, error) {
|
||||||
|
projects := make([]*model.Project, 0)
|
||||||
|
|
||||||
|
page := 1
|
||||||
|
for {
|
||||||
|
repositories, res, err := f.client.ListMyRepos(gitea.ListReposOptions{
|
||||||
|
ListOptions: gitea.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: 100,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
page = res.NextPage
|
||||||
|
|
||||||
|
if res.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range repositories {
|
||||||
|
projects = append(projects, &model.Project{
|
||||||
|
ID: strconv.FormatInt(r.ID, 10),
|
||||||
|
Label: r.Owner.UserName + "/" + r.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(projects, func(p1 *model.Project, p2 *model.Project) int {
|
||||||
|
return strings.Compare(p1.Label, p2.Label)
|
||||||
|
})
|
||||||
|
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewForge(client *gitea.Client) *Forge {
|
||||||
|
return &Forge{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ port.Forge = &Forge{}
|
@ -1,25 +1,20 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
DefaultAdmin DefaultAdmin `envPrefix:"DEFAULT_ADMIN_"`
|
Providers AuthProviders `envPrefix:"PROVIDERS_"`
|
||||||
Providers AuthProviders `envPrefix:"PROVIDERS_"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DefaultAdmin struct {
|
|
||||||
Email string `env:"EMAIL,expand"`
|
|
||||||
Provider string `env:"PROVIDER,expand"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthProviders struct {
|
type AuthProviders struct {
|
||||||
Google OAuth2Provider `envPrefix:"GOOGLE_"`
|
Google OAuth2Provider `envPrefix:"GOOGLE_"`
|
||||||
Github OAuth2Provider `envPrefix:"GITHUB_"`
|
Github OAuth2Provider `envPrefix:"GITHUB_"`
|
||||||
|
Gitea GiteaProvider `envPrefix:"GITEA_"`
|
||||||
OIDC OIDCProvider `envPrefix:"OIDC_"`
|
OIDC OIDCProvider `envPrefix:"OIDC_"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuth2Provider struct {
|
type OAuth2Provider struct {
|
||||||
Key string `env:"KEY,expand"`
|
Key string `env:"KEY,expand"`
|
||||||
Secret string `env:"SECRET,expand"`
|
Secret string `env:"SECRET,expand"`
|
||||||
Scopes []string `env:"SCOPES",expand"`
|
Scopes []string `env:"SCOPES,expand"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OIDCProvider struct {
|
type OIDCProvider struct {
|
||||||
@ -28,3 +23,11 @@ type OIDCProvider struct {
|
|||||||
Icon string `env:"ICON,expand" envDefault:"fa-passport"`
|
Icon string `env:"ICON,expand" envDefault:"fa-passport"`
|
||||||
Label string `env:"LABEL,expand" envDefault:"OpenID Connect"`
|
Label string `env:"LABEL,expand" envDefault:"OpenID Connect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GiteaProvider struct {
|
||||||
|
OAuth2Provider
|
||||||
|
TokenURL string `env:"TOKEN_URL,expand"`
|
||||||
|
AuthURL string `env:"AUTH_URL,expand"`
|
||||||
|
ProfileURL string `env:"PROFILE_URL,expand"`
|
||||||
|
Label string `env:"LABEL,expand" envDefault:"Gitea"`
|
||||||
|
}
|
||||||
|
@ -6,11 +6,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Logger Logger `envPrefix:"LOGGER_"`
|
Logger Logger `envPrefix:"LOGGER_"`
|
||||||
Auth Auth `envPrefix:"AUTH_"`
|
Auth Auth `envPrefix:"AUTH_"`
|
||||||
HTTP HTTP `envPrefix:"HTTP_"`
|
HTTP HTTP `envPrefix:"HTTP_"`
|
||||||
Storage Storage `envPrefix:"STORAGE_"`
|
LLM LLM `envPrefix:"LLM_"`
|
||||||
LLM LLM `envPrefix:"LLM_"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse() (*Config, error) {
|
func Parse() (*Config, error) {
|
||||||
|
@ -3,8 +3,8 @@ package config
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type HTTP struct {
|
type HTTP struct {
|
||||||
BaseURL string `env:"BASE_URL" envDefault:"http://localhost:3000"`
|
BaseURL string `env:"BASE_URL" envDefault:"http://localhost:3001"`
|
||||||
Address string `env:"ADDRESS,expand" envDefault:":3000"`
|
Address string `env:"ADDRESS,expand" envDefault:":3001"`
|
||||||
Session Session `envPrefix:"SESSION_"`
|
Session Session `envPrefix:"SESSION_"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
type Storage struct {
|
|
||||||
Database Database `envPrefix:"DATABASE_"`
|
|
||||||
Object Object `envPrefix:"OBJECT_"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Database struct {
|
|
||||||
DSN string `env:"DSN" envDefault:"sqlite://data.sqlite"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Object struct {
|
|
||||||
DSN string `env:"DSN" envDefault:"sqlite://data.sqlite"`
|
|
||||||
Encryption Encryption `envPrefix:"ENCRYPTION_"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Encryption struct {
|
|
||||||
Key string `env:"KEY,unset"`
|
|
||||||
}
|
|
6
internal/core/model/issue.go
Normal file
6
internal/core/model/issue.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Issue struct {
|
||||||
|
ID string
|
||||||
|
Content string
|
||||||
|
}
|
6
internal/core/model/project.go
Normal file
6
internal/core/model/project.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
ID string
|
||||||
|
Label string
|
||||||
|
}
|
8
internal/core/model/user.go
Normal file
8
internal/core/model/user.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
Provider string
|
||||||
|
AccessToken string
|
||||||
|
IDToken string
|
||||||
|
}
|
14
internal/core/port/forge.go
Normal file
14
internal/core/port/forge.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package port
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Forge interface {
|
||||||
|
GetProjects(ctx context.Context) ([]*model.Project, error)
|
||||||
|
CreateIssue(ctx context.Context, projectID string, title string, content string) error
|
||||||
|
GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error)
|
||||||
|
GetIssueTemplate(ctx context.Context, projectID string) (string, error)
|
||||||
|
}
|
151
internal/core/service/issue_manager.go
Normal file
151
internal/core/service/issue_manager.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/port"
|
||||||
|
"github.com/bornholm/genai/llm"
|
||||||
|
"github.com/num30/go-cache"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrForgeNotAvailable = errors.New("forge not available")
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed issue_system_prompt.gotmpl
|
||||||
|
var issueSystemPromptRawTemplate string
|
||||||
|
|
||||||
|
//go:embed issue_user_prompt.gotmpl
|
||||||
|
var issueUserPromptRawTemplate string
|
||||||
|
|
||||||
|
type ForgeFactory interface {
|
||||||
|
Match(user *model.User) bool
|
||||||
|
Create(ctx context.Context, user *model.User) (port.Forge, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IssueManager struct {
|
||||||
|
forgeFactories []ForgeFactory
|
||||||
|
llmClient llm.Client
|
||||||
|
projectCache *cache.Cache[[]*model.Project]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *IssueManager) GetUserProjects(ctx context.Context, user *model.User) ([]*model.Project, error) {
|
||||||
|
cacheKey := fmt.Sprintf("%s/%s", user.Provider, user.ID)
|
||||||
|
|
||||||
|
projects, exists := m.projectCache.Get(cacheKey)
|
||||||
|
if !exists {
|
||||||
|
forge, err := m.getUserForge(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshedProjects, err := forge.GetProjects(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.projectCache.Set(cacheKey, refreshedProjects, 0)
|
||||||
|
|
||||||
|
projects = refreshedProjects
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *IssueManager) GenerateIssue(ctx context.Context, user *model.User, projectID string, issueSummary string) (string, error) {
|
||||||
|
systemPrompt, err := m.getIssueSystemPrompt(ctx, user, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userPrompt, err := m.getIssueUserPrompt(ctx, user, projectID, issueSummary)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := []llm.Message{
|
||||||
|
llm.NewMessage(llm.RoleSystem, systemPrompt),
|
||||||
|
llm.NewMessage(llm.RoleUser, userPrompt),
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := m.llmClient.ChatCompletion(ctx, llm.WithMessages(messages...))
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Message().Content(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *IssueManager) getIssueSystemPrompt(ctx context.Context, user *model.User, projectID string) (string, error) {
|
||||||
|
forge, err := m.getUserForge(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
issueTemplate, err := forge.GetIssueTemplate(ctx, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPrompt, err := llm.PromptTemplate(issueSystemPromptRawTemplate, struct {
|
||||||
|
IssueTemplate string
|
||||||
|
}{
|
||||||
|
IssueTemplate: issueTemplate,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPrompt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *IssueManager) getIssueUserPrompt(ctx context.Context, user *model.User, projectID string, issueSummary string) (string, error) {
|
||||||
|
_, err := m.getUserForge(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userPrompt, err := llm.PromptTemplate(issueUserPromptRawTemplate, struct {
|
||||||
|
Context string
|
||||||
|
}{
|
||||||
|
Context: issueSummary,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userPrompt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *IssueManager) getUserForge(ctx context.Context, user *model.User) (port.Forge, error) {
|
||||||
|
for _, f := range m.forgeFactories {
|
||||||
|
if !f.Match(user) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
forge, err := f.Create(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "could not retrieve user forge", slog.Any("error", errors.WithStack(err)))
|
||||||
|
return nil, errors.WithStack(ErrForgeNotAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
return forge, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("no forge matching user found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIssueManager(llmClient llm.Client, forgeFactories ...ForgeFactory) *IssueManager {
|
||||||
|
return &IssueManager{
|
||||||
|
llmClient: llmClient,
|
||||||
|
forgeFactories: forgeFactories,
|
||||||
|
projectCache: cache.New[[]*model.Project](time.Minute*5, (time.Minute*5)/2),
|
||||||
|
}
|
||||||
|
}
|
20
internal/core/service/issue_system_prompt.gotmpl
Normal file
20
internal/core/service/issue_system_prompt.gotmpl
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
You are an expert software developer with extensive experience in writing clear and comprehensive issues and requests for software forges. Your task is to create well-structured issues/requests based on the provided contextual information, following a predefined Markdown layout.
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
1. **Issue/request Description**:
|
||||||
|
- Provide a detailed description of the issue/request, including:
|
||||||
|
- Background information.
|
||||||
|
- Steps to reproduce the issue.
|
||||||
|
- Expected behavior.
|
||||||
|
- Actual behavior.
|
||||||
|
- Any relevant error messages or logs.
|
||||||
|
|
||||||
|
2. **Additional Context**:
|
||||||
|
- Include any other relevant information that might help in understanding or resolving the issue/request.
|
||||||
|
|
||||||
|
**Markdown Layout:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
{{ .IssueTemplate }}
|
||||||
|
```
|
3
internal/core/service/issue_user_prompt.gotmpl
Normal file
3
internal/core/service/issue_user_prompt.gotmpl
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Write a formatted issue/request based on theses contextual informations:
|
||||||
|
|
||||||
|
{{ .Context }}
|
@ -3,14 +3,14 @@ package context
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/markbates/goth"
|
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const keyUser = "user"
|
const keyUser = "user"
|
||||||
|
|
||||||
func User(ctx context.Context) *goth.User {
|
func User(ctx context.Context) *model.User {
|
||||||
user, ok := ctx.Value(keyUser).(*goth.User)
|
user, ok := ctx.Value(keyUser).(*model.User)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(errors.New("no user in context"))
|
panic(errors.New("no user in context"))
|
||||||
}
|
}
|
||||||
@ -18,6 +18,6 @@ func User(ctx context.Context) *goth.User {
|
|||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetUser(ctx context.Context, user *goth.User) context.Context {
|
func SetUser(ctx context.Context, user *model.User) context.Context {
|
||||||
return context.WithValue(ctx, keyUser, user)
|
return context.WithValue(ctx, keyUser, user)
|
||||||
}
|
}
|
||||||
|
@ -17,15 +17,15 @@ templ LoginPage(vmodel LoginPageVModel) {
|
|||||||
<div class="is-flex is-justify-content-center is-align-items-center is-fullheight">
|
<div class="is-flex is-justify-content-center is-align-items-center is-fullheight">
|
||||||
<nav class="panel is-link" style="min-width: 33%">
|
<nav class="panel is-link" style="min-width: 33%">
|
||||||
<p class="panel-heading">
|
<p class="panel-heading">
|
||||||
<div class="title">ClearCase</div>
|
<span class="title">ClearCase</span>
|
||||||
<span> - choose your provider</span>
|
<span> - choose your platform</span>
|
||||||
</p>
|
</p>
|
||||||
for _, provider := range vmodel.Providers {
|
for _, provider := range vmodel.Providers {
|
||||||
<a class="panel-block" href={ templ.URL("/auth/providers/" + provider.ID) } hx-boost="false">
|
<a class="panel-block py-5" href={ templ.URL("/auth/providers/" + provider.ID) } hx-boost="false">
|
||||||
<span class="panel-icon is-size-3">
|
<span class="panel-icon is-size-3">
|
||||||
<i class={ "fab", provider.Icon } aria-hidden="true"></i>
|
<i class={ "fab", provider.Icon } aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
{ provider.Label }
|
<span class="is-size-5">{ provider.Label }</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -53,12 +53,12 @@ func LoginPage(vmodel LoginPageVModel) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"is-flex is-justify-content-center is-align-items-center is-fullheight\"><nav class=\"panel is-link\" style=\"min-width: 33%\"><p class=\"panel-heading\"><div class=\"title\">ClearCase</div><span> - choose your provider</span></p>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"is-flex is-justify-content-center is-align-items-center is-fullheight\"><nav class=\"panel is-link\" style=\"min-width: 33%\"><p class=\"panel-heading\"><span class=\"title\">ClearCase</span> <span> - choose your platform</span></p>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
for _, provider := range vmodel.Providers {
|
for _, provider := range vmodel.Providers {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a class=\"panel-block\" href=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a class=\"panel-block py-5\" href=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -89,20 +89,20 @@ func LoginPage(vmodel LoginPageVModel) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" aria-hidden=\"true\"></i></span> ")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" aria-hidden=\"true\"></i></span> <span class=\"is-size-5\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(provider.Label)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(provider.Label)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/auth/component/login_page.templ`, Line: 28, Col: 22}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/auth/component/login_page.templ`, Line: 28, Col: 46}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</a>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span></a>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ type Handler struct {
|
|||||||
sessionStore sessions.Store
|
sessionStore sessions.Store
|
||||||
sessionName string
|
sessionName string
|
||||||
providers []Provider
|
providers []Provider
|
||||||
defaultAdmin *DefaultAdmin
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler.
|
// ServeHTTP implements http.Handler.
|
||||||
@ -30,7 +29,6 @@ func NewHandler(sessionStore sessions.Store, funcs ...OptionFunc) *Handler {
|
|||||||
sessionStore: sessionStore,
|
sessionStore: sessionStore,
|
||||||
sessionName: opts.SessionName,
|
sessionName: opts.SessionName,
|
||||||
providers: opts.Providers,
|
providers: opts.Providers,
|
||||||
defaultAdmin: opts.DefaultAdmin,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h.mux.HandleFunc("GET /login", h.getLoginPage)
|
h.mux.HandleFunc("GET /login", h.getLoginPage)
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
type DefaultAdmin struct {
|
|
||||||
Provider string
|
|
||||||
Email string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Providers []Provider
|
Providers []Provider
|
||||||
DefaultAdmin *DefaultAdmin
|
SessionName string
|
||||||
SessionName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionFunc func(opts *Options)
|
type OptionFunc func(opts *Options)
|
||||||
@ -16,7 +10,7 @@ type OptionFunc func(opts *Options)
|
|||||||
func NewOptions(funcs ...OptionFunc) *Options {
|
func NewOptions(funcs ...OptionFunc) *Options {
|
||||||
opts := &Options{
|
opts := &Options{
|
||||||
Providers: make([]Provider, 0),
|
Providers: make([]Provider, 0),
|
||||||
SessionName: "rkvst_auth",
|
SessionName: "clearcase_auth",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fn := range funcs {
|
for _, fn := range funcs {
|
||||||
@ -32,12 +26,6 @@ func WithProviders(providers ...Provider) OptionFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithDefaultAdmin(defaultAdmin DefaultAdmin) OptionFunc {
|
|
||||||
return func(opts *Options) {
|
|
||||||
opts.DefaultAdmin = &defaultAdmin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithSessionName(sessionName string) OptionFunc {
|
func WithSessionName(sessionName string) OptionFunc {
|
||||||
return func(opts *Options) {
|
return func(opts *Options) {
|
||||||
opts.SessionName = sessionName
|
opts.SessionName = sessionName
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common"
|
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common"
|
||||||
"github.com/markbates/goth/gothic"
|
"github.com/markbates/goth/gothic"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -19,7 +20,7 @@ func (h *Handler) handleProvider(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleProviderCallback(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleProviderCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
user, err := gothic.CompleteUserAuth(w, r)
|
gothUser, err := gothic.CompleteUserAuth(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(r.Context(), "could not complete user auth", slog.Any("error", errors.WithStack(err)))
|
slog.ErrorContext(r.Context(), "could not complete user auth", slog.Any("error", errors.WithStack(err)))
|
||||||
http.Redirect(w, r, "/auth/logout", http.StatusTemporaryRedirect)
|
http.Redirect(w, r, "/auth/logout", http.StatusTemporaryRedirect)
|
||||||
@ -28,9 +29,16 @@ func (h *Handler) handleProviderCallback(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
slog.DebugContext(ctx, "authenticated user", slog.Any("user", user))
|
slog.DebugContext(ctx, "authenticated user", slog.Any("user", gothUser))
|
||||||
|
|
||||||
if err := h.storeSessionUser(w, r, &user); err != nil {
|
user := &model.User{
|
||||||
|
ID: gothUser.UserID,
|
||||||
|
Provider: gothUser.Provider,
|
||||||
|
AccessToken: gothUser.AccessToken,
|
||||||
|
IDToken: gothUser.IDToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.storeSessionUser(w, r, user); err != nil {
|
||||||
slog.ErrorContext(r.Context(), "could not store session user", slog.Any("error", errors.WithStack(err)))
|
slog.ErrorContext(r.Context(), "could not store session user", slog.Any("error", errors.WithStack(err)))
|
||||||
http.Redirect(w, r, "/auth/logout", http.StatusTemporaryRedirect)
|
http.Redirect(w, r, "/auth/logout", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/gob"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/markbates/goth"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,7 +15,11 @@ const tenantIDAttr = "t"
|
|||||||
|
|
||||||
var errSessionNotFound = errors.New("session not found")
|
var errSessionNotFound = errors.New("session not found")
|
||||||
|
|
||||||
func (h *Handler) storeSessionUser(w http.ResponseWriter, r *http.Request, user *goth.User) error {
|
func init() {
|
||||||
|
gob.Register(&model.User{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) storeSessionUser(w http.ResponseWriter, r *http.Request, user *model.User) error {
|
||||||
sess, err := h.getSession(r)
|
sess, err := h.getSession(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
@ -29,13 +34,13 @@ func (h *Handler) storeSessionUser(w http.ResponseWriter, r *http.Request, user
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) retrieveSessionUser(r *http.Request) (*goth.User, error) {
|
func (h *Handler) retrieveSessionUser(r *http.Request) (*model.User, error) {
|
||||||
sess, err := h.getSession(r)
|
sess, err := h.getSession(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, ok := sess.Values[userAttr].(*goth.User)
|
user, ok := sess.Values[userAttr].(*model.User)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.WithStack(errSessionNotFound)
|
return nil, errors.WithStack(errSessionNotFound)
|
||||||
}
|
}
|
||||||
|
@ -16,3 +16,15 @@ body {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.htmx-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-request .htmx-indicator {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-request.htmx-indicator {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@ -42,18 +42,99 @@ templ FormTextarea(form *form.Form, id string, name string, label string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ FormSelect(form *form.Form, id string, name string, label string, kvOptions ...string) {
|
type FormSelectOptions struct {
|
||||||
|
Options []formSelectOption
|
||||||
|
Attrs map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormSelectOptionFunc func(opts *FormSelectOptions)
|
||||||
|
|
||||||
|
func WithOptions(kvOptions ...string) FormSelectOptionFunc {
|
||||||
|
return func(opts *FormSelectOptions) {
|
||||||
|
opts.Options = keyValuesToOptions(kvOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAttrs(kvAttrs ...any) FormSelectOptionFunc {
|
||||||
|
return func(opts *FormSelectOptions) {
|
||||||
|
opts.Attrs = keyValuesToAttrs(kvAttrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyValuesToAttrs(kv []any) map[string]any {
|
||||||
|
if len(kv)%2 != 0 {
|
||||||
|
panic(errors.New("expected pair number of key/values"))
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := make(map[string]any, 0)
|
||||||
|
|
||||||
|
var key string
|
||||||
|
for idx := range kv {
|
||||||
|
if idx%2 == 0 {
|
||||||
|
key = kv[idx].(string)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs[key] = kv[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
type formSelectOption struct {
|
||||||
|
Value string
|
||||||
|
Label string
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyValuesToOptions(kv []string) []formSelectOption {
|
||||||
|
if len(kv)%2 != 0 {
|
||||||
|
panic(errors.New("expected pair number of key/values"))
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make([]formSelectOption, 0)
|
||||||
|
|
||||||
|
var key string
|
||||||
|
for idx := range kv {
|
||||||
|
if idx%2 == 0 {
|
||||||
|
key = kv[idx]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
options = append(options, formSelectOption{
|
||||||
|
Value: kv[idx],
|
||||||
|
Label: key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFormSelectOptions(funcs ...FormSelectOptionFunc) *FormSelectOptions {
|
||||||
|
opts := &FormSelectOptions{}
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opts)
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
templ FormSelect(form *form.Form, id string, name string, label string, funcs ...FormSelectOptionFunc) {
|
||||||
|
{{ opts := newFormSelectOptions(funcs...) }}
|
||||||
{{ field := form.Field(name) }}
|
{{ field := form.Field(name) }}
|
||||||
if field != nil {
|
if field != nil {
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for={ id }>{ label }</label>
|
<label class="label" for={ id }>{ label }</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{{ err, hasErr := form.Error(name) }}
|
{{ err, hasErr := form.Error(name) }}
|
||||||
|
{{ value, hasValue := field.Get("value") }}
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select id={ id } name={ field.Name() } { field.Attrs()... }>
|
<select id={ id } name={ field.Name() } { mergeAttrs(field.Attrs(), opts.Attrs)... }>
|
||||||
{{ options := keyValuesToOptions(kvOptions) }}
|
for _, o := range opts.Options {
|
||||||
for _, o := range options {
|
<option
|
||||||
<option value={ o.Value }>{ o.Label }</option>
|
if hasValue && value == o.Value {
|
||||||
|
selected
|
||||||
|
}
|
||||||
|
value={ o.Value }
|
||||||
|
>{ o.Label }</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -65,30 +146,12 @@ templ FormSelect(form *form.Form, id string, name string, label string, kvOption
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectOption struct {
|
func mergeAttrs(attrs ...map[string]any) map[string]any {
|
||||||
Value string
|
merged := make(form.Attrs)
|
||||||
Label string
|
for _, a := range attrs {
|
||||||
}
|
for k, v := range a {
|
||||||
|
merged[k] = v
|
||||||
func keyValuesToOptions(kv []string) []SelectOption {
|
|
||||||
if len(kv)%2 != 0 {
|
|
||||||
panic(errors.New("expected pair number of key/values"))
|
|
||||||
}
|
|
||||||
|
|
||||||
options := make([]SelectOption, 0)
|
|
||||||
|
|
||||||
var key string
|
|
||||||
for idx := range kv {
|
|
||||||
if idx%2 == 0 {
|
|
||||||
key = kv[idx]
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
options = append(options, SelectOption{
|
|
||||||
Value: kv[idx],
|
|
||||||
Label: key,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return merged
|
||||||
return options
|
|
||||||
}
|
}
|
||||||
|
@ -327,7 +327,82 @@ func FormTextarea(form *form.Form, id string, name string, label string) templ.C
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func FormSelect(form *form.Form, id string, name string, label string, kvOptions ...string) templ.Component {
|
type FormSelectOptions struct {
|
||||||
|
Options []formSelectOption
|
||||||
|
Attrs map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormSelectOptionFunc func(opts *FormSelectOptions)
|
||||||
|
|
||||||
|
func WithOptions(kvOptions ...string) FormSelectOptionFunc {
|
||||||
|
return func(opts *FormSelectOptions) {
|
||||||
|
opts.Options = keyValuesToOptions(kvOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAttrs(kvAttrs ...any) FormSelectOptionFunc {
|
||||||
|
return func(opts *FormSelectOptions) {
|
||||||
|
opts.Attrs = keyValuesToAttrs(kvAttrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyValuesToAttrs(kv []any) map[string]any {
|
||||||
|
if len(kv)%2 != 0 {
|
||||||
|
panic(errors.New("expected pair number of key/values"))
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := make(map[string]any, 0)
|
||||||
|
|
||||||
|
var key string
|
||||||
|
for idx := range kv {
|
||||||
|
if idx%2 == 0 {
|
||||||
|
key = kv[idx].(string)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs[key] = kv[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
type formSelectOption struct {
|
||||||
|
Value string
|
||||||
|
Label string
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyValuesToOptions(kv []string) []formSelectOption {
|
||||||
|
if len(kv)%2 != 0 {
|
||||||
|
panic(errors.New("expected pair number of key/values"))
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make([]formSelectOption, 0)
|
||||||
|
|
||||||
|
var key string
|
||||||
|
for idx := range kv {
|
||||||
|
if idx%2 == 0 {
|
||||||
|
key = kv[idx]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
options = append(options, formSelectOption{
|
||||||
|
Value: kv[idx],
|
||||||
|
Label: key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFormSelectOptions(funcs ...FormSelectOptionFunc) *FormSelectOptions {
|
||||||
|
opts := &FormSelectOptions{}
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opts)
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormSelect(form *form.Form, id string, name string, label string, funcs ...FormSelectOptionFunc) templ.Component {
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
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
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
@ -348,6 +423,7 @@ func FormSelect(form *form.Form, id string, name string, label string, kvOptions
|
|||||||
templ_7745c5c3_Var20 = templ.NopComponent
|
templ_7745c5c3_Var20 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
opts := newFormSelectOptions(funcs...)
|
||||||
field := form.Field(name)
|
field := form.Field(name)
|
||||||
if field != nil {
|
if field != nil {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"field\"><label class=\"label\" for=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"field\"><label class=\"label\" for=\"")
|
||||||
@ -357,7 +433,7 @@ func FormSelect(form *form.Form, id string, name string, label string, kvOptions
|
|||||||
var templ_7745c5c3_Var21 string
|
var templ_7745c5c3_Var21 string
|
||||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 49, Col: 32}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 125, Col: 32}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@ -370,7 +446,7 @@ func FormSelect(form *form.Form, id string, name string, label string, kvOptions
|
|||||||
var templ_7745c5c3_Var22 string
|
var templ_7745c5c3_Var22 string
|
||||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 49, Col: 42}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 125, Col: 42}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@ -381,6 +457,7 @@ func FormSelect(form *form.Form, id string, name string, label string, kvOptions
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
err, hasErr := form.Error(name)
|
err, hasErr := form.Error(name)
|
||||||
|
value, hasValue := field.Get("value")
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"select is-fullwidth\"><select id=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"select is-fullwidth\"><select id=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
@ -388,7 +465,7 @@ func FormSelect(form *form.Form, id string, name string, label string, kvOptions
|
|||||||
var templ_7745c5c3_Var23 string
|
var templ_7745c5c3_Var23 string
|
||||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 53, Col: 20}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 130, Col: 20}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@ -401,7 +478,7 @@ func FormSelect(form *form.Form, id string, name string, label string, kvOptions
|
|||||||
var templ_7745c5c3_Var24 string
|
var templ_7745c5c3_Var24 string
|
||||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(field.Name())
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(field.Name())
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 53, Col: 42}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 130, Col: 42}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@ -411,7 +488,7 @@ func FormSelect(form *form.Form, id string, name string, label string, kvOptions
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, field.Attrs())
|
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, mergeAttrs(field.Attrs(), opts.Attrs))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -419,63 +496,72 @@ func FormSelect(form *form.Form, id string, name string, label string, kvOptions
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
options := keyValuesToOptions(kvOptions)
|
for _, o := range opts.Options {
|
||||||
for _, o := range options {
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<option")
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<option value=\"")
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if hasValue && value == o.Value {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " value=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var25 string
|
var templ_7745c5c3_Var25 string
|
||||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(o.Value)
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(o.Value)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 56, Col: 30}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 136, Col: 23}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var26 string
|
var templ_7745c5c3_Var26 string
|
||||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(o.Label)
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(o.Label)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 56, Col: 42}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 137, Col: 17}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</option>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</option>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</select></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</select></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if hasErr {
|
if hasErr {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<p class=\"help is-danger\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<p class=\"help is-danger\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var27 string
|
var templ_7745c5c3_Var27 string
|
||||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(err.Message())
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(err.Message())
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 61, Col: 46}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 142, Col: 46}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</p>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</p>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -484,32 +570,14 @@ func FormSelect(form *form.Form, id string, name string, label string, kvOptions
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectOption struct {
|
func mergeAttrs(attrs ...map[string]any) map[string]any {
|
||||||
Value string
|
merged := make(form.Attrs)
|
||||||
Label string
|
for _, a := range attrs {
|
||||||
}
|
for k, v := range a {
|
||||||
|
merged[k] = v
|
||||||
func keyValuesToOptions(kv []string) []SelectOption {
|
|
||||||
if len(kv)%2 != 0 {
|
|
||||||
panic(errors.New("expected pair number of key/values"))
|
|
||||||
}
|
|
||||||
|
|
||||||
options := make([]SelectOption, 0)
|
|
||||||
|
|
||||||
var key string
|
|
||||||
for idx := range kv {
|
|
||||||
if idx%2 == 0 {
|
|
||||||
key = kv[idx]
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
options = append(options, SelectOption{
|
|
||||||
Value: kv[idx],
|
|
||||||
Label: key,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return merged
|
||||||
return options
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
@ -10,9 +10,9 @@ type PageOptionFunc func(opts *PageOptions)
|
|||||||
func WithTitle(title string) PageOptionFunc {
|
func WithTitle(title string) PageOptionFunc {
|
||||||
return func(opts *PageOptions) {
|
return func(opts *PageOptions) {
|
||||||
if title != "" {
|
if title != "" {
|
||||||
opts.Title = title + " | Rkvst"
|
opts.Title = title + " | ClearCase"
|
||||||
} else {
|
} else {
|
||||||
opts.Title = "Rkvst"
|
opts.Title = "ClearCase"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,9 @@ type PageOptionFunc func(opts *PageOptions)
|
|||||||
func WithTitle(title string) PageOptionFunc {
|
func WithTitle(title string) PageOptionFunc {
|
||||||
return func(opts *PageOptions) {
|
return func(opts *PageOptions) {
|
||||||
if title != "" {
|
if title != "" {
|
||||||
opts.Title = title + " | Rkvst"
|
opts.Title = title + " | ClearCase"
|
||||||
} else {
|
} else {
|
||||||
opts.Title = "Rkvst"
|
opts.Title = "ClearCase"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
116
internal/http/handler/webui/issue/component/issue_page.templ
Normal file
116
internal/http/handler/webui/issue/component/issue_page.templ
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/http/form"
|
||||||
|
common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssuePageVModel struct {
|
||||||
|
SummaryForm *form.Form
|
||||||
|
IssueForm *form.Form
|
||||||
|
Projects []*model.Project
|
||||||
|
SelectedProjectID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIssueSummaryForm() *form.Form {
|
||||||
|
return form.New(
|
||||||
|
form.NewField(
|
||||||
|
"project",
|
||||||
|
form.Attrs{},
|
||||||
|
form.NonEmpty("This field should not be empty"),
|
||||||
|
),
|
||||||
|
form.NewField(
|
||||||
|
"summary",
|
||||||
|
form.Attrs{
|
||||||
|
"type": "textarea",
|
||||||
|
"rows": "20",
|
||||||
|
"placeholder": "Write a rapid description of the issue here...",
|
||||||
|
},
|
||||||
|
form.NonEmpty("This field should not be empty"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIssueForm() *form.Form {
|
||||||
|
return form.New(
|
||||||
|
form.NewField(
|
||||||
|
"content",
|
||||||
|
form.Attrs{
|
||||||
|
"type": "textarea",
|
||||||
|
"rows": "25",
|
||||||
|
},
|
||||||
|
form.NonEmpty("This field should not be empty"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
templ IssuePage(vmodel IssuePageVModel) {
|
||||||
|
@common.Page(common.WithTitle("New issue")) {
|
||||||
|
<div class="container is-fluid">
|
||||||
|
<section class="section">
|
||||||
|
<div class="buttons is-right">
|
||||||
|
<a class="button is-medium" href={ common.BaseURL(ctx, common.WithPath("/auth/logout")) }>Logout</a>
|
||||||
|
</div>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<form id="summary-form" action={ common.CurrentURL(ctx) } method="post" hx-disabled-elt="#summary-form textarea, #summary-form select, #summary-form button" hx-indicator="#generation-progress">
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
<h2 class="title is-size-2 level-item">Describe your request</h2>
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="buttons is-right level-item">
|
||||||
|
<button type="submit" class="button is-primary is-large">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-robot"></i>
|
||||||
|
</span>
|
||||||
|
<span>Generate</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<progress id="generation-progress" class="htmx-indicator progress"></progress>
|
||||||
|
@common.FormSelect(
|
||||||
|
vmodel.SummaryForm, "issue-project", "project", "Project",
|
||||||
|
common.WithOptions(projectsToOptions(vmodel.Projects)...),
|
||||||
|
common.WithAttrs(
|
||||||
|
"hx-get", string(common.CurrentURL(ctx, common.WithoutValues("project", "*"))),
|
||||||
|
"hx-target", "body",
|
||||||
|
"hx-push-url", "true",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@common.FormTextarea(vmodel.SummaryForm, "issue-summary", "summary", "Summary")
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
<h2 class="title is-size-2">Generated issue</h2>
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="buttons is-right">
|
||||||
|
<button disabled type="submit" class="button is-primary is-large">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-rocket"></i>
|
||||||
|
</span>
|
||||||
|
<span>Create issue</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@common.FormTextarea(vmodel.IssueForm, "issue-content", "content", "")
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectsToOptions(projects []*model.Project) []string {
|
||||||
|
options := make([]string, 0, len(projects)*2)
|
||||||
|
for _, p := range projects {
|
||||||
|
options = append(options, p.Label, p.ID)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
157
internal/http/handler/webui/issue/component/issue_page_templ.go
Normal file
157
internal/http/handler/webui/issue/component/issue_page_templ.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.819
|
||||||
|
package component
|
||||||
|
|
||||||
|
//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 (
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/http/form"
|
||||||
|
common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssuePageVModel struct {
|
||||||
|
SummaryForm *form.Form
|
||||||
|
IssueForm *form.Form
|
||||||
|
Projects []*model.Project
|
||||||
|
SelectedProjectID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIssueSummaryForm() *form.Form {
|
||||||
|
return form.New(
|
||||||
|
form.NewField(
|
||||||
|
"project",
|
||||||
|
form.Attrs{},
|
||||||
|
form.NonEmpty("This field should not be empty"),
|
||||||
|
),
|
||||||
|
form.NewField(
|
||||||
|
"summary",
|
||||||
|
form.Attrs{
|
||||||
|
"type": "textarea",
|
||||||
|
"rows": "20",
|
||||||
|
"placeholder": "Write a rapid description of the issue here...",
|
||||||
|
},
|
||||||
|
form.NonEmpty("This field should not be empty"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIssueForm() *form.Form {
|
||||||
|
return form.New(
|
||||||
|
form.NewField(
|
||||||
|
"content",
|
||||||
|
form.Attrs{
|
||||||
|
"type": "textarea",
|
||||||
|
"rows": "25",
|
||||||
|
},
|
||||||
|
form.NonEmpty("This field should not be empty"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IssuePage(vmodel IssuePageVModel) 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=\"container is-fluid\"><section class=\"section\"><div class=\"buttons is-right\"><a class=\"button is-medium\" href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 templ.SafeURL = common.BaseURL(ctx, common.WithPath("/auth/logout"))
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">Logout</a></div><div class=\"columns\"><div class=\"column\"><form id=\"summary-form\" action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 templ.SafeURL = common.CurrentURL(ctx)
|
||||||
|
_, 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, 3, "\" method=\"post\" hx-disabled-elt=\"#summary-form textarea, #summary-form select, #summary-form button\" hx-indicator=\"#generation-progress\"><div class=\"level\"><div class=\"level-left\"><h2 class=\"title is-size-2 level-item\">Describe your request</h2></div><div class=\"level-right\"><div class=\"buttons is-right level-item\"><button type=\"submit\" class=\"button is-primary is-large\"><span class=\"icon\"><i class=\"fa fa-robot\"></i></span> <span>Generate</span></button></div></div></div><progress id=\"generation-progress\" class=\"htmx-indicator progress\"></progress>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = common.FormSelect(
|
||||||
|
vmodel.SummaryForm, "issue-project", "project", "Project",
|
||||||
|
common.WithOptions(projectsToOptions(vmodel.Projects)...),
|
||||||
|
common.WithAttrs(
|
||||||
|
"hx-get", string(common.CurrentURL(ctx, common.WithoutValues("project", "*"))),
|
||||||
|
"hx-target", "body",
|
||||||
|
"hx-push-url", "true",
|
||||||
|
),
|
||||||
|
).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = common.FormTextarea(vmodel.SummaryForm, "issue-summary", "summary", "Summary").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</form></div><div class=\"column\"><div class=\"level\"><div class=\"level-left\"><h2 class=\"title is-size-2\">Generated issue</h2></div><div class=\"level-right\"><div class=\"buttons is-right\"><button disabled type=\"submit\" class=\"button is-primary is-large\"><span class=\"icon\"><i class=\"fa fa-rocket\"></i></span> <span>Create issue</span></button></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = common.FormTextarea(vmodel.IssueForm, "issue-content", "content", "").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div></section></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = common.Page(common.WithTitle("New issue")).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectsToOptions(projects []*model.Project) []string {
|
||||||
|
options := make([]string, 0, len(projects)*2)
|
||||||
|
for _, p := range projects {
|
||||||
|
options = append(options, p.Label, p.ID)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
@ -1,8 +0,0 @@
|
|||||||
package component
|
|
||||||
|
|
||||||
import common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
|
||||||
|
|
||||||
templ IssuePage(title string) {
|
|
||||||
@common.Page(common.WithTitle(title)) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.819
|
|
||||||
package component
|
|
||||||
|
|
||||||
//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 common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
|
||||||
|
|
||||||
func IssuePage(title 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)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
templ_7745c5c3_Err = common.Page(common.WithTitle(title)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
@ -2,10 +2,13 @@ package issue
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
|
issueManager *service.IssueManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler.
|
// ServeHTTP implements http.Handler.
|
||||||
@ -13,12 +16,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.mux.ServeHTTP(w, r)
|
h.mux.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler() *Handler {
|
func NewHandler(issueManager *service.IssueManager) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
mux: http.NewServeMux(),
|
mux: http.NewServeMux(),
|
||||||
|
issueManager: issueManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.mux.HandleFunc("GET /", h.getIssuePage)
|
h.mux.HandleFunc("GET /", h.getIssuePage)
|
||||||
|
h.mux.HandleFunc("POST /", h.handleIssueSummary)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,128 @@
|
|||||||
package issue
|
package issue
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/service"
|
||||||
|
httpCtx "forge.cadoles.com/wpetit/clearcase/internal/http/context"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/http/form"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/issue/component"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/http/url"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
func (h *Handler) getIssuePage(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) getIssuePage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vmodel, err := h.fillIssuePageVModel(r)
|
||||||
|
if err != nil {
|
||||||
|
h.handleError(w, r, errors.WithStack(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
issue := component.IssuePage(*vmodel)
|
||||||
|
templ.Handler(issue).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) fillIssuePageVModel(r *http.Request) (*component.IssuePageVModel, error) {
|
||||||
|
vmodel := &component.IssuePageVModel{
|
||||||
|
SummaryForm: component.NewIssueSummaryForm(),
|
||||||
|
IssueForm: component.NewIssueForm(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := common.FillViewModel(
|
||||||
|
r.Context(), vmodel, r,
|
||||||
|
h.fillIssuePageProjects,
|
||||||
|
h.fillIssuePageSelectedProject,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vmodel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) fillIssuePageProjects(ctx context.Context, vmodel *component.IssuePageVModel, r *http.Request) error {
|
||||||
|
user := httpCtx.User(ctx)
|
||||||
|
|
||||||
|
projects, err := h.issueManager.GetUserProjects(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vmodel.Projects = projects
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) fillIssuePageSelectedProject(ctx context.Context, vmodel *component.IssuePageVModel, r *http.Request) error {
|
||||||
|
project := r.URL.Query().Get("project")
|
||||||
|
if project == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vmodel.SelectedProjectID = project
|
||||||
|
vmodel.SummaryForm.Field("project").Set("value", project)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleIssueSummary(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
issueSummaryForm := component.NewIssueSummaryForm()
|
||||||
|
|
||||||
|
if err := issueSummaryForm.Handle(r); err != nil {
|
||||||
|
h.handleError(w, r, errors.WithStack(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vmodel, err := h.fillIssuePageVModel(r)
|
||||||
|
if err != nil {
|
||||||
|
h.handleError(w, r, errors.WithStack(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vmodel.SummaryForm = issueSummaryForm
|
||||||
|
|
||||||
|
if errs := issueSummaryForm.Validate(); errs != nil {
|
||||||
|
page := component.IssuePage(*vmodel)
|
||||||
|
templ.Handler(page).ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID, err := form.FormFieldAttr[string](issueSummaryForm, "project", "value")
|
||||||
|
if err != nil {
|
||||||
|
h.handleError(w, r, errors.WithStack(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err := form.FormFieldAttr[string](issueSummaryForm, "summary", "value")
|
||||||
|
if err != nil {
|
||||||
|
h.handleError(w, r, errors.WithStack(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
issueContent, err := h.issueManager.GenerateIssue(ctx, httpCtx.User(ctx), projectID, summary)
|
||||||
|
if err != nil {
|
||||||
|
h.handleError(w, r, errors.WithStack(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vmodel.IssueForm.Field("content").Set("value", issueContent)
|
||||||
|
|
||||||
|
page := component.IssuePage(*vmodel)
|
||||||
|
templ.Handler(page).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
if errors.Is(err, service.ErrForgeNotAvailable) {
|
||||||
|
baseURL := url.Mutate(httpCtx.BaseURL(r.Context()), url.WithPath("/auth/logout"))
|
||||||
|
http.Redirect(w, r, baseURL.String(), http.StatusTemporaryRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.HandleError(w, r, errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/markbates/goth"
|
"github.com/markbates/goth"
|
||||||
"github.com/markbates/goth/gothic"
|
"github.com/markbates/goth/gothic"
|
||||||
|
"github.com/markbates/goth/providers/gitea"
|
||||||
"github.com/markbates/goth/providers/github"
|
"github.com/markbates/goth/providers/github"
|
||||||
"github.com/markbates/goth/providers/google"
|
"github.com/markbates/goth/providers/google"
|
||||||
"github.com/markbates/goth/providers/openidConnect"
|
"github.com/markbates/goth/providers/openidConnect"
|
||||||
@ -79,6 +80,26 @@ func NewAuthHandlerFromConfig(ctx context.Context, conf *config.Config) (*auth.H
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conf.Auth.Providers.Gitea.Key != "" && conf.Auth.Providers.Gitea.Secret != "" {
|
||||||
|
giteaProvider := gitea.NewCustomisedURL(
|
||||||
|
conf.Auth.Providers.Gitea.Key,
|
||||||
|
conf.Auth.Providers.Gitea.Secret,
|
||||||
|
fmt.Sprintf("%s/auth/providers/gitea/callback", conf.HTTP.BaseURL),
|
||||||
|
conf.Auth.Providers.Gitea.AuthURL,
|
||||||
|
conf.Auth.Providers.Gitea.TokenURL,
|
||||||
|
conf.Auth.Providers.Gitea.ProfileURL,
|
||||||
|
conf.Auth.Providers.Gitea.Scopes...,
|
||||||
|
)
|
||||||
|
|
||||||
|
gothProviders = append(gothProviders, giteaProvider)
|
||||||
|
|
||||||
|
providers = append(providers, auth.Provider{
|
||||||
|
ID: giteaProvider.Name(),
|
||||||
|
Label: conf.Auth.Providers.Gitea.Label,
|
||||||
|
Icon: "fa-git-alt",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if conf.Auth.Providers.OIDC.Key != "" && conf.Auth.Providers.OIDC.Secret != "" {
|
if conf.Auth.Providers.OIDC.Key != "" && conf.Auth.Providers.OIDC.Secret != "" {
|
||||||
oidcProvider, err := openidConnect.New(
|
oidcProvider, err := openidConnect.New(
|
||||||
conf.Auth.Providers.OIDC.Key,
|
conf.Auth.Providers.OIDC.Key,
|
||||||
@ -107,13 +128,6 @@ func NewAuthHandlerFromConfig(ctx context.Context, conf *config.Config) (*auth.H
|
|||||||
auth.WithProviders(providers...),
|
auth.WithProviders(providers...),
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Auth.DefaultAdmin.Email != "" && conf.Auth.DefaultAdmin.Provider != "" {
|
|
||||||
opts = append(opts, auth.WithDefaultAdmin(auth.DefaultAdmin{
|
|
||||||
Provider: conf.Auth.DefaultAdmin.Provider,
|
|
||||||
Email: conf.Auth.DefaultAdmin.Email,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
auth := auth.NewHandler(
|
auth := auth.NewHandler(
|
||||||
sessionStore,
|
sessionStore,
|
||||||
opts...,
|
opts...,
|
||||||
|
@ -4,9 +4,40 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"forge.cadoles.com/wpetit/clearcase/internal/config"
|
"forge.cadoles.com/wpetit/clearcase/internal/config"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/port"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/service"
|
||||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/issue"
|
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/issue"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewIssueHandlerFromConfig(ctx context.Context, conf *config.Config) (*issue.Handler, error) {
|
func NewIssueHandlerFromConfig(ctx context.Context, conf *config.Config) (*issue.Handler, error) {
|
||||||
return issue.NewHandler(), nil
|
issueManager, err := NewIssueManagerFromConfig(ctx, conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return issue.NewHandler(issueManager), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type authProviderBasedForgeFactory struct {
|
||||||
|
provider string
|
||||||
|
create func(ctx context.Context, user *model.User) (port.Forge, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create implements service.ForgeFactory.
|
||||||
|
func (a *authProviderBasedForgeFactory) Create(ctx context.Context, user *model.User) (port.Forge, error) {
|
||||||
|
forge, err := a.create(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return forge, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match implements service.ForgeFactory.
|
||||||
|
func (a *authProviderBasedForgeFactory) Match(user *model.User) bool {
|
||||||
|
return user.Provider == a.provider
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ service.ForgeFactory = &authProviderBasedForgeFactory{}
|
||||||
|
60
internal/setup/issue_manager.go
Normal file
60
internal/setup/issue_manager.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
giteaAdapter "forge.cadoles.com/wpetit/clearcase/internal/adapter/gitea"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/config"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/port"
|
||||||
|
"forge.cadoles.com/wpetit/clearcase/internal/core/service"
|
||||||
|
"github.com/bornholm/genai/llm/provider"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
_ "github.com/bornholm/genai/llm/provider/openai"
|
||||||
|
_ "github.com/bornholm/genai/llm/provider/openrouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewIssueManagerFromConfig(ctx context.Context, conf *config.Config) (*service.IssueManager, error) {
|
||||||
|
llmCtx := provider.FromMap(ctx, "", map[string]string{
|
||||||
|
string(provider.ContextKeyAPIBaseURL): conf.LLM.Provider.BaseURL,
|
||||||
|
string(provider.ContextKeyAPIKey): conf.LLM.Provider.Key,
|
||||||
|
string(provider.ContextKeyModel): conf.LLM.Provider.Model,
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := provider.Create(llmCtx, provider.Name(conf.LLM.Provider.Name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not create llm client '%s'", conf.LLM.Provider.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
forgeFactories := make([]service.ForgeFactory, 0)
|
||||||
|
|
||||||
|
if conf.Auth.Providers.Gitea.Key != "" && conf.Auth.Providers.Gitea.Secret != "" {
|
||||||
|
baseURL, err := url.Parse(conf.Auth.Providers.Gitea.AuthURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not parse gitea auth url '%s'", conf.Auth.Providers.Gitea.AuthURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL.Path = ""
|
||||||
|
|
||||||
|
forgeFactories = append(forgeFactories, &authProviderBasedForgeFactory{
|
||||||
|
provider: "gitea",
|
||||||
|
create: func(ctx context.Context, user *model.User) (port.Forge, error) {
|
||||||
|
client, err := gitea.NewClient(baseURL.String(), gitea.SetToken(user.AccessToken))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forge := giteaAdapter.NewForge(client)
|
||||||
|
|
||||||
|
return forge, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
issueManager := service.NewIssueManager(client, forgeFactories...)
|
||||||
|
|
||||||
|
return issueManager, nil
|
||||||
|
}
|
@ -25,7 +25,7 @@ func NewWebUIHandlerFromConfig(ctx context.Context, conf *config.Config) (*webui
|
|||||||
// Configure issue handler
|
// Configure issue handler
|
||||||
issueHandler, err := NewIssueHandlerFromConfig(ctx, conf)
|
issueHandler, err := NewIssueHandlerFromConfig(ctx, conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "could not configure explorer handler from config")
|
return nil, errors.Wrap(err, "could not configure issue handler from config")
|
||||||
}
|
}
|
||||||
|
|
||||||
opts = append(opts, webui.WithMount("/", authMiddleware(issueHandler)))
|
opts = append(opts, webui.WithMount("/", authMiddleware(issueHandler)))
|
||||||
|
@ -21,18 +21,7 @@ RUN --mount=type=cache,target=/go/pkg/mod/ \
|
|||||||
|
|
||||||
FROM alpine:3.21 AS runtime
|
FROM alpine:3.21 AS runtime
|
||||||
|
|
||||||
RUN apk add \
|
RUN apk add ca-certificates \
|
||||||
ca-certificates \
|
|
||||||
openssl \
|
|
||||||
openjdk21-jdk \
|
|
||||||
msttcorefonts-installer \
|
|
||||||
ttf-dejavu \
|
|
||||||
fontconfig \
|
|
||||||
tesseract-ocr \
|
|
||||||
tesseract-ocr-data-eng \
|
|
||||||
tesseract-ocr-data-fra \
|
|
||||||
&& update-ms-fonts \
|
|
||||||
&& fc-cache -f -v \
|
|
||||||
&& update-ca-certificates
|
&& update-ca-certificates
|
||||||
|
|
||||||
COPY --from=build /src/bin/clearcase /usr/local/bin/clearcase
|
COPY --from=build /src/bin/clearcase /usr/local/bin/clearcase
|
||||||
|
@ -5,4 +5,4 @@ docker-image:
|
|||||||
docker build -f misc/docker/Dockerfile -t $(DOCKER_STANDALONE_IMAGE_NAME):$(DOCKER_STANDALONE_IMAGE_TAG) .
|
docker build -f misc/docker/Dockerfile -t $(DOCKER_STANDALONE_IMAGE_NAME):$(DOCKER_STANDALONE_IMAGE_TAG) .
|
||||||
|
|
||||||
docker-run:
|
docker-run:
|
||||||
docker run -it --rm -p 3000:3000 --name rkvst --tmpfs /tmp --env-file .env $(DOCKER_STANDALONE_IMAGE_NAME):$(DOCKER_STANDALONE_IMAGE_TAG)
|
docker run -it --rm -p 3000:3000 --name clearcase --tmpfs /tmp --env-file .env $(DOCKER_STANDALONE_IMAGE_NAME):$(DOCKER_STANDALONE_IMAGE_TAG)
|
5
misc/dokku/dokku.mk
Normal file
5
misc/dokku/dokku.mk
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
DOKKU_APP ?= clearcase
|
||||||
|
DOKKU_DEPLOY_URL ?= dokku@dev.lookingfora.name
|
||||||
|
|
||||||
|
dokku-deploy:
|
||||||
|
git push $(DOKKU_DEPLOY_URL):$(DOKKU_APP) $(shell git rev-parse HEAD):refs/heads/master --force
|
Loading…
x
Reference in New Issue
Block a user