feat: introduce React web frontend and migrate home + 404 pages (#8276)

This commit is contained in:
ᴊᴏᴇ ᴄʜᴇɴ
2026-05-20 17:45:31 -04:00
committed by GitHub
parent a3c9f4acef
commit b67c13c6bb
95 changed files with 6362 additions and 560 deletions
+5
View File
@@ -11,5 +11,10 @@ scripts/**
.gitignore
Dockerfile*
gogs
node_modules
**/node_modules
public/dist
**/*.tsbuildinfo
**/.vite
!Taskfile.yml
+14 -3
View File
@@ -46,6 +46,17 @@ jobs:
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: 1.26.x
- name: Set up pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
- name: Build web assets
run: |
pnpm install --frozen-lockfile
pnpm --filter gogs-web run build
- name: Determine version
id: version
run: |
@@ -70,9 +81,9 @@ jobs:
BINARY_NAME="gogs.exe"
fi
TAGS_FLAG=""
TAGS="prod"
if [ -n "${{ matrix.tags }}" ]; then
TAGS_FLAG="-tags ${{ matrix.tags }}"
TAGS="$TAGS ${{ matrix.tags }}"
fi
go build -v \
@@ -80,7 +91,7 @@ jobs:
-X \"gogs.io/gogs/internal/conf.BuildTime=$(date -u '+%Y-%m-%d %I:%M:%S %Z')\"
-X \"gogs.io/gogs/internal/conf.BuildCommit=$(git rev-parse HEAD)\"
" \
$TAGS_FLAG \
-tags "$TAGS" \
-trimpath -o "$BINARY_NAME" ./cmd/gogs
- name: Prepare archive contents
run: |
+54
View File
@@ -0,0 +1,54 @@
name: Web
on:
push:
branches:
- main
- 'release/**'
paths:
- 'web/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'package.json'
- 'conf/locale/locale_*.ini'
- '.github/workflows/web.yml'
pull_request:
paths:
- 'web/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'package.json'
- 'conf/locale/locale_*.ini'
- '.github/workflows/web.yml'
permissions:
contents: read
jobs:
web:
name: Lint and build
runs-on: ubuntu-latest
env:
MOON_COLOR: "true"
FORCE_COLOR: "1"
steps:
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
- name: Set up pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
- name: Set up moon
uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4
with:
auto-install: false
- name: Lint
run: moon run web:lint
- name: Build
run: moon run web:build
- name: Check for uncommitted changes
run: git diff --exit-code --stat
+4 -2
View File
@@ -1,6 +1,9 @@
# Build artifacts
# Build artifacts and cache
.bin/
dist/
.moon/cache/
.task/
node_modules/
# Runtime data
log/
@@ -9,7 +12,6 @@ data/
# Configuration and application files
.idea/
.task/
.envrc
# System junk
+5
View File
@@ -0,0 +1,5 @@
$schema: "https://moonrepo.dev/schemas/workspace.json"
projects:
gogs: "."
web: "web"
+10 -3
View File
@@ -9,6 +9,8 @@ This applies to all texts, including but not limited to UI, documentation, code
- Use sentence case. Preserve original casing for brand names.
- End with a period for a full sentence.
- Never use em dashes (`—`) or en dashes (``) in prose. Rewrite the sentence with a comma, period, colon, or parentheses instead. Exception: em/en dashes are allowed as visual separators in UI design (e.g., between a title and description, in a terminal prompt label) where they function as a graphic element rather than punctuation.
- Do not overuse semicolons. Two short sentences are almost always clearer than one sentence joined by a semicolon. Reserve the semicolon for the rare case where the two clauses are so tightly coupled that splitting them loses meaning, never as a default em-dash replacement or a way to chain related thoughts.
- Do not add comments that repeat what the code is doing, always prefer more descriptive names. Do add comments for intentions that aren't obvious via reading the code alone. This rule takes precedence over matching existing patterns.
## Coding guidelines
@@ -16,11 +18,16 @@ This applies to all texts, including but not limited to UI, documentation, code
- Use `github.com/cockroachdb/errors` for error handling.
- Use `github.com/stretchr/testify` for assertions in tests. Be mindful about the choice of `require` and `assert`, the former should be used when the test cannot proceed meaningfully after a failed assertion.
## UI guidelines
- Design mobile-friendly. Every UI must look and work well on narrow viewports before adding desktop refinements via responsive breakpoints. Test at ~375px width before considering a UI done.
- Meet WCAG 2.2 AA at minimum. Specifically: every interactive control has a discernible accessible name (visible label or `aria-label`); color is never the sole carrier of information (pair with text, icon, or shape); text and meaningful icons meet 4.5:1 contrast against their background (3:1 for large text and UI components); focus is always visible and never trapped; touch targets are at least 24×24 CSS px (40×40 preferred). When unsure, lean toward more contrast, larger targets, and explicit labels.
- For work under `web/`, follow the patterns in [`web/DESIGN.md`](web/DESIGN.md) (typography, color hierarchy, surface chrome, file naming, accessibility specifics). Update that doc when a pattern is used in two places.
## Build instructions
- Prefer `task` command over vanilla `go` command when available. Use `--force` flag when necessary.
- Run `task lint` after every time you finish changing code, and fix all linter errors.
- Run `go mod tidy` after every time you change `go.mod`, do not manually edit `go.sum` file.
- Prefer `moon run <project>:<task>` over vanilla `go` or `pnpm` commands when available (e.g. `moon run gogs:build`, `moon run web:dev`). Pass `--force` to bypass cache when necessary.
- Run `moon run gogs:lint` after every time you finish changing Go code, and `moon run web:lint` after changing frontend code; fix all linter errors.
## Tool-use guidance
+11 -1
View File
@@ -1,3 +1,12 @@
FROM --platform=$BUILDPLATFORM node:24-alpine AS webbuilder
RUN corepack enable
WORKDIR /src
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY web ./web
COPY conf/locale ./conf/locale
RUN pnpm install --frozen-lockfile
RUN pnpm --filter gogs-web run build
FROM golang:1.26-alpine3.23 AS binarybuilder
RUN apk --no-cache --no-progress add --virtual \
build-deps \
@@ -7,9 +16,10 @@ RUN apk --no-cache --no-progress add --virtual \
WORKDIR /gogs.io/gogs
COPY . .
COPY --from=webbuilder /src/public/dist ./public/dist
RUN ./docker/build/install-task.sh
RUN TAGS="cert pam" task build
RUN TAGS="pam prod" task build
FROM alpine:3.23
RUN apk --no-cache --no-progress add \
+11 -1
View File
@@ -1,3 +1,12 @@
FROM --platform=$BUILDPLATFORM node:24-alpine AS webbuilder
RUN corepack enable
WORKDIR /src
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY web ./web
COPY conf/locale ./conf/locale
RUN pnpm install --frozen-lockfile
RUN pnpm --filter gogs-web run build
FROM golang:1.26-alpine3.23 AS binarybuilder
RUN apk --no-cache --no-progress add --virtual \
build-deps \
@@ -7,9 +16,10 @@ RUN apk --no-cache --no-progress add --virtual \
WORKDIR /gogs.io/gogs
COPY . .
COPY --from=webbuilder /src/public/dist ./public/dist
RUN ./docker/build/install-task.sh
RUN TAGS="cert pam" task build
RUN TAGS="pam prod" task build
FROM alpine:3.23
+13
View File
@@ -30,6 +30,19 @@ func configFromLineage(cmd *cli.Command) string {
return ""
}
func intFlag(name string, value int, usage string) *cli.IntFlag {
parts := strings.SplitN(name, ", ", 2)
f := &cli.IntFlag{
Name: parts[0],
Value: value,
Usage: usage,
}
if len(parts) > 1 {
f.Aliases = []string{parts[1]}
}
return f
}
func boolFlag(name, usage string) *cli.BoolFlag {
parts := strings.SplitN(name, ", ", 2)
f := &cli.BoolFlag{
+205 -137
View File
@@ -1,4 +1,4 @@
package main
package web
import (
stdctx "context"
@@ -10,8 +10,11 @@ import (
"net/http/fcgi"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/flamego/flamego"
"github.com/go-macaron/binding"
"github.com/go-macaron/cache"
"github.com/go-macaron/captcha"
@@ -20,7 +23,6 @@ import (
"github.com/go-macaron/i18n"
"github.com/go-macaron/session"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/urfave/cli/v3"
"gopkg.in/macaron.v1"
log "unknwon.dev/clog/v2"
@@ -44,135 +46,22 @@ import (
"gogs.io/gogs/templates"
)
var webCommand = cli.Command{
Name: "web",
Usage: "Start web server",
Description: `Gogs web server is the only thing you need to run,
and it takes care of all the other things for you`,
Action: runWeb,
Flags: []cli.Flag{
stringFlag("port, p", "3000", "Temporary port number to prevent conflict"),
stringFlag("config, c", filepath.Join(conf.CustomDir(), "conf", "app.ini"), "Custom configuration file path"),
},
}
// newMacaron initializes Macaron instance.
func newMacaron() *macaron.Macaron {
m := macaron.New()
if !conf.Server.DisableRouterLog {
m.Use(macaron.Logger())
}
m.Use(macaron.Recovery())
if conf.Server.EnableGzip {
m.Use(gzip.Gziper())
}
if conf.Server.Protocol == "fcgi" {
m.SetURLPrefix(conf.Server.Subpath)
}
// Register custom middleware first to make it possible to override files under "public".
m.Use(macaron.Static(
filepath.Join(conf.CustomDir(), "public"),
macaron.StaticOptions{
SkipLogging: conf.Server.DisableRouterLog,
},
))
var publicFs http.FileSystem
if !conf.Server.LoadAssetsFromDisk {
publicFs = http.FS(public.Files)
}
m.Use(macaron.Static(
filepath.Join(conf.WorkDir(), "public"),
macaron.StaticOptions{
ETag: true,
SkipLogging: conf.Server.DisableRouterLog,
FileSystem: publicFs,
},
))
m.Use(macaron.Static(
conf.Picture.AvatarUploadPath,
macaron.StaticOptions{
ETag: true,
Prefix: conf.UsersAvatarPathPrefix,
SkipLogging: conf.Server.DisableRouterLog,
},
))
m.Use(macaron.Static(
conf.Picture.RepositoryAvatarUploadPath,
macaron.StaticOptions{
ETag: true,
Prefix: database.RepoAvatarURLPrefix,
SkipLogging: conf.Server.DisableRouterLog,
},
))
customDir := filepath.Join(conf.CustomDir(), "templates")
renderOpt := macaron.RenderOptions{
Directory: filepath.Join(conf.WorkDir(), "templates"),
AppendDirectories: []string{customDir},
Funcs: template.FuncMap(),
IndentJSON: macaron.Env != macaron.PROD,
}
if !conf.Server.LoadAssetsFromDisk {
renderOpt.TemplateFileSystem = templates.NewTemplateFileSystem("", customDir)
}
m.Use(macaron.Renderer(renderOpt))
localeNames, err := embedConf.FileNames("locale")
// Run starts the web server with the given configuration path and port override.
func Run(configPath string, portOverride int) error {
err := route.GlobalInit(configPath)
if err != nil {
log.Fatal("Failed to list locale files: %v", err)
return errors.Wrap(err, "initialize application")
}
localeFiles := make(map[string][]byte)
for _, name := range localeNames {
localeFiles[name], err = embedConf.Files.ReadFile("locale/" + name)
if err != nil {
log.Fatal("Failed to read locale file %q: %v", name, err)
}
}
m.Use(i18n.I18n(i18n.Options{
SubURL: conf.Server.Subpath,
Files: localeFiles,
CustomDirectory: filepath.Join(conf.CustomDir(), "conf", "locale"),
Langs: conf.I18n.Langs,
Names: conf.I18n.Names,
DefaultLang: "en-US",
Redirect: true,
}))
m.Use(cache.Cacher(cache.Options{
Adapter: conf.Cache.Adapter,
AdapterConfig: conf.Cache.Host,
Interval: conf.Cache.Interval,
}))
m.Use(captcha.Captchaer(captcha.Options{
SubURL: conf.Server.Subpath,
}))
m.Route("/healthcheck", http.MethodHead+","+http.MethodGet, healthCheck)
return m
}
func healthCheck(w http.ResponseWriter, r *http.Request) {
if err := database.Ping(); err != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = fmt.Fprintf(w, "* Database connection: %s\n", err)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
_, _ = w.Write([]byte("* Database connection: OK\n"))
}
func runWeb(_ stdctx.Context, cmd *cli.Command) error {
err := route.GlobalInit(configFromLineage(cmd))
m, err := newMacaron()
if err != nil {
log.Fatal("Failed to initialize application: %v", err)
return errors.Wrap(err, "initialize macaron")
}
m := newMacaron()
webHandler, err := newRoutingHandler()
if err != nil {
return errors.Wrap(err, "initialize web handler")
}
reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
ignSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: conf.Auth.RequireSigninView})
@@ -661,7 +550,7 @@ func runWeb(_ stdctx.Context, cmd *cli.Command) error {
SetCookie: true,
Secure: conf.Server.URL.Scheme == "https",
}),
context.Contexter(context.NewStore()),
context.Contexter(context.NewStore(), webHandler),
)
// ***************************
@@ -675,7 +564,18 @@ func runWeb(_ stdctx.Context, cmd *cli.Command) error {
lfs.RegisterRoutes(m.Router)
})
m.Route("/*", "GET,POST,OPTIONS", context.ServeGoGet(), repo.HTTPContexter(repo.NewStore()), repo.HTTP)
gitHTTP := []macaron.Handler{context.ServeGoGet(), repo.HTTPContexter(repo.NewStore()), repo.HTTP}
m.Route("/info/refs", "GET,OPTIONS", gitHTTP...)
m.Route("/HEAD", "GET,OPTIONS", gitHTTP...)
m.Route("/git-upload-pack", "POST,OPTIONS", gitHTTP...)
m.Route("/git-receive-pack", "POST,OPTIONS", gitHTTP...)
m.Route("/objects/info/alternates", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/info/http-alternates", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/info/packs", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/info/*", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/:prefix([0-9a-f]{2})/:suffix([0-9a-f]{38})", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/pack/pack-:sha([0-9a-f]{40}).pack", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/pack/pack-:sha([0-9a-f]{40}).idx", "GET,OPTIONS", gitHTTP...)
})
// ***************************
@@ -702,20 +602,46 @@ func runWeb(_ stdctx.Context, cmd *cli.Command) error {
}
})
m.NotFound(route.NotFound)
// True 404s never reach context.Contexter, so populate WebContext
// explicitly. Without this, subpath deployments would emit a shell with
// root-relative asset URLs that the browser cannot resolve. Read the
// language preference straight from the cookie that the i18n middleware
// previously wrote, but only accept values that match a configured
// locale. The cookie value lands in the HTML via raw string substitution
// in renderIndex, so an unvalidated value would let an attacker who can
// set this cookie inject markup into the 404 shell.
langAllowed := make(map[string]struct{}, len(conf.I18n.Langs))
for _, lang := range conf.I18n.Langs {
langAllowed[lang] = struct{}{}
}
m.NotFound(func(w http.ResponseWriter, r *http.Request) {
lang := "en-US"
if c, err := r.Cookie("lang"); err == nil {
if _, ok := langAllowed[c.Value]; ok {
lang = c.Value
}
}
ctx := stdctx.WithValue(r.Context(), context.WebContextKey{}, context.WebContext{
Lang: lang,
SubURL: conf.Server.Subpath,
Status: http.StatusNotFound,
})
webHandler.ServeHTTP(w, r.WithContext(ctx))
})
// Flag for port number in case first time run conflict.
if cmd.IsSet("port") {
conf.Server.URL.Host = strings.Replace(conf.Server.URL.Host, ":"+conf.Server.URL.Port(), ":"+cmd.String("port"), 1)
if portOverride > 0 {
port := strconv.Itoa(portOverride)
conf.Server.URL.Host = strings.Replace(conf.Server.URL.Host, ":"+conf.Server.URL.Port(), ":"+port, 1)
conf.Server.ExternalURL = conf.Server.URL.String()
conf.Server.HTTPPort = cmd.String("port")
conf.Server.HTTPPort = portOverride
}
var listenAddr string
if conf.Server.Protocol == "unix" {
listenAddr = conf.Server.HTTPAddr
} else {
listenAddr = fmt.Sprintf("%s:%s", conf.Server.HTTPAddr, conf.Server.HTTPPort)
listenAddr = fmt.Sprintf("%s:%d", conf.Server.HTTPAddr, conf.Server.HTTPPort)
}
log.Info("Available on %s", conf.Server.ExternalURL)
@@ -760,30 +686,172 @@ func runWeb(_ stdctx.Context, cmd *cli.Command) error {
if osx.Exist(listenAddr) {
err = os.Remove(listenAddr)
if err != nil {
log.Fatal("Failed to remove existing Unix domain socket: %v", err)
return errors.Wrap(err, "remove existing Unix domain socket")
}
}
var listener *net.UnixListener
listener, err = net.ListenUnix("unix", &net.UnixAddr{Name: listenAddr, Net: "unix"})
if err != nil {
log.Fatal("Failed to listen on Unix networks: %v", err)
return errors.Wrap(err, "listen on Unix network")
}
// FIXME: add proper implementation of signal capture on all protocols
// execute this on SIGTERM or SIGINT: listener.Close()
if err = os.Chmod(listenAddr, conf.Server.UnixSocketMode); err != nil {
log.Fatal("Failed to change permission of Unix domain socket: %v", err)
return errors.Wrap(err, "change permission of Unix domain socket")
}
err = http.Serve(listener, m)
default:
log.Fatal("Unexpected server protocol: %s", conf.Server.Protocol)
return errors.Newf("unexpected server protocol: %s", conf.Server.Protocol)
}
if err != nil {
log.Fatal("Failed to start server: %v", err)
return errors.Wrap(err, "start server")
}
return nil
}
func newRoutingHandler() (http.Handler, error) {
f := flamego.New()
f.Use(flamego.Recovery())
if err := mountWebRoutes(f); err != nil {
return nil, errors.Wrap(err, "mount web routes")
}
return f, nil
}
// newMacaron initializes Macaron instance.
func newMacaron() (*macaron.Macaron, error) {
m := macaron.New()
if !conf.Server.DisableRouterLog {
m.Use(macaron.Logger())
}
m.Use(macaron.Recovery())
if conf.Server.EnableGzip {
m.Use(gzip.Gziper())
}
if conf.Server.Protocol == "fcgi" {
m.SetURLPrefix(conf.Server.Subpath)
}
// Register custom middleware first to make it possible to override files under "public".
m.Use(macaron.Static(
filepath.Join(conf.CustomDir(), "public"),
macaron.StaticOptions{
SkipLogging: conf.Server.DisableRouterLog,
},
))
var publicFs http.FileSystem
if !conf.Server.LoadAssetsFromDisk {
publicFs = http.FS(public.Files)
}
m.Use(macaron.Static(
filepath.Join(conf.WorkDir(), "public"),
macaron.StaticOptions{
ETag: true,
SkipLogging: conf.Server.DisableRouterLog,
FileSystem: publicFs,
},
))
m.Use(macaron.Static(
conf.Picture.AvatarUploadPath,
macaron.StaticOptions{
ETag: true,
Prefix: conf.UsersAvatarPathPrefix,
SkipLogging: conf.Server.DisableRouterLog,
},
))
m.Use(macaron.Static(
conf.Picture.RepositoryAvatarUploadPath,
macaron.StaticOptions{
ETag: true,
Prefix: database.RepoAvatarURLPrefix,
SkipLogging: conf.Server.DisableRouterLog,
},
))
customDir := filepath.Join(conf.CustomDir(), "templates")
renderOpt := macaron.RenderOptions{
Directory: filepath.Join(conf.WorkDir(), "templates"),
AppendDirectories: []string{customDir},
Funcs: template.FuncMap(),
IndentJSON: macaron.Env != macaron.PROD,
}
if !conf.Server.LoadAssetsFromDisk {
renderOpt.TemplateFileSystem = templates.NewTemplateFileSystem("", customDir)
}
m.Use(macaron.Renderer(renderOpt))
localeNames, err := embedConf.FileNames("locale")
if err != nil {
return nil, errors.Wrap(err, "list locale files")
}
localeFiles := make(map[string][]byte)
for _, name := range localeNames {
localeFiles[name], err = embedConf.Files.ReadFile("locale/" + name)
if err != nil {
return nil, errors.Wrapf(err, "read locale file %q", name)
}
}
m.Use(i18n.I18n(i18n.Options{
SubURL: conf.Server.Subpath,
Files: localeFiles,
CustomDirectory: filepath.Join(conf.CustomDir(), "conf", "locale"),
Langs: conf.I18n.Langs,
Names: conf.I18n.Names,
DefaultLang: "en-US",
Redirect: true,
}))
m.Use(cache.Cacher(cache.Options{
Adapter: conf.Cache.Adapter,
AdapterConfig: conf.Cache.Host,
Interval: conf.Cache.Interval,
}))
m.Use(captcha.Captchaer(captcha.Options{
SubURL: conf.Server.Subpath,
}))
m.Route("/healthcheck", http.MethodHead+","+http.MethodGet, healthCheck)
return m, nil
}
// renderIndex applies per-request substitutions to the index.html shell:
// the {{.Lang}} and {{.SubURL}} placeholders, plus the deployment subpath
// for asset URLs Vite bakes into the bundle. Returns a fresh byte slice.
func renderIndex(index []byte, wc context.WebContext) []byte {
pairs := []string{
"{{.Lang}}", wc.Lang,
"{{.SubURL}}", wc.SubURL,
}
if wc.SubURL != "" {
// Vite bakes absolute root paths into the bundle output (e.g.
// src="/assets/index-xxx.js"). Prefix them with the subpath so the
// browser fetches /<subpath>/assets/... when Gogs is mounted on a
// non-root URL.
pairs = append(pairs,
`src="/assets/`, `src="`+wc.SubURL+`/assets/`,
`href="/assets/`, `href="`+wc.SubURL+`/assets/`,
`src="/src/`, `src="`+wc.SubURL+`/src/`,
)
}
return []byte(strings.NewReplacer(pairs...).Replace(string(index)))
}
func healthCheck(w http.ResponseWriter, r *http.Request) {
if err := database.Ping(); err != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = fmt.Fprintf(w, "* Database connection: %s\n", err)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
_, _ = w.Write([]byte("* Database connection: OK\n"))
}
+55
View File
@@ -0,0 +1,55 @@
//go:build !prod
package web
import (
"bytes"
"io"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/flamego/flamego"
"gogs.io/gogs/internal/context"
)
func mountWebRoutes(f *flamego.Flame) error {
viteURL, err := url.Parse("http://localhost:5173")
if err != nil {
return errors.Wrap(err, "parse Vite URL")
}
proxy := httputil.NewSingleHostReverseProxy(viteURL)
proxy.ModifyResponse = func(resp *http.Response) error {
if !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
return nil
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "read Vite response body")
}
_ = resp.Body.Close()
wc := context.WebContextFrom(resp.Request)
body := renderIndex(raw, wc)
resp.Body = io.NopCloser(bytes.NewReader(body))
resp.ContentLength = int64(len(body))
resp.Header.Set("Content-Length", strconv.Itoa(len(body)))
if wc.Status != 0 {
resp.StatusCode = wc.Status
resp.Status = http.StatusText(wc.Status)
}
// The upstream validators describe the unmodified body. Drop them
// so the browser does not satisfy a conditional request from a
// cached copy that has a stale injected lang attribute.
resp.Header.Del("ETag")
resp.Header.Del("Last-Modified")
return nil
}
f.Any("/{**}", func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
})
return nil
}
+62
View File
@@ -0,0 +1,62 @@
//go:build prod
package web
import (
"io/fs"
"net/http"
"github.com/cockroachdb/errors"
"github.com/flamego/flamego"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/context"
"gogs.io/gogs/public"
)
func mountWebRoutes(f *flamego.Flame) error {
webFS, err := fs.Sub(public.WebAssets, "dist")
if err != nil {
return errors.Wrap(err, "load embedded web assets")
}
// Prefix matches the path rewrites renderIndex applies to the index
// shell. Without it the browser fetches /<subpath>/assets/... and the
// static handler looks them up in webFS at "<subpath>/assets/...",
// which has no <subpath> directory, so every asset would 404 and fall
// through to the wildcard handler as text/html.
//
// Index is set to a sentinel that does not exist in the FS so flamego.Static
// never serves the raw index.html for "/" requests. The catch-all below
// always renders the shell through renderIndex instead, ensuring template
// substitutions are applied.
f.Use(flamego.Static(flamego.StaticOptions{
FileSystem: http.FS(webFS),
Prefix: conf.Server.Subpath,
Index: "__disabled__",
}))
index, err := public.WebAssets.ReadFile("dist/index.html")
if err != nil {
return errors.Wrap(err, `read "dist/index.html"`)
}
f.Get("/{**}", func(w http.ResponseWriter, r *http.Request) {
wc := context.WebContextFrom(r)
body := renderIndex(index, wc)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// The body is rewritten per request (lang injection, future
// runtime config), so caching it would serve stale content to
// any user whose request resolves to a different locale. Use
// no-store rather than no-cache so the browser cannot keep a
// copy at all, not even for revalidation. Static assets keep
// their normal caching via flamego.Static.
w.Header().Set("Cache-Control", "no-store")
status := wc.Status
if status <= 0 {
status = http.StatusOK
}
w.WriteHeader(status)
_, _ = w.Write(body)
})
return nil
}
+20 -1
View File
@@ -4,10 +4,12 @@ package main
import (
"context"
"os"
"path/filepath"
"github.com/urfave/cli/v3"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/cmd/gogs/internal/web"
"gogs.io/gogs/internal/conf"
)
@@ -15,10 +17,27 @@ func init() {
conf.App.Version = "0.15.0+dev"
}
var webCommand = cli.Command{
Name: "web",
Usage: "Start the web server",
Description: "Serves the web interface, API, and HTTP Git endpoints.",
Action: func(_ context.Context, cmd *cli.Command) error {
var portOverride int
if cmd.IsSet("port") {
portOverride = cmd.Int("port")
}
return web.Run(configFromLineage(cmd), portOverride)
},
Flags: []cli.Flag{
intFlag("port, p", 3000, "Alternative listening port to use"),
stringFlag("config, c", filepath.Join(conf.CustomDir(), "conf", "app.ini"), "Custom configuration file path"),
},
}
func main() {
cmd := &cli.Command{
Name: "Gogs",
Usage: "A painless self-hosted Git service",
Usage: "The painless way to host your own Git service",
Version: conf.App.Version,
Commands: []*cli.Command{
&webCommand,
-1
View File
@@ -571,6 +571,5 @@ mn-MN = mn
ro-RO = ro
[other]
SHOW_FOOTER_BRANDING = false
; Show time of template execution in the footer
SHOW_FOOTER_TEMPLATE_LOAD_TIME = true
+7 -3
View File
@@ -1,10 +1,10 @@
app_desc = A painless self-hosted Git service
app_desc = The painless way to host your own Git service
home = Home
dashboard = Dashboard
explore = Explore
help = Help
sign_in = Sign In
sign_in = Sign in
sign_out = Sign Out
sign_up = Sign Up
register = Register
@@ -34,6 +34,10 @@ manage_org = Manage Organizations
admin_panel = Admin Panel
account_settings = Account Settings
settings = Settings
theme = Theme
theme_light = Light
theme_dark = Dark
theme_system = System
your_profile = Your Profile
your_settings = Your Settings
@@ -44,7 +48,7 @@ issues = Issues
cancel = Cancel
[status]
page_not_found = Page Not Found
page_not_found = Page not found
internal_server_error = Internal Server Error
[install]
+18 -4
View File
@@ -8,6 +8,8 @@ require (
github.com/cockroachdb/errors v1.13.0
github.com/derision-test/go-mockgen/v2 v2.1.1
github.com/editorconfig/editorconfig-core-go/v2 v2.6.4
github.com/fatih/color v1.18.0
github.com/flamego/flamego v1.12.0
github.com/glebarez/go-sqlite v1.21.2
github.com/glebarez/sqlite v1.11.0
github.com/go-ldap/ldap/v3 v3.4.12
@@ -60,14 +62,22 @@ require (
require (
bitbucket.org/creachadair/shell v0.0.7 // indirect
charm.land/lipgloss/v2 v2.0.1 // indirect
charm.land/log/v2 v2.0.0 // indirect
github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/alecthomas/participle/v2 v2.1.4 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -75,9 +85,9 @@ require (
github.com/djherbis/buffer v1.2.0 // indirect
github.com/djherbis/nio/v3 v3.0.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/getsentry/sentry-go v0.46.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-macaron/inject v0.0.0-20200308113650-138e5925c53b // indirect
@@ -98,11 +108,13 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.2 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
@@ -114,9 +126,11 @@ require (
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.bobheadxi.dev/streamline v1.2.1 // indirect
go.opentelemetry.io/otel v1.11.0 // indirect
go.opentelemetry.io/otel/trace v1.11.0 // indirect
+38 -6
View File
@@ -1,5 +1,9 @@
bitbucket.org/creachadair/shell v0.0.7 h1:Z96pB6DkSb7F3Y3BBnJeOZH2gazyMTWlvecSD4vDqfk=
bitbucket.org/creachadair/shell v0.0.7/go.mod h1:oqtXSSvSYr4624lnnabXHaBsYW6RD80caLi2b3hJk0U=
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
charm.land/log/v2 v2.0.0 h1:SY3Cey7ipx86/MBXQHwsguOT6X1exT94mmJRdzTNs+s=
charm.land/log/v2 v2.0.0/go.mod h1:c3cZSRqm20qUVVAR1WmS/7ab8bgha3C6G7DjPcaVZz0=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
@@ -15,6 +19,12 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
@@ -31,16 +41,26 @@ github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQ
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI=
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cockroachdb/errors v1.13.0 h1:BoCcJeiP9hpBJDETkX19qi8Tb8So37srSsp3stTaDMQ=
github.com/cockroachdb/errors v1.13.0/go.mod h1:bjxt/4E5+OyuAnacpTIU9rn2mzPu1VlthvHP+xpROq0=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
@@ -80,6 +100,8 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/flamego/flamego v1.12.0 h1:BS0iY6RytweVvu5j40fQJ53X2ZcUVeuQ8ZSigVkDB9A=
github.com/flamego/flamego v1.12.0/go.mod h1:MM4kNGS7SvJtwUZYb2oGySR+ncdtIvtJHsl8OhH1Ngo=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -99,6 +121,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -261,6 +285,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lunny/log v0.0.0-20160921050905-7887c61bf0de/go.mod h1:3q8WtuPQsoRbatJuy3nvq/hRSvuBJrHHr+ybPPiNvHQ=
github.com/lunny/nodb v0.0.0-20160621015157-fc1ef06ad4af/go.mod h1:Cqz6pqow14VObJ7peltM+2n3PWOz7yTrfUuGbVFkzN0=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -283,6 +309,8 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE=
github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@@ -352,6 +380,8 @@ github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlT
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
@@ -410,6 +440,8 @@ github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
+8
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/cockroachdb/errors"
"github.com/fatih/color"
_ "github.com/go-macaron/cache/redis"
_ "github.com/go-macaron/session/redis"
"github.com/gogs/go-libravatar"
@@ -23,6 +24,13 @@ import (
)
func init() {
// fatih/color disables ANSI codes when stdout is not a TTY, which is the
// case under process managers like moon. Honor TTY_FORCE so callers can
// opt back in without a real terminal.
if os.Getenv("TTY_FORCE") != "" {
color.NoColor = false
}
// Initialize the primary logger until logging service is up.
err := log.NewConsole()
if err != nil {
+1 -2
View File
@@ -222,7 +222,6 @@ var (
// Other settings
Other struct {
ShowFooterBranding bool
ShowFooterTemplateLoadTime bool
}
@@ -268,7 +267,7 @@ type ServerOpts struct {
Domain string
Protocol string
HTTPAddr string `ini:"HTTP_ADDR"`
HTTPPort string `ini:"HTTP_PORT"`
HTTPPort int `ini:"HTTP_PORT"`
CertFile string
KeyFile string
TLSMinVersion string `ini:"TLS_MIN_VERSION"`
-1
View File
@@ -48,7 +48,6 @@ func TestCheckInvalidOptions(t *testing.T) {
_, _ = cfg.Section("server").NewKey("LANDING_PAGE", "true")
_, _ = cfg.Section("database").NewKey("DB_TYPE", "true")
_, _ = cfg.Section("database").NewKey("PASSWD", "true")
_, _ = cfg.Section("other").NewKey("SHOW_FOOTER_BRANDING", "true")
_, _ = cfg.Section("other").NewKey("SHOW_FOOTER_TEMPLATE_LOAD_TIME", "true")
_, _ = cfg.Section("email").NewKey("ENABLED", "true")
_, _ = cfg.Section("server").NewKey("NONEXISTENT_OPTION", "true")
+60 -12
View File
@@ -1,6 +1,7 @@
package context
import (
stdctx "context"
"fmt"
"io"
"net/http"
@@ -38,6 +39,8 @@ type Context struct {
Repo *Repository
Org *Organization
webHandler http.Handler
}
// RawTitle sets the "Title" field in template data.
@@ -156,10 +159,54 @@ func (c *Context) RenderWithErr(msg string, status int, tpl string, f any) {
c.HTML(status, tpl)
}
// NotFound renders the 404 page.
// WebContext carries per-request inputs into the web handler so it can
// render the React shell. Fields are read by helpers like WebContextFrom.
type WebContext struct {
Lang string
SubURL string
Status int
}
// WebContextKey is the request context key for WebContext values. Exported
// so callers outside this package (e.g. the web NotFound handler) can attach
// a WebContext when the request bypasses Contexter.
type WebContextKey struct{}
// WebContextFrom returns the WebContext attached to r, or a zero value with
// sensible defaults when nothing was attached.
func WebContextFrom(r *http.Request) WebContext {
wr, ok := r.Context().Value(WebContextKey{}).(WebContext)
if !ok {
return WebContext{Lang: "en-US"}
}
if wr.Lang == "" {
wr.Lang = "en-US"
}
return wr
}
// NotFound renders the React 404 page through the web handler with a 404
// status.
func (c *Context) NotFound() {
c.Title("status.page_not_found")
c.HTML(http.StatusNotFound, fmt.Sprintf("status/%d", http.StatusNotFound))
c.serveWeb(WebContext{
Lang: c.Language(),
SubURL: conf.Server.Subpath,
Status: http.StatusNotFound,
})
}
// ServeWeb delegates the current request to the web handler. The web frontend
// decides what to render based on the request path.
func (c *Context) ServeWeb() {
c.serveWeb(WebContext{
Lang: c.Language(),
SubURL: conf.Server.Subpath,
})
}
func (c *Context) serveWeb(wr WebContext) {
ctx := stdctx.WithValue(c.Req.Context(), WebContextKey{}, wr)
c.webHandler.ServeHTTP(c.Resp, c.Req.WithContext(ctx))
}
// Error renders the 500 page.
@@ -221,16 +268,18 @@ func (c *Context) ServeContent(name string, r io.ReadSeeker, params ...any) {
// https://github.com/go-macaron/csrf/blob/5d38f39de352972063d1ef026fc477283841bb9b/csrf.go#L148.
var csrfTokenExcludePattern = lazyregexp.New(`[^a-zA-Z0-9-_].*`)
// Contexter initializes a classic context for a request.
func Contexter(store Store) macaron.Handler {
// Contexter initializes a classic context for a request. webHandler
// receives 404 responses so the React frontend can render its own 404 page.
func Contexter(store Store, webHandler http.Handler) macaron.Handler {
return func(ctx *macaron.Context, l i18n.Locale, cache cache.Cache, sess session.Store, f *session.Flash, x csrf.CSRF) {
c := &Context{
Context: ctx,
Cache: cache,
csrf: x,
Flash: f,
Session: sess,
Link: conf.Server.Subpath + strings.TrimSuffix(ctx.Req.URL.Path, "/"),
Context: ctx,
Cache: cache,
csrf: x,
Flash: f,
Session: sess,
Link: conf.Server.Subpath + strings.TrimSuffix(ctx.Req.URL.Path, "/"),
webHandler: webHandler,
Repo: &Repository{
PullRequest: &PullRequest{},
},
@@ -279,7 +328,6 @@ func Contexter(store Store) macaron.Handler {
log.Trace("CSRF Token: %v", c.Data["CSRFToken"])
c.Data["ShowRegistrationButton"] = !conf.Auth.DisableRegistration
c.Data["ShowFooterBranding"] = conf.Other.ShowFooterBranding
c.renderNoticeBanner()
+1 -12
View File
@@ -2,12 +2,8 @@ package route
import (
gocontext "context"
"fmt"
"net/http"
"github.com/go-macaron/i18n"
"github.com/unknwon/paginater"
"gopkg.in/macaron.v1"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/context"
@@ -16,7 +12,6 @@ import (
)
const (
tmplHome = "home"
tmplExploreRepos = "explore/repos"
tmplExploreUsers = "explore/users"
tmplExploreOrganizations = "explore/organizations"
@@ -40,8 +35,7 @@ func Home(c *context.Context) {
return
}
c.Data["PageIsHome"] = true
c.Success(tmplHome)
c.ServeWeb()
}
func ExploreRepos(c *context.Context) {
@@ -157,8 +151,3 @@ func ExploreOrganizations(c *context.Context) {
TplName: tmplExploreOrganizations,
})
}
func NotFound(c *macaron.Context, l i18n.Locale) {
c.Data["Title"] = l.Tr("status.page_not_found")
c.HTML(http.StatusNotFound, fmt.Sprintf("status/%d", http.StatusNotFound))
}
+1 -1
View File
@@ -157,7 +157,7 @@ func Install(c *context.Context) {
f.Domain = conf.Server.Domain
f.SSHPort = conf.SSH.Port
f.UseBuiltinSSHServer = conf.SSH.StartBuiltinServer
f.HTTPPort = conf.Server.HTTPPort
f.HTTPPort = strconv.Itoa(conf.Server.HTTPPort)
f.AppUrl = conf.Server.ExternalURL
f.LogRootPath = conf.Log.RootPath
f.DefaultBranch = conf.Repository.DefaultBranch
+22 -7
View File
@@ -40,14 +40,32 @@ func askCredentials(c *macaron.Context, status int, text string) {
c.Error(status, text)
}
// gitHTTPAction returns the Git HTTP path suffix after /:username/:reponame/.
func gitHTTPAction(c *macaron.Context) string {
return gitHTTPActionFromPath(
c.Req.URL.Path,
conf.Server.Subpath,
c.Params(":username"),
c.Params(":reponame"),
)
}
func gitHTTPActionFromPath(urlPath, subpath, owner, repo string) string {
if subpath != "" {
urlPath = strings.TrimPrefix(urlPath, subpath)
}
prefix := path.Join("/", owner, repo) + "/"
if after, ok := strings.CutPrefix(urlPath, prefix); ok {
return after
}
return ""
}
func HTTPContexter(store Store) macaron.Handler {
return func(c *macaron.Context) {
if len(conf.HTTP.AccessControlAllowOrigin) > 0 {
// Set CORS headers for browser-based git clients
c.Header().Set("Access-Control-Allow-Origin", conf.HTTP.AccessControlAllowOrigin)
c.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
// Handle preflight OPTIONS request
if c.Req.Method == "OPTIONS" {
c.Status(http.StatusOK)
return
@@ -93,7 +111,7 @@ func HTTPContexter(store Store) macaron.Handler {
}
// In case user requested a wrong URL and not intended to access Git objects.
action := c.Params("*")
action := gitHTTPAction(c)
if !strings.Contains(action, "git-") &&
!strings.Contains(action, "info/") &&
!strings.Contains(action, "HEAD") &&
@@ -394,9 +412,6 @@ func HTTP(c *HTTPContext) {
continue
}
// We perform check here because route matched in cmd/web.go is wider than needed,
// but we only want to output this message only if user is really trying to access
// Git HTTP endpoints.
if conf.Repository.DisableHTTPGit {
c.Error(http.StatusForbidden, "Interacting with repositories by HTTP protocol is disabled")
return
+75
View File
@@ -0,0 +1,75 @@
package repo
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGitHTTPActionFromPath(t *testing.T) {
tests := []struct {
name string
urlPath string
subpath string
owner string
repo string
want string
}{
{
name: "info refs",
urlPath: "/owner/repo/info/refs",
owner: "owner",
repo: "repo",
want: "info/refs",
},
{
name: "git suffix repo",
urlPath: "/owner/repo.git/git-receive-pack",
owner: "owner",
repo: "repo.git",
want: "git-receive-pack",
},
{
name: "head",
urlPath: "/owner/repo/HEAD",
owner: "owner",
repo: "repo",
want: "HEAD",
},
{
name: "objects info wildcard",
urlPath: "/owner/repo/objects/info/exclude",
owner: "owner",
repo: "repo",
want: "objects/info/exclude",
},
{
name: "loose object",
urlPath: "/owner/repo/objects/ab/cdef0123456789abcdef0123456789abcdef",
owner: "owner",
repo: "repo",
want: "objects/ab/cdef0123456789abcdef0123456789abcdef",
},
{
name: "with subpath",
urlPath: "/gogs/owner/repo/info/refs",
subpath: "/gogs",
owner: "owner",
repo: "repo",
want: "info/refs",
},
{
name: "non git suffix",
urlPath: "/owner/repo/src/main.go",
owner: "owner",
repo: "repo",
want: "src/main.go",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := gitHTTPActionFromPath(tc.urlPath, tc.subpath, tc.owner, tc.repo)
assert.Equal(t, tc.want, got)
})
}
}
+101
View File
@@ -0,0 +1,101 @@
$schema: "https://moonrepo.dev/schemas/project.json"
language: "go"
stack: "backend"
id: "gogs"
fileGroups:
sources:
- "cmd/**/*.go"
- "internal/**/*.go"
- "public/**/*.go"
- "templates/**/*.go"
- "conf/**/*.go"
tests:
- "**/*_test.go"
configs:
- "go.mod"
- "go.sum"
- ".golangci.yml"
assets:
- "conf/**/*"
- "public/**/*"
- "templates/**/*"
tasks:
install:
command: "go mod tidy"
inputs:
- "@group(configs)"
format:
command: "golangci-lint fmt"
inputs:
- "@group(sources)"
- "@group(configs)"
deps:
- "install"
lint:
command: "golangci-lint run"
inputs:
- "@group(sources)"
- "@group(configs)"
deps:
- "install"
- "format"
test:
command: "go test -cover -race ./..."
inputs:
- "@group(sources)"
- "@group(tests)"
- "@group(configs)"
deps:
- "install"
build:
script: |
go build -trimpath \
-ldflags "-X 'gogs.io/gogs/internal/conf.BuildTime=$(date -u '+%Y-%m-%d %I:%M:%S %Z')' -X 'gogs.io/gogs/internal/conf.BuildCommit=$(git rev-parse HEAD)'" \
-o .bin/gogs ./cmd/gogs
inputs:
- "@group(sources)"
- "@group(configs)"
- "@group(assets)"
outputs:
- ".bin/gogs"
deps:
- "install"
build-prod:
script: |
go build -trimpath -tags prod \
-ldflags "-X 'gogs.io/gogs/internal/conf.BuildTime=$(date -u '+%Y-%m-%d %I:%M:%S %Z')' -X 'gogs.io/gogs/internal/conf.BuildCommit=$(git rev-parse HEAD)'" \
-o .bin/gogs ./cmd/gogs
inputs:
- "@group(sources)"
- "@group(configs)"
- "@group(assets)"
outputs:
- ".bin/gogs"
deps:
- "install"
- "web:build"
dev:
command: ".bin/gogs web"
preset: "server"
env:
TTY_FORCE: "1"
deps:
- "build"
- "web:dev"
prod:
command: ".bin/gogs web"
preset: "server"
env:
TTY_FORCE: "1"
deps:
- "build-prod"
+5
View File
@@ -0,0 +1,5 @@
{
"name": "gogs",
"private": true,
"packageManager": "pnpm@11.1.3"
}
+4001
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
packages:
- "web"
onlyBuiltDependencies:
- esbuild
+9
View File
@@ -0,0 +1,9 @@
//go:build !prod
package public
import "embed"
// WebAssets is empty in dev — requests are proxied to the live Vite server.
// Declared so the prod and dev builds share the same symbol.
var WebAssets embed.FS
+8
View File
@@ -0,0 +1,8 @@
//go:build prod
package public
import "embed"
//go:embed all:dist
var WebAssets embed.FS
+2 -5
View File
@@ -15,11 +15,8 @@
{{.i18n.Tr "page"}}: <strong>{{LoadTimes .PageStartTime}}</strong> {{.i18n.Tr "template"}}: <strong>{{call .TmplLoadTimes}}</strong>
</span>
{{end}}
{{if .ShowFooterBranding}}
<a target="_blank" rel="noopener noreferrer" href="https://github.com/gogs/gogs"><i class="fa fa-github-square"></i><span class="sr-only">GitHub</span></a>
<a target="_blank" rel="noopener noreferrer" href="https://twitter.com/GogsHQ"><i class="fa fa-twitter"></i><span class="sr-only">Twitter</span></a>
<a target="_blank" rel="noopener noreferrer" href="http://weibo.com/gogschina"><i class="fa fa-weibo"></i><span class="sr-only">Sina Weibo</span></a>
{{end}}
<a target="_blank" rel="noopener noreferrer" href="https://github.com/gogs/gogs"><i class="fa fa-github-square"></i><span class="sr-only">GitHub</span></a>
<a target="_blank" rel="noopener noreferrer" href="https://twitter.com/GogsHQ"><i class="fa fa-twitter"></i><span class="sr-only">Twitter</span></a>
<div class="ui language bottom floating slide up dropdown link item">
<i class="world icon"></i>
<div class="text">{{.LangName}}</div>
-350
View File
@@ -1,350 +0,0 @@
{{template "base/head" .}}
<div class="home">
<div class="ui stackable middle very relaxed page grid">
<div class="sixteen wide center aligned centered column">
<div class="logo">
<img src="{{AppSubURL}}/img/gogs-hero.png" />
</div>
<div class="hero">
<h2>{{.i18n.Tr "app_desc"}}</h2>
</div>
</div>
</div>
{{if eq .Lang "de-DE"}}
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-flame"></i> Einfach zu installieren
</h1>
<p class="large">
Starte einfach die Anwendung für deine Plattform. Gogs gibt es auch für <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogs/gogs/tree/main/docker">Docker</a> oder als Installationspaket.
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-device-desktop"></i> Plattformübergreifend
</h1>
<p class="large">
Gogs läuft überall. <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> kompiliert für: Windows, macOS, Linux, ARM, etc. Wähle dasjenige System, was dir am meisten gefällt!
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-rocket"></i> Leichtgewicht
</h1>
<p class="large">
Gogs hat minimale Systemanforderungen und kann selbst auf einem günstigen und stromsparenden Raspberry Pi betrieben werden.
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-code"></i> Quelloffen
</h1>
<p class="large">
Der komplette Code befindet sich auf <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogits/gogs/">GitHub</a>! Unterstütze uns bei der Verbesserung dieses Projekts. Trau dich!
</p>
</div>
</div>
{{else if eq .Lang "zh-CN"}}
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-flame"></i> 易安装
</h1>
<p class="large">
您除了可以根据操作系统平台通过二进制运行,还可以通过 <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogs/gogs/tree/main/docker">Docker</a>,以及包管理安装。
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-device-desktop"></i> 跨平台
</h1>
<p class="large">
任何 <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go 语言</a> 支持的平台都可以运行 Gogs,包括 Windows、Mac、Linux 以及 ARM。挑一个您喜欢的就行!
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-rocket"></i> 轻量级
</h1>
<p class="large">
一个廉价的树莓派的配置足以满足 Gogs 的最低系统硬件要求。最大程度上节省您的服务器资源!
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-code"></i> 开源化
</h1>
<p class="large">
所有的代码都开源在 <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogits/gogs/">GitHub</a> 上,赶快加入我们来共同发展这个伟大的项目!还等什么?成为贡献者吧!
</p>
</div>
</div>
{{else if eq .Lang "fr-FR"}}
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-flame"></i> Facile à installer
</h1>
<p class="large">
Il suffit de lancer l'exécutable correspondant à votre système.
Ou d'utiliser Gogs avec <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogs/gogs/tree/main/docker">Docker</a>
ou en l'installant depuis un package.
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-device-desktop"></i> Multi-plateforme
</h1>
<p class="large">
Gogs tourne partout où <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> peut être compilé : Windows, macOS, Linux, ARM, etc. Choisissez votre préféré !
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-rocket"></i> Léger
</h1>
<p class="large">
Gogs utilise peu de ressources. Il peut même tourner sur un Raspberry Pi très bon marché. Économisez l'énergie de vos serveurs !
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-code"></i> Open Source
</h1>
<p class="large">
Toutes les sources sont sur <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogits/gogs/">GitHub</a> ! Rejoignez-nous et contribuez à rendre ce projet encore meilleur.
</p>
</div>
</div>
{{else if eq .Lang "es-ES"}}
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-flame"></i> Fácil de instalar
</h1>
<p class="large">
Simplemente arranca el binario para tu plataforma. O usa Gogs con <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogs/gogs/tree/main/docker">Docker</a>, o utilice el paquete.
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-device-desktop"></i> Multiplatforma
</h1>
<p class="large">
Gogs funciona en cualquier parte, <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> puede compilarse en: Windows, macOS, Linux, ARM, etc. !Elige tu favorita!
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-rocket"></i> Ligero
</h1>
<p class="large">
Gogs tiene pocos requisitos y puede funcionar en una Raspberry Pi barata. !Ahorra energía!
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-code"></i> Open Source
</h1>
<p class="large">
¡Está todo en <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogits/gogs/">GitHub</a>! Uniros contribuyendo a hacer este proyecto todavía mejor. ¡No seas tímido y colabora!
</p>
</div>
</div>
{{else if eq .Lang "pt-BR"}}
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-flame"></i> Fácil de instalar
</h1>
<p class="large">
Simplesmente rode o executável para o seu sistema operacional. Ou obtenha o Gogs com o <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogs/gogs/tree/main/docker">Docker</a>, ou baixe o pacote.
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-device-desktop"></i> Multi-plataforma
</h1>
<p class="large">
Gogs roda em qualquer sistema operacional em que <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> consegue compilar: Windows, macOS, Linux, ARM, etc. Escolha qual você gosta mais!
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-rocket"></i> Leve e rápido
</h1>
<p class="large">
Gogs utiliza poucos recursos e consegue mesmo rodar no barato Raspberry Pi. Economize energia elétrica da sua máquina!
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-code"></i> Código aberto
</h1>
<p class="large">
Está tudo no <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogits/gogs/">GitHub</a>! Contribua e torne este projeto ainda melhor. Não tenha vergonha de contribuir!
</p>
</div>
</div>
{{else if eq .Lang "ru-RU"}}
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-flame"></i> Простой в установке
</h1>
<p class="large">
Просто запустите исполняемый файл для вашей платформы. Используйте Gogs с <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogs/gogs/tree/main/docker">Docker</a> или загрузите пакет.
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-device-desktop"></i> Кроссплатформенный
</h1>
<p class="large">
Gogs работает на любой операционной системе, которая может компилировать <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a>: Windows, macOS, Linux, ARM и т. д. Выбирайте, что вам больше нравится!
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-rocket"></i> Легковесный
</h1>
<p class="large">
Gogs имеет низкие системные требования и может работать на недорогом Raspberry Pi. Экономьте энергию вашей машины!
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-code"></i> Открытый исходный код
</h1>
<p class="large">
Всё это на <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogits/gogs/">GitHub</a>! Присоединяйтесь к нам, внося вклад, чтобы сделать этот проект еще лучше. Не бойтесь помогать!
</p>
</div>
</div>
{{else if eq .Lang "uk-UA"}}
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-flame"></i> Простий у втановленні
</h1>
<p class="large">
Просто запустіть виконуваний файл для вашої платформи. Використовуйте Gogs с <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogs/gogs/tree/main/docker">Docker</a> або завантажте пакет.
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-device-desktop"></i> Кросплатформність
</h1>
<p class="large">
Gogs працює у будь-якій операційній системі, що може компілювати <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a>: Windows, macOS, Linux, ARM і т. д. Обирайте що вам більше до вподоби!
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-rocket"></i> Легковісний
</h1>
<p class="large">
Gogs має низькі системні вимоги та може працювати на недорогому Raspberry Pi. Економте енергію вашої машини!
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-code"></i> Відкритий сирцевий код
</h1>
<p class="large">
Все це у <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogits/gogs/">GitHub</a>! Приєднуйтеся до нас, робіть внесок, щоб зробити цей проект ще краще. Не бійтеся допомагати!
</p>
</div>
</div>
{{else if eq .Lang "it-IT"}}
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-flame"></i> Facie da installare
</h1>
<p class="large">
Basta avviare il binario per la tua piattaforma.
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-device-desktop"></i> Multipiattaforma
</h1>
<p class="large">
Gogs funziona ovunque, <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> si può compilare su: Windows, macOS, Linux, ARM, etc. Scegli il tuo preferito!
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-rocket"></i> Leggero
</h1>
<p class="large">
Gogs ha requisiti bassi e può funzionare su un Raspberry Pi economico. Risparmiare energia!
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-code"></i> Open Source
</h1>
<p class="large">
Sta tutto su <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogits/gogs/">GitHub</a>! È tutto su GitHub! Unisciti a noi contribuendo a rendere questo progetto ancora miglior$
</p>
</div>
</div>
{{else}}
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-flame"></i> Easy to install
</h1>
<p class="large">
Simply run the binary for your platform. Or ship Gogs with <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogs/gogs/tree/main/docker">Docker</a>, or get it packaged.
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-device-desktop"></i> Cross-platform
</h1>
<p class="large">
Gogs runs anywhere <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> can compile for: Windows, macOS, Linux, ARM, etc. Choose the one you love!
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-rocket"></i> Lightweight
</h1>
<p class="large">
Gogs has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
<i class="octicon octicon-code"></i> Open Source
</h1>
<p class="large">
It's all on <a target="_blank" rel="noopener noreferrer" href="https://github.com/gogits/gogs/">GitHub</a>! Join us by contributing to make this project even better. Don't be shy to be a contributor!
</p>
</div>
</div>
{{end}}
</div>
{{template "base/footer" .}}
-8
View File
@@ -1,8 +0,0 @@
{{template "base/head" .}}
<div class="ui container center">
<p style="margin-top: 100px"><img src="{{AppSubURL}}/img/404.png" alt="404"/></p>
<div class="ui divider"></div>
<br>
<p>If you think this is an error, please open an issue on <a href="https://github.com/gogs/gogs/issues/new">GitHub</a>.</p>
</div>
{{template "base/footer" .}}
+2
View File
@@ -0,0 +1,2 @@
*.tsbuildinfo
.vite
+12
View File
@@ -0,0 +1,12 @@
/** @type {import("prettier").Config} */
export default {
useTabs: false,
tabWidth: 2,
singleQuote: false,
trailingComma: "all",
printWidth: 120,
plugins: ["@trivago/prettier-plugin-sort-imports"],
importOrder: ["<BUILTIN_MODULES>", "<THIRD_PARTY_MODULES>", "^@/(.+)", "^[./]"],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
};
+79
View File
@@ -0,0 +1,79 @@
# Gogs web design notes
A running record of design decisions made for the web frontend. Add an entry when a pattern is used in two places, or when a question caused a redo. Don't write aspirationally. Only capture what's already true in the code.
## Typography
Self-hosted via `@fontsource-variable`:
- **Sans**: Geist Variable, with PingFang SC and Microsoft YaHei as CJK fallbacks. Used for body text, headings, and UI chrome.
- **Mono**: Geist Mono Variable, with the same CJK fallbacks. Used for code-shaped content (SHA, branch name, file path, shell command, terminal-style surfaces).
The browser does per-glyph fallback via the font-family stack. Latin characters render in Geist (the designed personality). CJK characters render in the next available system font (PingFang SC, Microsoft YaHei). The result: English looks distinctively Gogs, other scripts look clean and native.
Use mono only for content that **is** code, not for UI chrome (navbars, buttons, labels). Mono CJK fallbacks aren't truly monospace (CJK glyphs are wider than Latin), which is fine when the content is genuinely code, but reads as broken alignment if used decoratively for chrome.
Don't mix sans and mono within the same UI surface for arbitrary reasons. If a component is showing code, all of it goes mono.
## Color hierarchy
Palettes are derived from [Happy Hues](https://www.happyhues.co): palette 6 for light, palette 13 for dark. Dark mode is opt-in via the `.dark` class on `:root` (see `lib/theme.ts`), not media-query driven, so the user's stored preference always wins. The `@custom-variant dark` rule in `index.css` lets utilities like `dark:...` target the same class.
Use these tokens. Don't introduce raw hex values in components.
**Surfaces and content**
- `--color-background`: page background. Body uses this by default.
- `--color-foreground`: primary content. Headings, active states, the main label of any item, body text on `--color-background`.
- `--color-muted-foreground`: secondary content. Metadata, helper text, terminal prompt characters, footer chrome, inactive items in a toggle group.
- `--color-surface`: subtle raised surface. Used for hover backgrounds (`hover:bg-(--color-surface)` on links, buttons, menu rows) and for the muted fill of the faux-terminal frame.
- `--color-card` / `--color-card-foreground`: card surface and its body text. Not currently used in components, but available for content cards.
- `--color-popover` / `--color-popover-foreground`: popover surface and body. Used by the Radix popover primitive.
**Accents and state**
- `--color-primary` / `--color-primary-foreground`: brand purple. Reserved for genuine brand emphasis. Don't use it to mean "primary action" between two peer links (see the peer-item rule below).
- `--color-secondary` / `--color-secondary-foreground`: muted brand support. Available for chips, tags, low-emphasis fills.
- `--color-destructive` / `--color-destructive-foreground`: error and danger. The 404 page uses `text-(--color-destructive)` on the `fatal:` token, always paired with the word itself (color is never the sole signal).
- `--color-ring`: keyboard focus ring color. Don't override per-component. If a default ring looks wrong, fix it at the token level.
**Structure**
- `--color-border`: borders on dividers, popovers, the terminal frame.
- `--color-input`: input field borders. Not currently used; reserved for forms.
**Peer-item rule**
Don't use foreground vs muted-foreground to imply "primary action" vs "secondary action" between two peer items (e.g. Sign in vs Register). Peer items get the same color. Differentiation comes from positioning, weight, or affordance, not arbitrary contrast. Active vs inactive _states_ of the same control (e.g. the selected theme tile in `SettingsMenu`) are a different case and may use the foreground/muted-foreground split to communicate selection.
**Ad-hoc colors**
The traffic-light cluster in the faux-terminal frame uses one ad-hoc value: the amber dot falls back to `oklch(0.795 0.184 86.047)` via `bg-(--color-warning,...)`. There is no `--color-warning` token defined, so the fallback always wins. This is deliberate. Promoting it to a real token would invite reuse, and warning is not a system-wide concern in the current UI. Leave it inline until a second site needs warning semantics, then define the token in both light and dark palettes.
## Surface chrome
The 404 page wraps its content in a faux-terminal frame (rounded border, traffic-light dots, monospace body). Reuse the same frame for any page that represents a Git/CLI state: error pages, command-output stubs, raw diff fallbacks. Don't reuse it for normal content pages.
Strings rendered inside a terminal frame stay in English across all locales, regardless of the active UI language. Real CLI output (`git`, `ls`, `cat`, etc.) doesn't localize. Faux-CLI that does loses authenticity and reads as a translated error page in a costume. Translate the surrounding prose (headings, descriptions, CTAs), but leave command names, prompts, error tokens like `fatal:`, and command output strings untouched.
## File naming
Two conventions coexist in `web/src/`:
- **shadcn primitives** in `components/ui/` use **lowercase** filenames (`popover.tsx`). This matches the `shadcn` CLI output and lets dropped-in components stay byte-identical to upstream.
- **App components** anywhere else use **PascalCase** matching the exported component (`Footer.tsx`, `SettingsMenu.tsx`, `Landing.tsx`). This is the React community default.
Library modules in `lib/` are plain `.ts` files in lowercase (`i18n.ts`, `theme.ts`, `utils.ts`).
## Accessibility
WCAG 2.2 AA is the floor. Apply these patterns in components:
- **Icon-only buttons need an accessible name.** Set `aria-label` on every button or link whose visible content is purely a glyph (settings cog, hamburger, social icons in the footer, language switcher trigger). The label is the action, not the icon name (`aria-label="Settings"`, not `"Cog icon"`).
- **Decorative icons inside a labeled control** get `aria-hidden`. If the button already has visible text or a sibling label, mark the SVG `aria-hidden` so screen readers don't double-announce.
- **Interactive states must be reachable by keyboard.** Anything that handles `onClick` must also be focusable (use a `<button>` or `<a>`, not a `<div>`). Tab order should follow visual order. Esc closes overlays. Click-outside also closes overlays, but Esc is mandatory and click-outside is convenience.
- **Don't disable focus rings.** If the default ring is visually wrong, restyle via `--color-ring` or `focus-visible:` utilities. Never remove it. Sighted keyboard users need to see where focus is.
- **Touch targets are 40×40 CSS px or larger** for primary actions, with 24×24 as the absolute minimum. The settings cog and hamburger use `size-9` (36px), which is acceptable on dense chrome. Full-width tap rows on mobile menus use `py-3` to clear 40px.
- **Color is never the sole signal.** The current-item indicator in the language list is a ✓ icon, not just a color shift. The destructive token is paired with the word `fatal:` in the 404 terminal, not just red text. The theme toggle has Sun/Moon/Monitor icons alongside the label.
- **Respect `prefers-reduced-motion`.** Popover animations from `tw-animate-css` honor this by default. If hand-rolling animations, gate them behind `motion-safe:`.
- **Test before merging:** tab through the new UI with the keyboard only; resize to 375px; toggle dark mode; check focus rings are visible against both themes.
+44
View File
@@ -0,0 +1,44 @@
import js from "@eslint/js";
import importPlugin from "eslint-plugin-import";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default [
{
ignores: ["dist", "eslint.config.js", ".prettierrc.js", "vite.config.ts", "scripts/**"],
},
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
importPlugin.flatConfigs.recommended,
importPlugin.flatConfigs.typescript,
{
rules: {
"import/no-unresolved": "off",
},
},
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
ecmaVersion: "latest",
sourceType: "module",
},
globals: {
document: "readonly",
window: "readonly",
},
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
},
},
];
+34
View File
@@ -0,0 +1,34 @@
<!doctype html>
<html lang="{{.Lang}}" class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="sub-url" content="{{.SubURL}}" />
<link rel="shortcut icon" href="{{.SubURL}}/img/favicon.png" />
<title>Gogs</title>
<style>
/* Inlined so the page paints with the right theme background before the main stylesheet arrives. */
html {
background-color: #fffffe;
}
html.dark {
background-color: #16161a;
}
</style>
<script>
// Inlined to run before paint and avoid theme flash. Treat any value
// other than "light" or "dark" (missing, corrupted, "system", etc.)
// as a follow-the-system signal so the OS preference still wins.
(function () {
var saved = localStorage.getItem("gogs-theme");
var explicit = saved === "light" || saved === "dark";
var dark = saved === "dark" || (!explicit && window.matchMedia("(prefers-color-scheme: dark)").matches);
if (dark) document.documentElement.classList.add("dark");
})();
</script>
</head>
<body class="h-full">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+59
View File
@@ -0,0 +1,59 @@
$schema: "https://moonrepo.dev/schemas/project.json"
language: "typescript"
stack: "frontend"
id: "web"
fileGroups:
sources:
- "src/**/*"
- "index.html"
configs:
- "package.json"
- "tsconfig*.json"
- "vite.config.*"
tasks:
install:
command: "pnpm install"
inputs:
- "package.json"
- "/pnpm-lock.yaml"
- "/pnpm-workspace.yaml"
options:
runFromWorkspaceRoot: true
dev:
command: "pnpm run dev"
preset: "server"
deps:
- "install"
build:
command: "pnpm run build"
inputs:
- "@group(sources)"
- "@group(configs)"
outputs:
- "/public/dist"
deps:
- "install"
lint:
command: "pnpm run lint"
inputs:
- "@group(sources)"
- "@group(configs)"
deps:
- "install"
- "format"
format:
command: "pnpm run format"
inputs:
- "@group(sources)"
- "@group(configs)"
- ".prettierrc.*"
- ".prettierignore"
deps:
- "install"
+48
View File
@@ -0,0 +1,48 @@
{
"name": "gogs-web",
"type": "module",
"scripts": {
"extract-locales": "node scripts/extract-locales.mjs",
"predev": "node scripts/extract-locales.mjs",
"prebuild": "node scripts/extract-locales.mjs",
"prelint": "node scripts/extract-locales.mjs",
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write ."
},
"dependencies": {
"@fontsource-variable/geist": "^5.2.9",
"@fontsource-variable/geist-mono": "^5.2.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-router": "^1.137.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^26.2.0",
"lucide-react": "^1.16.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-i18next": "^17.0.8",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.3.0",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.8.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^10.3.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"prettier": "^3.8.3",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.3",
"vite": "^8.0.13"
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

+63
View File
@@ -0,0 +1,63 @@
// Extracts the subset of keys the SPA needs from conf/locale/locale_*.ini and
// writes them as JSON under web/src/locales/. Run with `node scripts/extract-locales.mjs`
// after adding a new key or changing source translations.
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, "../..");
const inDir = join(repoRoot, "conf/locale");
const outDir = join(here, "..", "src/locales");
// Keys pulled from Gogs's INI files. Add new entries here when the SPA needs
// another translation. Locales missing a key fall back to en-US via react-i18next.
const REUSED_KEYS = [
"app_desc",
"home",
"explore",
"help",
"register",
"sign_in",
"settings",
"language",
"page_not_found",
"theme",
"theme_light",
"theme_dark",
"theme_system",
];
// Lightweight INI parser: handles `key = value` and `key=value`, ignores
// comments, and flattens sections into a single namespace. Gogs's locale
// files group keys under sections like [status] (e.g. status.page_not_found
// resolves to a key named "page_not_found" inside [status]), but downstream
// callers reference keys by their bare name, so the section header is
// dropped here. First occurrence wins on collisions.
function parseIni(text) {
const out = {};
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith(";") || line.startsWith("#") || line.startsWith("[")) continue;
const eq = line.indexOf("=");
if (eq < 0) continue;
const key = line.slice(0, eq).trim();
const value = line.slice(eq + 1).trim();
if (key && !(key in out)) out[key] = value;
}
return out;
}
mkdirSync(outDir, { recursive: true });
const files = readdirSync(inDir).filter((f) => f.startsWith("locale_") && f.endsWith(".ini"));
for (const file of files) {
const lang = file.slice("locale_".length, -".ini".length);
const parsed = parseIni(readFileSync(join(inDir, file), "utf8"));
const out = {};
for (const key of REUSED_KEYS) {
if (parsed[key]) out[key] = parsed[key];
}
writeFileSync(join(outDir, `${lang}.json`), JSON.stringify(out, null, 2) + "\n", "utf8");
console.log(`wrote ${lang}.json (${Object.keys(out).length} keys)`);
}
+17
View File
@@ -0,0 +1,17 @@
import { Footer } from "@/components/Footer";
import { Navbar } from "@/components/Navbar";
import { subUrl } from "@/lib/url";
import { Landing } from "@/pages/Landing";
import { NotFound } from "@/pages/NotFound";
export function App() {
const path = typeof window === "undefined" ? "/" : window.location.pathname.replace(/\/+$/, "") || "/";
const isLanding = path === subUrl("/");
return (
<div className="flex min-h-dvh flex-col">
<Navbar />
{isLanding ? <Landing /> : <NotFound />}
<Footer />
</div>
);
}
+50
View File
@@ -0,0 +1,50 @@
import { subUrl } from "@/lib/url";
export function Footer() {
return (
<footer className="border-t border-(--color-border)">
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-5 gap-y-3 px-4 py-6 text-sm text-(--color-muted-foreground) sm:px-6">
<span>© {new Date().getFullYear()} Gogs®</span>
<div className="flex flex-wrap items-center gap-x-2 gap-y-2">
<a
href="https://github.com/gogs/gogs"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="inline-flex size-8 items-center justify-center rounded-md hover:bg-(--color-surface) hover:text-(--color-foreground)"
>
<GitHubIcon />
</a>
<a
href="https://twitter.com/GogsHQ"
target="_blank"
rel="noopener noreferrer"
aria-label="Twitter"
className="inline-flex size-8 items-center justify-center rounded-md hover:bg-(--color-surface) hover:text-(--color-foreground)"
>
<TwitterIcon />
</a>
</div>
</div>
<a href={subUrl("/assets/librejs/librejs.html")} className="hidden" data-jslicense="1">
JavaScript Licenses
</a>
</footer>
);
}
function GitHubIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.44 9.8 8.21 11.39.6.11.82-.26.82-.58 0-.28-.01-1.04-.02-2.05-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.1-.75.08-.74.08-.74 1.21.09 1.85 1.24 1.85 1.24 1.07 1.84 2.81 1.31 3.5 1 .11-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.94 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.12-3.18 0 0 1.01-.32 3.3 1.23A11.5 11.5 0 0 1 12 5.8c1.02.01 2.05.14 3.01.4 2.29-1.55 3.3-1.23 3.3-1.23.66 1.66.24 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.62-2.81 5.63-5.49 5.93.43.37.81 1.1.81 2.22 0 1.61-.01 2.9-.01 3.3 0 .32.22.7.83.58A12 12 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
</svg>
);
}
function TwitterIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
}
+105
View File
@@ -0,0 +1,105 @@
import { Menu } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { SettingsMenu } from "@/components/SettingsMenu";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { subUrl } from "@/lib/url";
export function Navbar() {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
return (
<header className="sticky top-0 z-10 border-b border-(--color-border) bg-(--color-background)/95 backdrop-blur">
<nav className="mx-auto flex h-14 max-w-6xl items-center gap-3 px-4 text-sm sm:gap-4 sm:px-6">
<a href={subUrl("/")} className="flex shrink-0 items-center" aria-label="Gogs">
<img src={subUrl("/img/favicon.png")} alt="" width="28" height="28" className="size-7" />
</a>
<div className="hidden flex-1 items-center gap-1 sm:flex">
<NavLink href="/">{t("home")}</NavLink>
<NavLink href="/explore/repos">{t("explore")}</NavLink>
<NavLink href="https://gogs.io" external>
{t("help")}
</NavLink>
</div>
<div className="hidden shrink-0 items-center gap-1 sm:flex">
<SettingsMenu />
<NavLink href="/user/sign_up">{t("register")}</NavLink>
<NavLink href="/user/login">{t("sign_in")}</NavLink>
</div>
<div className="ml-auto flex shrink-0 items-center gap-1 sm:hidden">
<SettingsMenu />
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
aria-label="Open menu"
className="inline-flex size-9 cursor-pointer items-center justify-center rounded-md text-(--color-foreground) hover:bg-(--color-surface)"
>
<Menu className="size-[18px]" />
</PopoverTrigger>
<PopoverContent align="end" className="w-56 p-1" onOpenAutoFocus={(e) => e.preventDefault()}>
<ul className="flex flex-col text-sm">
<MobileLink href="/" onClick={() => setOpen(false)}>
{t("home")}
</MobileLink>
<MobileLink href="/explore/repos" onClick={() => setOpen(false)}>
{t("explore")}
</MobileLink>
<MobileLink href="https://gogs.io" external onClick={() => setOpen(false)}>
{t("help")}
</MobileLink>
<li className="my-1 h-px bg-(--color-border)" />
<MobileLink href="/user/sign_up" onClick={() => setOpen(false)}>
{t("register")}
</MobileLink>
<MobileLink href="/user/login" onClick={() => setOpen(false)}>
{t("sign_in")}
</MobileLink>
</ul>
</PopoverContent>
</Popover>
</div>
</nav>
</header>
);
}
function NavLink({ href, external, children }: { href: string; external?: boolean; children: React.ReactNode }) {
return (
<a
href={external ? href : subUrl(href)}
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
className="inline-flex rounded-md px-3 py-1.5 text-(--color-foreground) hover:bg-(--color-surface)"
>
{children}
</a>
);
}
function MobileLink({
href,
external,
onClick,
children,
}: {
href: string;
external?: boolean;
onClick?: () => void;
children: React.ReactNode;
}) {
return (
<li>
<a
href={external ? href : subUrl(href)}
onClick={onClick}
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
className="flex w-full rounded-sm px-2 py-1.5 text-(--color-foreground) hover:bg-(--color-surface)"
>
{children}
</a>
</li>
);
}
+154
View File
@@ -0,0 +1,154 @@
import { Check, Monitor, Moon, Settings, Sun } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { type Theme, useTheme } from "@/lib/theme";
import { cn } from "@/lib/utils";
const LANGUAGES: { code: string; name: string }[] = [
{ code: "en-US", name: "English" },
{ code: "zh-CN", name: "简体中文" },
{ code: "zh-HK", name: "繁體中文(香港)" },
{ code: "zh-TW", name: "繁體中文(臺灣)" },
{ code: "de-DE", name: "Deutsch" },
{ code: "fr-FR", name: "français" },
{ code: "nl-NL", name: "Nederlands" },
{ code: "lv-LV", name: "latviešu" },
{ code: "ru-RU", name: "русский" },
{ code: "ja-JP", name: "日本語" },
{ code: "es-ES", name: "español" },
{ code: "pt-BR", name: "português do Brasil" },
{ code: "pl-PL", name: "polski" },
{ code: "bg-BG", name: "български" },
{ code: "it-IT", name: "italiano" },
{ code: "fi-FI", name: "suomi" },
{ code: "tr-TR", name: "Türkçe" },
{ code: "cs-CZ", name: "čeština" },
{ code: "sr-SP", name: "српски" },
{ code: "sv-SE", name: "svenska" },
{ code: "ko-KR", name: "한국어" },
{ code: "gl-ES", name: "galego" },
{ code: "uk-UA", name: "українська" },
{ code: "en-GB", name: "English (United Kingdom)" },
{ code: "hu-HU", name: "Magyar" },
{ code: "sk-SK", name: "Slovenčina" },
{ code: "id-ID", name: "Indonesian" },
{ code: "fa-IR", name: "Persian" },
{ code: "vi-VN", name: "Vietnamese" },
{ code: "pt-PT", name: "Português" },
{ code: "mn-MN", name: "Монгол" },
{ code: "ro-RO", name: "Română" },
];
function currentLangCode(): string {
if (typeof document === "undefined") return "en-US";
return document.documentElement.lang || "en-US";
}
export function SettingsMenu() {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [currentLang] = useState(() => currentLangCode());
const { theme, setTheme } = useTheme();
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
aria-label={t("settings")}
className="inline-flex size-9 cursor-pointer items-center justify-center rounded-md text-(--color-muted-foreground) hover:bg-(--color-surface) hover:text-(--color-foreground)"
>
<Settings className="size-4" />
</PopoverTrigger>
<PopoverContent align="end" className="w-64 p-0" onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="px-2 pt-2 pb-1 text-xs font-medium text-(--color-muted-foreground)">{t("theme")}</div>
<div className="grid grid-cols-3 gap-1 p-1">
<ThemeOption
value="light"
current={theme}
onSelect={setTheme}
icon={<Sun className="size-4" />}
label={t("theme_light")}
/>
<ThemeOption
value="dark"
current={theme}
onSelect={setTheme}
icon={<Moon className="size-4" />}
label={t("theme_dark")}
/>
<ThemeOption
value="system"
current={theme}
onSelect={setTheme}
icon={<Monitor className="size-4" />}
label={t("theme_system")}
/>
</div>
<div className="my-1 h-px bg-(--color-border)" />
<div className="px-2 pt-2 pb-1 text-xs font-medium text-(--color-muted-foreground)">{t("language")}</div>
<ul role="listbox" className="max-h-60 overflow-y-auto p-1 text-sm">
{LANGUAGES.map((lang) => {
const isActive = lang.code === currentLang;
return (
<li key={lang.code}>
<button
type="button"
role="option"
aria-selected={isActive}
onClick={() => {
if (isActive) return;
const params = new URLSearchParams(window.location.search);
params.set("lang", lang.code);
window.location.search = "?" + params.toString();
}}
className={cn(
"flex w-full items-center rounded-sm px-2 py-1.5 text-left hover:bg-(--color-surface) hover:text-(--color-foreground)",
isActive ? "cursor-default" : "cursor-pointer",
)}
>
<Check className={cn("mr-2 size-4", isActive ? "opacity-100" : "opacity-0")} />
{lang.name}
</button>
</li>
);
})}
</ul>
</PopoverContent>
</Popover>
);
}
function ThemeOption({
value,
current,
onSelect,
icon,
label,
}: {
value: Theme;
current: Theme;
onSelect: (t: Theme) => void;
icon: React.ReactNode;
label: string;
}) {
const isActive = current === value;
return (
<button
type="button"
onClick={() => onSelect(value)}
aria-pressed={isActive}
className={cn(
"flex cursor-pointer flex-col items-center gap-1 rounded-md px-2 py-2 text-xs hover:bg-(--color-surface)",
isActive
? "bg-(--color-surface) text-(--color-foreground)"
: "text-(--color-muted-foreground) hover:text-(--color-foreground)",
)}
>
{icon}
{label}
</button>
);
}
+30
View File
@@ -0,0 +1,30 @@
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-(--color-border) bg-(--color-popover) p-1 text-(--color-popover-foreground) shadow-md outline-none",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };
+90
View File
@@ -0,0 +1,90 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "@fontsource-variable/geist";
@import "@fontsource-variable/geist-mono";
@theme inline {
--font-sans:
"Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, -apple-system, sans-serif;
--font-mono: "Geist Mono Variable", "PingFang SC", "Microsoft YaHei", Consolas, "Liberation Mono", Menlo, monospace;
}
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-background: initial;
--color-foreground: initial;
--color-card: initial;
--color-card-foreground: initial;
--color-popover: initial;
--color-popover-foreground: initial;
--color-primary: initial;
--color-primary-foreground: initial;
--color-secondary: initial;
--color-secondary-foreground: initial;
--color-surface: initial;
--color-muted-foreground: initial;
--color-destructive: initial;
--color-destructive-foreground: initial;
--color-border: initial;
--color-input: initial;
--color-ring: initial;
--radius: 0.5rem;
}
/* Theme palettes are derived from Happy Hues (https://www.happyhues.co):
light mode = palette 6, dark mode = palette 13. */
:root {
color-scheme: light dark;
--color-background: #fffffe;
--color-foreground: #2b2c34;
--color-card: #fffffe;
--color-card-foreground: #2b2c34;
--color-popover: #fffffe;
--color-popover-foreground: #2b2c34;
--color-primary: #6246ea;
--color-primary-foreground: #fffffe;
--color-secondary: #d1d1e9;
--color-secondary-foreground: #2b2c34;
--color-surface: #ececf6;
--color-muted-foreground: #5f6172;
--color-destructive: #c2392f;
--color-destructive-foreground: #fffffe;
--color-border: #2b2c34;
--color-input: #2b2c34;
--color-ring: #6246ea;
}
:root.dark {
color-scheme: dark;
--color-background: #16161a;
--color-foreground: #fffffe;
--color-card: #242629;
--color-card-foreground: #fffffe;
--color-popover: #242629;
--color-popover-foreground: #fffffe;
--color-primary: #7551e9;
--color-primary-foreground: #fffffe;
--color-secondary: #5c5f68;
--color-secondary-foreground: #fffffe;
--color-surface: #33363c;
--color-muted-foreground: #94a1b2;
--color-destructive: #d63653;
--color-destructive-foreground: #fffffe;
--color-border: #5c5f68;
--color-input: #5c5f68;
--color-ring: #7551e9;
}
@layer base {
html,
body {
background-color: var(--color-background);
color: var(--color-foreground);
}
body {
-webkit-font-smoothing: antialiased;
}
}
+87
View File
@@ -0,0 +1,87 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import bgBG from "@/locales/bg-BG.json";
import csCZ from "@/locales/cs-CZ.json";
import deDE from "@/locales/de-DE.json";
import enGB from "@/locales/en-GB.json";
import enUS from "@/locales/en-US.json";
import esES from "@/locales/es-ES.json";
import faIR from "@/locales/fa-IR.json";
import fiFI from "@/locales/fi-FI.json";
import frFR from "@/locales/fr-FR.json";
import glES from "@/locales/gl-ES.json";
import huHU from "@/locales/hu-HU.json";
import idID from "@/locales/id-ID.json";
import itIT from "@/locales/it-IT.json";
import jaJP from "@/locales/ja-JP.json";
import koKR from "@/locales/ko-KR.json";
import lvLV from "@/locales/lv-LV.json";
import mnMN from "@/locales/mn-MN.json";
import nlNL from "@/locales/nl-NL.json";
import plPL from "@/locales/pl-PL.json";
import ptBR from "@/locales/pt-BR.json";
import ptPT from "@/locales/pt-PT.json";
import roRO from "@/locales/ro-RO.json";
import ruRU from "@/locales/ru-RU.json";
import skSK from "@/locales/sk-SK.json";
import srSP from "@/locales/sr-SP.json";
import svSE from "@/locales/sv-SE.json";
import trTR from "@/locales/tr-TR.json";
import ukUA from "@/locales/uk-UA.json";
import viVN from "@/locales/vi-VN.json";
import zhCN from "@/locales/zh-CN.json";
import zhHK from "@/locales/zh-HK.json";
import zhTW from "@/locales/zh-TW.json";
// The server resolves the active locale (via cookie, Accept-Language, or
// ?lang query) and writes it into <html lang="…"> before the SPA boots.
// Trust that single source instead of re-deriving from cookies client-side.
function detectLang(): string {
if (typeof document === "undefined") return "en-US";
return document.documentElement.lang || "en-US";
}
// eslint-disable-next-line import/no-named-as-default-member
void i18n.use(initReactI18next).init({
resources: {
"bg-BG": { translation: bgBG },
"cs-CZ": { translation: csCZ },
"de-DE": { translation: deDE },
"en-GB": { translation: enGB },
"en-US": { translation: enUS },
"es-ES": { translation: esES },
"fa-IR": { translation: faIR },
"fi-FI": { translation: fiFI },
"fr-FR": { translation: frFR },
"gl-ES": { translation: glES },
"hu-HU": { translation: huHU },
"id-ID": { translation: idID },
"it-IT": { translation: itIT },
"ja-JP": { translation: jaJP },
"ko-KR": { translation: koKR },
"lv-LV": { translation: lvLV },
"mn-MN": { translation: mnMN },
"nl-NL": { translation: nlNL },
"pl-PL": { translation: plPL },
"pt-BR": { translation: ptBR },
"pt-PT": { translation: ptPT },
"ro-RO": { translation: roRO },
"ru-RU": { translation: ruRU },
"sk-SK": { translation: skSK },
"sr-SP": { translation: srSP },
"sv-SE": { translation: svSE },
"tr-TR": { translation: trTR },
"uk-UA": { translation: ukUA },
"vi-VN": { translation: viVN },
"zh-CN": { translation: zhCN },
"zh-HK": { translation: zhHK },
"zh-TW": { translation: zhTW },
},
lng: detectLang(),
fallbackLng: "en-US",
interpolation: { escapeValue: false, prefix: "{", suffix: "}" },
returnNull: false,
});
export default i18n;
+9
View File
@@ -0,0 +1,9 @@
import { useEffect } from "react";
const APP_NAME = "Gogs";
export function usePageTitle(title?: string) {
useEffect(() => {
document.title = title ? `${title} - ${APP_NAME}` : APP_NAME;
}, [title]);
}
+41
View File
@@ -0,0 +1,41 @@
import { useEffect, useState } from "react";
export type Theme = "light" | "dark" | "system";
const STORAGE_KEY = "gogs-theme";
function systemPrefersDark(): boolean {
return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
}
function readStoredTheme(): Theme {
if (typeof localStorage === "undefined") return "system";
const v = localStorage.getItem(STORAGE_KEY);
return v === "light" || v === "dark" || v === "system" ? v : "system";
}
function applyTheme(theme: Theme) {
const dark = theme === "dark" || (theme === "system" && systemPrefersDark());
document.documentElement.classList.toggle("dark", dark);
}
export function useTheme() {
const [theme, setThemeState] = useState<Theme>(readStoredTheme);
useEffect(() => {
applyTheme(theme);
if (theme !== "system") return;
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = () => applyTheme("system");
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, [theme]);
const setTheme = (next: Theme) => {
localStorage.setItem(STORAGE_KEY, next);
setThemeState(next);
applyTheme(next);
};
return { theme, setTheme };
}
+19
View File
@@ -0,0 +1,19 @@
// Read once at module load. The server injects the value via
// <meta name="sub-url"> in index.html, defaulting to "" when Gogs is served
// at the domain root.
const subURL = (() => {
if (typeof document === "undefined") return "";
const meta = document.querySelector('meta[name="sub-url"]');
return meta?.getAttribute("content") ?? "";
})();
// subUrl prefixes an internal absolute path with the deployment subpath so
// links work whether Gogs is mounted at "/" or behind a reverse proxy on a
// prefix like "/gogs". Pass paths that start with "/" (e.g. "/user/login").
// The result is canonicalized by trimming trailing slashes, so subUrl("/")
// returns "/gogs" (or "" at root), letting callers compare against
// location.pathname without juggling both "/gogs" and "/gogs/" forms.
export function subUrl(path: string): string {
const url = subURL + path;
return url.length > 1 ? url.replace(/\/+$/, "") : url;
}
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Безпроблемен собствен Git сървър",
"home": "Начало",
"explore": "Каталог",
"help": "Помощ",
"register": "Регистрация",
"sign_in": "Вход",
"settings": "Настройки",
"language": "Език",
"page_not_found": "Страницата не е намерена"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Snadná soukromá služba Git",
"home": "Domů",
"explore": "Procházet",
"help": "Nápověda",
"register": "Registrovat se",
"sign_in": "Přihlásit se",
"settings": "Nastavení",
"language": "Jazyk",
"page_not_found": "Page Not Found"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Ein einfacher, selbst gehosteter Git-Service",
"home": "Startseite",
"explore": "Erkunden",
"help": "Hilfe",
"register": "Registrieren",
"sign_in": "Anmelden",
"settings": "Einstellungen",
"language": "Sprache",
"page_not_found": "Seite nicht gefunden"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "A painless self-hosted Git service",
"home": "Home",
"explore": "Explore",
"help": "Help",
"register": "Register",
"sign_in": "Sign In",
"settings": "Settings",
"language": "Language",
"page_not_found": "Page Not Found"
}
+15
View File
@@ -0,0 +1,15 @@
{
"app_desc": "The painless way to host your own Git service",
"home": "Home",
"explore": "Explore",
"help": "Help",
"register": "Register",
"sign_in": "Sign in",
"settings": "Settings",
"language": "Language",
"page_not_found": "Page not found",
"theme": "Theme",
"theme_light": "Light",
"theme_dark": "Dark",
"theme_system": "System"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Un servicio de Git auto alojado y sin complicaciones",
"home": "Inicio",
"explore": "Explorar",
"help": "Ayuda",
"register": "Registro",
"sign_in": "Iniciar sesión",
"settings": "Configuraciones",
"language": "Idioma",
"page_not_found": "Página no encontrada"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "یک سرویس گیت بی‌درد سر و راحت",
"home": "صفحهٔ اصلی",
"explore": "گشت‌و‌گذار",
"help": "راهنما",
"register": "ثبت نام",
"sign_in": "ورود",
"settings": "تنظيمات",
"language": "زبان",
"page_not_found": "صفحه مورد نظر یافت نشد."
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Kivuton itse-hostattu Git palvelu",
"home": "Etusivu",
"explore": "Tutki",
"help": "Apua",
"register": "Rekisteröidy",
"sign_in": "Kirjaudu sisään",
"settings": "Asetukset",
"language": "Kieli",
"page_not_found": "Sivua ei löydy"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Un service Git auto-hébergé et indolore.",
"home": "Accueil",
"explore": "Explorer",
"help": "Aide",
"register": "S'inscrire",
"sign_in": "Connexion",
"settings": "Paramètres",
"language": "Langue",
"page_not_found": "Page non trouvée"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Un servizo de Git auto aloxado e sen complicacións",
"home": "Inicio",
"explore": "Explorar",
"help": "Axuda",
"register": "Rexistro",
"sign_in": "Iniciar sesión",
"settings": "Configuracións",
"language": "Idioma",
"page_not_found": "Page Not Found"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Fájdalommentes, saját gépre telepíthető Git szolgáltatás",
"home": "Kezdőlap",
"explore": "Felfedezés",
"help": "Súgó",
"register": "Regisztráció",
"sign_in": "Bejelentkezés",
"settings": "Beállítások",
"language": "Nyelv",
"page_not_found": "Az oldal nem található"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Sebuah layanan Git hosting-pribadi yang mudah",
"home": "Halaman utama",
"explore": "Jelajahi",
"help": "Bantuan",
"register": "Daftar",
"sign_in": "Masuk",
"settings": "Pengaturan",
"language": "Bahasa",
"page_not_found": "Halaman tidak ditemukan"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Un servizio Git auto-ospitato pronto all'uso",
"home": "Home",
"explore": "Esplora",
"help": "Aiuto",
"register": "Registrati",
"sign_in": "Accedi",
"settings": "Impostazioni",
"language": "Lingua",
"page_not_found": "Pagina Non Trovata"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Go言語で実装したセルフホストGitサービス",
"home": "ホーム",
"explore": "エクスプローラ",
"help": "ヘルプ",
"register": "登録",
"sign_in": "サインイン",
"settings": "設定",
"language": "言語",
"page_not_found": "ページが見つかりません"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "편리한 설치형 Git 서비스",
"home": "홈",
"explore": "탐색",
"help": "도움말",
"register": "가입하기",
"sign_in": "로그인",
"settings": "설정",
"language": "언어",
"page_not_found": "페이지를 찾을 수 없음"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Viegli uzstādāms Git serviss, kas rakstīts valodā Go",
"home": "Sākums",
"explore": "Izpētīt",
"help": "Palīdzība",
"register": "Reģistrēties",
"sign_in": "Pierakstīties",
"settings": "Iestatījumi",
"language": "Valoda",
"page_not_found": "Page Not Found"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Хамгийн хялбар GIT үйлчилгээ",
"home": "Эхлэл",
"explore": "Бүгдийг харах",
"help": "Тусламж",
"register": "Бүртгүүлэх",
"sign_in": "Нэвтрэх",
"settings": "Тохиргоо",
"language": "Хэл",
"page_not_found": "Хуудас олдсонгүй"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Een eenvoudige zelfgehoste Git service geschreven in Go",
"home": "Home",
"explore": "Verkennen",
"help": "Help",
"register": "Registreren",
"sign_in": "Inloggen",
"settings": "Instellingen",
"language": "Taal",
"page_not_found": "Pagina niet gevonden"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Bezbolesna usługa Git na własnym serwerze",
"home": "Strona główna",
"explore": "Odkrywaj",
"help": "Pomoc",
"register": "Zarejestruj się",
"sign_in": "Zaloguj się",
"settings": "Ustawienia",
"language": "Język",
"page_not_found": "Strona nie została znaleziona"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Um serviço de Git auto-hospedado e amigável escrito em Go",
"home": "Página inicial",
"explore": "Explorar",
"help": "Ajuda",
"register": "Registrar",
"sign_in": "Entrar",
"settings": "Configurações",
"language": "Idioma",
"page_not_found": "Página Não Encontrada"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Um Serviço Git fácil e simples",
"home": "Página Principal",
"explore": "Explorar",
"help": "Ajuda",
"register": "Registe-se",
"sign_in": "Iniciar Sessão",
"settings": "Definições",
"language": "Língua",
"page_not_found": "Página Não Encontrada"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Un serviciu Git auto-găzduit fără dureri de cap",
"home": "Acasă",
"explore": "Explorează",
"help": "Ajutor",
"register": "Înregistrare",
"sign_in": "Autentificare",
"settings": "Setări",
"language": "Limba",
"page_not_found": "Pagina nu a fost găsită"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Удобная служба для собственного Git-репозитория",
"home": "Главная",
"explore": "Обзор",
"help": "Помощь",
"register": "Регистрация",
"sign_in": "Вход",
"settings": "Настройки",
"language": "Язык",
"page_not_found": "Страница не найдена"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "A painless self-hosted Git service",
"home": "Domovská stránka",
"explore": "Prehľadávať",
"help": "Pomoc",
"register": "Registrovať",
"sign_in": "Prihlásiť sa",
"settings": "Nastavenia",
"language": "Jazyk",
"page_not_found": "Page Not Found"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Једноставан Git сервис",
"home": "Почетна",
"explore": "Преглед",
"help": "Помоћ",
"register": "Регистрација",
"sign_in": "Пријавите се",
"settings": "Подешавања",
"language": "Језик",
"page_not_found": "Page Not Found"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "En smidig Git-tjänst du kör själv skriven i Go",
"home": "Startsida",
"explore": "Utforska",
"help": "Hjälp",
"register": "Registrera dig",
"sign_in": "Logga in",
"settings": "inställningar",
"language": "Språk",
"page_not_found": "Sidan hittades inte"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Kendi sunucunuzda barındırabileceğiniz zahmetsiz bir Git servisi",
"home": "Ana Sayfa",
"explore": "Keşfet",
"help": "Yardım",
"register": "Üye Ol",
"sign_in": "Giriş Yap",
"settings": "Ayarlar",
"language": "Dil",
"page_not_found": "Sayfa Bulunamadı"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Зручний сервіс власного Git хостингу",
"home": "Головна сторінка",
"explore": "Огляд",
"help": "Довідка",
"register": "Реєстрація",
"sign_in": "Увійти",
"settings": "Налаштування",
"language": "Мова",
"page_not_found": "Сторінку не знайдено"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "Một server lưu trữ Git tự host dễ dàng",
"home": "Trang chủ",
"explore": "Khám phá",
"help": "Trợ giúp",
"register": "Đăng ký",
"sign_in": "Đăng nhập",
"settings": "Cài đặt",
"language": "Ngôn ngữ",
"page_not_found": "Không tìm thấy trang này!"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "一款极易搭建的自助 Git 服务",
"home": "首页",
"explore": "发现",
"help": "帮助",
"register": "注册",
"sign_in": "登录",
"settings": "帐户设置",
"language": "语言选项",
"page_not_found": "页面未找到"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "基於 Go 語言的自助 Git 服務",
"home": "首頁",
"explore": "探索",
"help": "說明",
"register": "註冊",
"sign_in": "登入",
"settings": "設定",
"language": "語言",
"page_not_found": "Page Not Found"
}
+11
View File
@@ -0,0 +1,11 @@
{
"app_desc": "一款極易搭建的自助 Git 服務",
"home": "首頁",
"explore": "探索",
"help": "說明",
"register": "註冊",
"sign_in": "登入",
"settings": "設定",
"language": "語言",
"page_not_found": "找不到頁面"
}
+10
View File
@@ -0,0 +1,10 @@
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "./index.css";
import "./lib/i18n";
const root = document.getElementById("root");
if (root) {
createRoot(root).render(<App />);
}
+75
View File
@@ -0,0 +1,75 @@
import { useTranslation } from "react-i18next";
import { usePageTitle } from "@/lib/page-title";
import { subUrl } from "@/lib/url";
export function Landing() {
const { t } = useTranslation();
usePageTitle();
return (
<main className="flex flex-1 items-center justify-center px-4 py-10 sm:px-6 sm:py-16">
<div className="w-full max-w-2xl">
<div className="rounded-lg border border-(--color-border) bg-(--color-surface)/40 font-mono shadow-xs">
<div className="flex items-center gap-1.5 border-b border-(--color-border) px-3 py-2 sm:px-4 sm:py-2.5">
<span className="size-2.5 rounded-full bg-(--color-destructive)/70" />
<span className="size-2.5 rounded-full bg-(--color-warning,oklch(0.795_0.184_86.047))/70" />
<span className="size-2.5 rounded-full bg-(--color-foreground)/20" />
<span className="ml-2 text-xs text-(--color-muted-foreground) sm:ml-3">gogs zsh</span>
</div>
<pre className="px-4 py-4 text-xs leading-relaxed break-all whitespace-pre-wrap text-(--color-foreground) sm:px-5 sm:py-5 sm:text-sm">
<span className="text-(--color-muted-foreground)">$ </span>
<span>cat /etc/motd</span>
{"\n"}
<img
src={subUrl("/img/banner-light.svg")}
alt="Gogs"
width="775"
height="294"
className="mx-auto block max-w-[280px] dark:hidden sm:max-w-sm"
/>
<img
src={subUrl("/img/banner-dark.svg")}
alt="Gogs"
width="775"
height="294"
className="mx-auto hidden max-w-[280px] dark:block sm:max-w-sm"
/>
{"\n"}
<span className="block text-center font-sans text-base text-(--color-muted-foreground) sm:text-lg">
{t("app_desc")}
</span>
{"\n"}
<span className="text-(--color-muted-foreground)">$ </span>
<span>gogs help</span>
{"\n"}
<CmdLink href="/user/login" cmd="sign-in" desc={t("sign_in")} />
{"\n"}
<CmdLink href="/user/sign_up" cmd="sign-up" desc={t("register")} />
{"\n"}
<CmdLink href="/explore/repos" cmd="explore" desc={t("explore")} />
{"\n"}
<CmdLink href="https://gogs.io" cmd="help" desc={t("help")} external />
{"\n"}
{"\n"}
<span className="text-(--color-muted-foreground)">$ </span>
<span className="inline-block w-2 animate-pulse bg-(--color-foreground) align-baseline"> </span>
</pre>
</div>
</div>
</main>
);
}
function CmdLink({ href, cmd, desc, external }: { href: string; cmd: string; desc: string; external?: boolean }) {
return (
<a
href={external ? href : subUrl(href)}
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
className="group inline-flex items-baseline gap-2 rounded-sm hover:bg-(--color-surface) hover:text-(--color-foreground)"
>
<span className="inline-block w-16 text-(--color-foreground) sm:w-20">{cmd}</span>
<span className="text-(--color-muted-foreground) group-hover:text-(--color-foreground)/80"> {desc}</span>
<span className="text-(--color-muted-foreground) group-hover:text-(--color-foreground)"></span>
</a>
);
}
+34
View File
@@ -0,0 +1,34 @@
import { useTranslation } from "react-i18next";
import { usePageTitle } from "@/lib/page-title";
export function NotFound() {
const { t } = useTranslation();
usePageTitle(t("page_not_found"));
const path = typeof window === "undefined" ? "/" : window.location.pathname;
return (
<main className="flex flex-1 items-center justify-center px-4 py-10 sm:px-6 sm:py-16">
<div className="w-full max-w-2xl">
<div className="rounded-lg border border-(--color-border) bg-(--color-surface)/40 font-mono shadow-xs">
<div className="flex items-center gap-1.5 border-b border-(--color-border) px-3 py-2 sm:px-4 sm:py-2.5">
<span className="size-2.5 rounded-full bg-(--color-destructive)/70" />
<span className="size-2.5 rounded-full bg-(--color-warning,oklch(0.795_0.184_86.047))/70" />
<span className="size-2.5 rounded-full bg-(--color-foreground)/20" />
<span className="ml-2 text-xs text-(--color-muted-foreground) sm:ml-3">gogs zsh</span>
</div>
<pre className="px-4 py-4 text-xs leading-relaxed break-all whitespace-pre-wrap text-(--color-foreground) sm:px-5 sm:py-5 sm:text-sm">
<span className="text-(--color-muted-foreground)">$ </span>
<span>gogs show {path}</span>
{"\n"}
<span className="text-(--color-destructive)">fatal:</span> pathspec &apos;{path}&apos; did not match any
files known to gogs
{"\n"}
{"\n"}
<span className="text-(--color-muted-foreground)">$ </span>
<span className="inline-block w-2 animate-pulse bg-(--color-foreground) align-baseline"> </span>
</pre>
</div>
</div>
</main>
);
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
+4
View File
@@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}
+21
View File
@@ -0,0 +1,21 @@
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "src"),
},
},
server: {
port: 5173,
},
build: {
outDir: "../public/dist",
emptyOutDir: true,
},
});