mirror of
https://github.com/gogs/gogs.git
synced 2026-05-28 21:30:36 +00:00
feat: introduce React web frontend and migrate home + 404 pages (#8276)
This commit is contained in:
@@ -11,5 +11,10 @@ scripts/**
|
||||
.gitignore
|
||||
Dockerfile*
|
||||
gogs
|
||||
node_modules
|
||||
**/node_modules
|
||||
public/dist
|
||||
**/*.tsbuildinfo
|
||||
**/.vite
|
||||
|
||||
!Taskfile.yml
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
$schema: "https://moonrepo.dev/schemas/workspace.json"
|
||||
|
||||
projects:
|
||||
gogs: "."
|
||||
web: "web"
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "gogs",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.1.3"
|
||||
}
|
||||
Generated
+4001
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
- "web"
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build prod
|
||||
|
||||
package public
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed all:dist
|
||||
var WebAssets embed.FS
|
||||
@@ -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>
|
||||
|
||||
@@ -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" .}}
|
||||
@@ -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" .}}
|
||||
@@ -0,0 +1,2 @@
|
||||
*.tsbuildinfo
|
||||
.vite
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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.
|
||||
@@ -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 }],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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 |
@@ -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)`);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "Безпроблемен собствен Git сървър",
|
||||
"home": "Начало",
|
||||
"explore": "Каталог",
|
||||
"help": "Помощ",
|
||||
"register": "Регистрация",
|
||||
"sign_in": "Вход",
|
||||
"settings": "Настройки",
|
||||
"language": "Език",
|
||||
"page_not_found": "Страницата не е намерена"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "یک سرویس گیت بیدرد سر و راحت",
|
||||
"home": "صفحهٔ اصلی",
|
||||
"explore": "گشتوگذار",
|
||||
"help": "راهنما",
|
||||
"register": "ثبت نام",
|
||||
"sign_in": "ورود",
|
||||
"settings": "تنظيمات",
|
||||
"language": "زبان",
|
||||
"page_not_found": "صفحه مورد نظر یافت نشد."
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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ó"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "Go言語で実装したセルフホストGitサービス",
|
||||
"home": "ホーム",
|
||||
"explore": "エクスプローラ",
|
||||
"help": "ヘルプ",
|
||||
"register": "登録",
|
||||
"sign_in": "サインイン",
|
||||
"settings": "設定",
|
||||
"language": "言語",
|
||||
"page_not_found": "ページが見つかりません"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "편리한 설치형 Git 서비스",
|
||||
"home": "홈",
|
||||
"explore": "탐색",
|
||||
"help": "도움말",
|
||||
"register": "가입하기",
|
||||
"sign_in": "로그인",
|
||||
"settings": "설정",
|
||||
"language": "언어",
|
||||
"page_not_found": "페이지를 찾을 수 없음"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "Хамгийн хялбар GIT үйлчилгээ",
|
||||
"home": "Эхлэл",
|
||||
"explore": "Бүгдийг харах",
|
||||
"help": "Тусламж",
|
||||
"register": "Бүртгүүлэх",
|
||||
"sign_in": "Нэвтрэх",
|
||||
"settings": "Тохиргоо",
|
||||
"language": "Хэл",
|
||||
"page_not_found": "Хуудас олдсонгүй"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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ă"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "Удобная служба для собственного Git-репозитория",
|
||||
"home": "Главная",
|
||||
"explore": "Обзор",
|
||||
"help": "Помощь",
|
||||
"register": "Регистрация",
|
||||
"sign_in": "Вход",
|
||||
"settings": "Настройки",
|
||||
"language": "Язык",
|
||||
"page_not_found": "Страница не найдена"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "Једноставан Git сервис",
|
||||
"home": "Почетна",
|
||||
"explore": "Преглед",
|
||||
"help": "Помоћ",
|
||||
"register": "Регистрација",
|
||||
"sign_in": "Пријавите се",
|
||||
"settings": "Подешавања",
|
||||
"language": "Језик",
|
||||
"page_not_found": "Page Not Found"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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ı"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "Зручний сервіс власного Git хостингу",
|
||||
"home": "Головна сторінка",
|
||||
"explore": "Огляд",
|
||||
"help": "Довідка",
|
||||
"register": "Реєстрація",
|
||||
"sign_in": "Увійти",
|
||||
"settings": "Налаштування",
|
||||
"language": "Мова",
|
||||
"page_not_found": "Сторінку не знайдено"
|
||||
}
|
||||
@@ -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!"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "一款极易搭建的自助 Git 服务",
|
||||
"home": "首页",
|
||||
"explore": "发现",
|
||||
"help": "帮助",
|
||||
"register": "注册",
|
||||
"sign_in": "登录",
|
||||
"settings": "帐户设置",
|
||||
"language": "语言选项",
|
||||
"page_not_found": "页面未找到"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "基於 Go 語言的自助 Git 服務",
|
||||
"home": "首頁",
|
||||
"explore": "探索",
|
||||
"help": "說明",
|
||||
"register": "註冊",
|
||||
"sign_in": "登入",
|
||||
"settings": "設定",
|
||||
"language": "語言",
|
||||
"page_not_found": "Page Not Found"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_desc": "一款極易搭建的自助 Git 服務",
|
||||
"home": "首頁",
|
||||
"explore": "探索",
|
||||
"help": "說明",
|
||||
"register": "註冊",
|
||||
"sign_in": "登入",
|
||||
"settings": "設定",
|
||||
"language": "語言",
|
||||
"page_not_found": "找不到頁面"
|
||||
}
|
||||
@@ -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 />);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 '{path}' 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>
|
||||
);
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user