diff options
| -rw-r--r-- | .env.example | 24 | ||||
| -rw-r--r-- | README.md | 45 | ||||
| -rw-r--r-- | bot/Dockerfile | 13 | ||||
| -rw-r--r-- | bot/go.mod | 3 | ||||
| -rw-r--r-- | bot/main.go | 464 | ||||
| -rw-r--r-- | docker-compose.yml | 44 | ||||
| -rw-r--r-- | ssh/Dockerfile | 13 | ||||
| -rwxr-xr-x | ssh/entrypoint.sh | 20 | ||||
| -rwxr-xr-x | ssh/git-ns | 25 | ||||
| -rw-r--r-- | web/Dockerfile | 16 | ||||
| -rw-r--r-- | web/MonaspaceKrypton.woff2 | bin | 0 -> 445408 bytes | |||
| -rw-r--r-- | web/cgitrc | 14 | ||||
| -rw-r--r-- | web/dark.css | 983 | ||||
| -rwxr-xr-x | web/entrypoint.sh | 7 | ||||
| -rw-r--r-- | web/nginx.conf | 39 | ||||
| -rw-r--r-- | web/pg-favicon.png | bin | 0 -> 6851 bytes | |||
| -rw-r--r-- | web/pg-logo.png | bin | 0 -> 29948 bytes |
17 files changed, 1710 insertions, 0 deletions
diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ed5ac2f --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# --- скопируй в .env и заполни --- + +# токен бота от @BotFather +BOT_TOKEN=123456789:AA... + +# супер-админы: TG user-id через запятую (всегда можно). Пусто = нет. +ADMINS= + +# ВАЙТЛИСТ: id чата, чьи участники допущены к боту. Бот должен быть добавлен в этот чат. +# Пусто = без вайтлиста (если и ADMINS пуст — открыто всем). +WHITELIST_CHAT= + +# публичный адрес для clone/browse-ссылок (HTTP), что шлёт бот +PUBLIC_URL=https://git.ancap.in.ua + +# ssh-адрес для ssh-clone-ссылок (git@host). Пусто = ssh-ссылки не показывать. +# порт хоста для sshd (SSH через HTTP-туннель не ходит — либо этот порт наружу, либо свой TCP-туннель) +SSH_PORT=2222 + +# cloudflared tunnel token: +# Cloudflare Zero Trust -> Networks -> Tunnels -> Create tunnel -> +# public hostname: git.ancap.in.ua -> service: http://web:80 -> скопировать token +TUNNEL_TOKEN=eyJhIjoi... diff --git a/README.md b/README.md new file mode 100644 index 0000000..078f2a8 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Пацанский Гит 🏴☠️ + +Минимальный self-hosted git: **телеграм-бот** рулит репозиториями, **cgit** их показывает, +**git-http-backend** отдаёт clone/push, **cloudflared** пробрасывает наружу. Всё в докере. + +``` +tg-бот ──создаёт/удаляет──▶ /repos (bare) ◀──показывает── cgit ──┐ + ├─ nginx ─ cloudflared ─▶ git.ancap.in.ua +git push/clone ──HTTP──▶ git-http-backend ────────────────────────┘ +``` + +## Запуск + +1. `cp .env.example .env` и заполни: `BOT_TOKEN`, `ADMINS`, `PUBLIC_URL`, `TUNNEL_TOKEN`. +2. В Cloudflare Zero Trust создай туннель, public hostname `git.ancap.in.ua` → service `http://web:80`, вставь token в `.env`. +3. `docker compose up -d --build` + +## Бот + +Репозитории живут под **неймспейсом юзера** (`<юзер>/<репо>.git`, юзер = @username). +- `/new имя [описание]` — создать репо у себя +- `/mine` — свои репо · `/list` — все (по владельцам) · `/info имя` +- `/del имя confirm` — удалить своё +- `/addkey ssh-ed25519 AAAA... коммент` — добавить ssh-ключ +- `/keys` · `/delkey N` + +Вайтлист: `WHITELIST_CHAT` = id чата, бот должен быть в него добавлен. + +## Работа с репо + +По HTTP (через cloudflared-туннель): +``` +git clone https://git.ancap.in.ua/юзер/имя.git +``` + +По SSH (ключи добавляются ботом, пуш только в свой неймспейс): +``` +git clone ssh://[email protected]:2222/юзер/имя.git +# или: git clone [email protected]:юзер/имя.git (для порта 22) +``` +Браузить (cgit, тёмная тема): `https://git.ancap.in.ua/юзер/имя/` + +⚠️ **SSH через cloudflared HTTP-туннель не ходит** — для ssh пробрось порт `SSH_PORT` +наружу напрямую или отдельным TCP-туннелем. HTTP-пуш открыт (`GIT_HTTP_EXPORT_ALL`); +хочешь авторизацию и по HTTP — навесь `auth_basic` в `web/nginx.conf`. diff --git a/bot/Dockerfile b/bot/Dockerfile new file mode 100644 index 0000000..fb89d33 --- /dev/null +++ b/bot/Dockerfile @@ -0,0 +1,13 @@ +# сборка Go-бота в статик-бинарь +FROM golang:1.22-alpine AS build +WORKDIR /src +COPY go.mod . +COPY main.go . +RUN CGO_ENABLED=0 go build -o /gitbot -ldflags="-s -w" . + +FROM alpine:3.20 +RUN apk add --no-cache git ca-certificates tzdata +COPY --from=build /gitbot /usr/local/bin/gitbot +ENV REPOS_DIR=/repos +VOLUME /repos +ENTRYPOINT ["/usr/local/bin/gitbot"] diff --git a/bot/go.mod b/bot/go.mod new file mode 100644 index 0000000..7a4f81e --- /dev/null +++ b/bot/go.mod @@ -0,0 +1,3 @@ +module gitbot + +go 1.21 diff --git a/bot/main.go b/bot/main.go new file mode 100644 index 0000000..61e1621 --- /dev/null +++ b/bot/main.go @@ -0,0 +1,464 @@ +// Пацанский Гит — Telegram-бот. Репозитории под неймспейсом юзера (<user>/<repo>.git), +// управление SSH-ключами (генерит authorized_keys для sshd), вайтлист по чату. +// Stdlib-only. +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "time" +) + +var ( + token = os.Getenv("BOT_TOKEN") + reposDir = envDefault("REPOS_DIR", "/repos") + keysDir = envDefault("KEYS_DIR", "/keys") + publicURL = strings.TrimRight(envDefault("PUBLIC_URL", "http://localhost"), "/") + sshHost = os.Getenv("SSH_HOST") // напр. [email protected] (для ssh clone-url; пусто = не показывать) + nsCmd = envDefault("GIT_NS", "git-ns") // форс-команда в authorized_keys (в докере "git-ns" на PATH, живьём — абс.путь) + admins = parseAdmins(os.Getenv("ADMINS")) + whitelistChat = os.Getenv("WHITELIST_CHAT") + api = "https://api.telegram.org/bot" + token + nameRe = regexp.MustCompile(`^[a-zA-Z0-9._-]{1,64}$`) + userRe = regexp.MustCompile(`[^a-z0-9._-]+`) + sshKeyRe = regexp.MustCompile(`^(ssh-(rsa|ed25519|dss)|ecdsa-sha2-\S+)\s+[A-Za-z0-9+/=]+(\s+\S+)?$`) + mu sync.Mutex // защищает файловые операции с ключами/authorized_keys +) + +func envDefault(k, d string) string { + if v := os.Getenv(k); v != "" { + return v + } + return d +} + +func parseAdmins(s string) map[int64]bool { + m := map[int64]bool{} + for _, p := range strings.Split(s, ",") { + if id, err := strconv.ParseInt(strings.TrimSpace(p), 10, 64); err == nil { + m[id] = true + } + } + return m +} + +type Update struct { + UpdateID int64 `json:"update_id"` + Message *struct { + From struct { + ID int64 `json:"id"` + Username string `json:"username"` + } `json:"from"` + Chat struct { + ID int64 `json:"id"` + } `json:"chat"` + Text string `json:"text"` + } `json:"message"` +} + +func tg(method string, params url.Values) ([]byte, error) { + resp, err := http.PostForm(api+"/"+method, params) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return io.ReadAll(resp.Body) +} + +func send(chat int64, text string) { + tg("sendMessage", url.Values{"chat_id": {strconv.FormatInt(chat, 10)}, + "text": {text}, "parse_mode": {"HTML"}, "disable_web_page_preview": {"true"}}) +} + +// имя-неймспейс юзера: @username в нижнем регистре, иначе u<id> +func nsOf(username string, id int64) string { + u := strings.ToLower(strings.TrimSpace(username)) + if u == "" { + return "u" + strconv.FormatInt(id, 10) + } + u = userRe.ReplaceAllString(u, "-") + if u == "" { + return "u" + strconv.FormatInt(id, 10) + } + return u +} + +func repoPath(ns, name string) string { return filepath.Join(reposDir, ns, name+".git") } + +// ---- вайтлист по членству в чате ---- +type memEntry struct { + ok bool + exp time.Time +} + +var ( + memCache = map[int64]memEntry{} + memCacheMu sync.Mutex +) + +func authorized(id int64) bool { + if admins[id] { + return true + } + if whitelistChat != "" { + return isChatMember(id) + } + return len(admins) == 0 +} + +func isChatMember(id int64) bool { + memCacheMu.Lock() + if e, ok := memCache[id]; ok && time.Now().Before(e.exp) { + memCacheMu.Unlock() + return e.ok + } + memCacheMu.Unlock() + ok := false + if b, err := tg("getChatMember", url.Values{"chat_id": {whitelistChat}, "user_id": {strconv.FormatInt(id, 10)}}); err == nil { + var r struct { + OK bool `json:"ok"` + Result struct { + Status string `json:"status"` + } `json:"result"` + } + if json.Unmarshal(b, &r) == nil && r.OK { + switch r.Result.Status { + case "creator", "administrator", "member", "restricted": + ok = true + } + } + } + memCacheMu.Lock() + memCache[id] = memEntry{ok: ok, exp: time.Now().Add(2 * time.Minute)} + memCacheMu.Unlock() + return ok +} + +func main() { + if token == "" { + log.Fatal("BOT_TOKEN не задан") + } + os.MkdirAll(reposDir, 0o755) + os.MkdirAll(filepath.Join(keysDir, "users"), 0o700) + rebuildAuthorizedKeys() + log.Printf("gitbot up · repos=%s · keys=%s · public=%s · whitelist=%s", reposDir, keysDir, publicURL, whitelistChat) + var offset int64 + for { + b, err := tg("getUpdates", url.Values{"offset": {strconv.FormatInt(offset+1, 10)}, "timeout": {"30"}}) + if err != nil { + time.Sleep(3 * time.Second) + continue + } + var r struct { + OK bool `json:"ok"` + Result []Update `json:"result"` + } + if json.Unmarshal(b, &r) != nil || !r.OK { + time.Sleep(2 * time.Second) + continue + } + for _, u := range r.Result { + offset = u.UpdateID + if u.Message != nil && u.Message.Text != "" { + handle(u) + } + } + } +} + +func handle(u Update) { + chat, uid, uname := u.Message.Chat.ID, u.Message.From.ID, u.Message.From.Username + ns := nsOf(uname, uid) + fields := strings.Fields(strings.TrimSpace(u.Message.Text)) + if len(fields) == 0 { + return + } + cmd := fields[0] + if i := strings.Index(cmd, "@"); i >= 0 { + cmd = cmd[:i] + } + // для команд с многострочным аргументом (ключи) берём полный текст после команды + rest := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(u.Message.Text), fields[0])) + args := fields[1:] + + switch cmd { + case "/start", "/help": + send(chat, helpText()) + case "/new": + if !authorized(uid) { + send(chat, "⛔ Только участники чата.") + return + } + if len(args) < 1 || !nameRe.MatchString(args[0]) { + send(chat, "Использование: <code>/new имя [описание]</code>") + return + } + if err := createRepo(ns, args[0], strings.Join(args[1:], " ")); err != nil { + send(chat, "❌ "+err.Error()) + return + } + send(chat, repoInfo(ns, args[0], "✅ Репо создан.\n")) + case "/list": + send(chat, listAll()) + case "/mine": + send(chat, listMine(ns)) + case "/info": + if len(args) < 1 { + send(chat, "Использование: <code>/info имя</code> или <code>/info юзер/имя</code>") + return + } + o, n := ns, args[0] + if strings.Contains(args[0], "/") { + p := strings.SplitN(args[0], "/", 2) + o, n = p[0], strings.TrimSuffix(p[1], ".git") + } + send(chat, repoInfo(o, n, "")) + case "/del", "/rm": + if !authorized(uid) { + send(chat, "⛔ Только участники чата.") + return + } + if len(args) < 1 || !nameRe.MatchString(args[0]) { + send(chat, "Использование: <code>/del имя confirm</code> (только своё)") + return + } + if len(args) < 2 || args[1] != "confirm" { + send(chat, fmt.Sprintf("⚠ Удалить <b>%s/%s</b>? Подтверди: <code>/del %s confirm</code>", ns, args[0], args[0])) + return + } + if err := delRepo(ns, args[0]); err != nil { + send(chat, "❌ "+err.Error()) + return + } + send(chat, "🗑 Удалён <b>"+ns+"/"+args[0]+"</b>.") + case "/addkey": + if !authorized(uid) { + send(chat, "⛔ Только участники чата.") + return + } + if err := addKey(ns, rest); err != nil { + send(chat, "❌ "+err.Error()) + return + } + s := "" + if sshHost != "" { + s = fmt.Sprintf("\nSSH clone: <code>%s:%s/имя.git</code>", sshHost, ns) + } + send(chat, "🔑 Ключ добавлен."+s) + case "/keys": + send(chat, listKeys(ns)) + case "/delkey": + if len(args) < 1 { + send(chat, "Использование: <code>/delkey N</code> (номер из /keys)") + return + } + n, _ := strconv.Atoi(args[0]) + if err := delKey(ns, n); err != nil { + send(chat, "❌ "+err.Error()) + return + } + send(chat, "🔑 Ключ #"+args[0]+" удалён.") + } +} + +func createRepo(ns, name, desc string) error { + p := repoPath(ns, name) + if _, err := os.Stat(p); err == nil { + return fmt.Errorf("репо уже есть") + } + os.MkdirAll(filepath.Dir(p), 0o755) + if out, err := exec.Command("git", "init", "--bare", "--initial-branch=main", p).CombinedOutput(); err != nil { + return fmt.Errorf("git init: %s", strings.TrimSpace(string(out))) + } + if desc == "" { + desc = name + } + os.WriteFile(filepath.Join(p, "description"), []byte(desc+"\n"), 0o644) + os.WriteFile(filepath.Join(p, "git-daemon-export-ok"), []byte(""), 0o644) + exec.Command("git", "--git-dir", p, "config", "http.receivepack", "true").Run() + return nil +} + +func delRepo(ns, name string) error { + p := repoPath(ns, name) + if _, err := os.Stat(p); err != nil { + return fmt.Errorf("нет такого репо в твоём неймспейсе") + } + return os.RemoveAll(p) +} + +func reposOf(ns string) []string { + ents, _ := os.ReadDir(filepath.Join(reposDir, ns)) + var out []string + for _, e := range ents { + if e.IsDir() && strings.HasSuffix(e.Name(), ".git") { + out = append(out, strings.TrimSuffix(e.Name(), ".git")) + } + } + sort.Strings(out) + return out +} + +func listMine(ns string) string { + rs := reposOf(ns) + if len(rs) == 0 { + return "У тебя (<b>" + ns + "</b>) нет репо. Создай: <code>/new имя</code>" + } + var sb strings.Builder + fmt.Fprintf(&sb, "📦 Твои репо (%s): %d\n", ns, len(rs)) + for _, n := range rs { + sb.WriteString("• " + n + "\n") + } + return sb.String() +} + +func listAll() string { + ents, _ := os.ReadDir(reposDir) + var owners []string + for _, e := range ents { + if e.IsDir() { + owners = append(owners, e.Name()) + } + } + sort.Strings(owners) + var sb strings.Builder + total := 0 + for _, o := range owners { + rs := reposOf(o) + if len(rs) == 0 { + continue + } + total += len(rs) + sb.WriteString("<b>" + o + "/</b>\n") + for _, n := range rs { + sb.WriteString(" • " + n + "\n") + } + } + if total == 0 { + return "Репозиториев пока нет." + } + return fmt.Sprintf("📦 Всего репо: %d\n", total) + sb.String() +} + +func repoInfo(ns, name, prefix string) string { + p := repoPath(ns, name) + if _, err := os.Stat(p); err != nil { + return "нет такого репо" + } + desc, _ := os.ReadFile(filepath.Join(p, "description")) + s := fmt.Sprintf("%s<b>%s/%s</b>\n%s\nHTTP: <code>%s/%s/%s.git</code>\nбраузить: %s/%s/%s/", + prefix, ns, name, strings.TrimSpace(string(desc)), publicURL, ns, name, publicURL, ns, name) + if sshHost != "" { + s += fmt.Sprintf("\nSSH: <code>%s:%s/%s.git</code>", sshHost, ns, name) + } + return s +} + +// ---- SSH-ключи ---- +func userKeysFile(ns string) string { return filepath.Join(keysDir, "users", ns) } + +func readKeys(ns string) []string { + b, _ := os.ReadFile(userKeysFile(ns)) + var out []string + for _, l := range strings.Split(string(b), "\n") { + if strings.TrimSpace(l) != "" { + out = append(out, strings.TrimSpace(l)) + } + } + return out +} + +func addKey(ns, key string) error { + key = strings.TrimSpace(strings.ReplaceAll(key, "\n", " ")) + key = strings.Join(strings.Fields(key), " ") + if !sshKeyRe.MatchString(key) { + return fmt.Errorf("не похоже на ssh-pubkey (ssh-ed25519/ssh-rsa/ecdsa...)") + } + mu.Lock() + defer mu.Unlock() + keys := readKeys(ns) + for _, k := range keys { + if strings.Fields(k)[1] == strings.Fields(key)[1] { + return fmt.Errorf("такой ключ уже добавлен") + } + } + keys = append(keys, key) + os.WriteFile(userKeysFile(ns), []byte(strings.Join(keys, "\n")+"\n"), 0o600) + rebuildAuthorizedKeys() + return nil +} + +func delKey(ns string, n int) error { + mu.Lock() + defer mu.Unlock() + keys := readKeys(ns) + if n < 1 || n > len(keys) { + return fmt.Errorf("нет ключа #%d (см. /keys)", n) + } + keys = append(keys[:n-1], keys[n:]...) + os.WriteFile(userKeysFile(ns), []byte(strings.Join(keys, "\n")+"\n"), 0o600) + rebuildAuthorizedKeys() + return nil +} + +func listKeys(ns string) string { + keys := readKeys(ns) + if len(keys) == 0 { + return "У тебя нет ключей. Добавь: <code>/addkey ssh-ed25519 AAAA... comment</code>" + } + var sb strings.Builder + sb.WriteString("🔑 Твои ключи:\n") + for i, k := range keys { + f := strings.Fields(k) + typ, blob := f[0], f[1] + short := blob + if len(blob) > 20 { + short = blob[:10] + "…" + blob[len(blob)-8:] + } + cmt := "" + if len(f) > 2 { + cmt = " " + strings.Join(f[2:], " ") + } + fmt.Fprintf(&sb, "%d. %s %s%s\n", i+1, typ, short, cmt) + } + sb.WriteString("Удалить: <code>/delkey N</code>") + return sb.String() +} + +// authorized_keys для sshd: каждый ключ -> git-shell-обёртка, ограниченная неймспейсом юзера +func rebuildAuthorizedKeys() { + mu2 := &mu + _ = mu2 + ents, _ := os.ReadDir(filepath.Join(keysDir, "users")) + var sb strings.Builder + for _, e := range ents { + ns := e.Name() + for _, k := range readKeys(ns) { + fmt.Fprintf(&sb, `command="%s %s",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding %s`+"\n", nsCmd, ns, k) + } + } + os.WriteFile(filepath.Join(keysDir, "authorized_keys"), []byte(sb.String()), 0o600) +} + +func helpText() string { + s := "<b>🤖 Пацанский Гит — бот</b>\nРепо под твоим неймспейсом.\n\n" + + "<b>Репо:</b>\n/new имя [описание] · /mine · /list · /info имя · /del имя confirm\n\n" + + "<b>SSH-ключи:</b>\n/addkey ssh-ed25519 AAAA... · /keys · /delkey N\n\n" + + "HTTP: <code>" + publicURL + "/юзер/имя.git</code>" + if sshHost != "" { + s += "\nSSH: <code>" + sshHost + ":юзер/имя.git</code>" + } + return s +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bf0e5fb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +name: pacan-git + +services: + web: # cgit (браузинг) + git-http-backend (clone/push) + build: ./web + volumes: + - repos:/repos + expose: + - "80" + restart: unless-stopped + + bot: # телеграм-бот: /new /list /info /del + build: ./bot + environment: + BOT_TOKEN: ${BOT_TOKEN:?нужен токен от @BotFather} + ADMINS: ${ADMINS:-} + WHITELIST_CHAT: ${WHITELIST_CHAT:-} + PUBLIC_URL: ${PUBLIC_URL:-https://git.ancap.in.ua} + SSH_HOST: ${SSH_HOST:-} + REPOS_DIR: /repos + KEYS_DIR: /keys + GIT_NS: /usr/local/bin/git-ns # форс-команда в authorized_keys (её читает ssh-сервис) + volumes: + - repos:/repos + - keys:/keys + restart: unless-stopped + + tunnel: # cloudflared: git.ancap.in.ua -> http://web:80 (HTTP) + image: cloudflare/cloudflared:latest + command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN:?нужен cloudflared tunnel token} + restart: unless-stopped + + ssh: # git по ssh-ключам: пуш только в свой неймспейс + build: ./ssh + volumes: + - repos:/repos + - keys:/keys + ports: + - "${SSH_PORT:-2222}:22" # SSH через HTTP-туннель не ходит — пробрось порт или свой TCP-туннель + restart: unless-stopped + +volumes: + repos: + keys: diff --git a/ssh/Dockerfile b/ssh/Dockerfile new file mode 100644 index 0000000..13367b6 --- /dev/null +++ b/ssh/Dockerfile @@ -0,0 +1,13 @@ +# sshd для git-доступа по ключам. Ключи и authorized_keys приезжают из volume /keys (пишет бот). +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends openssh-server git \ + && rm -rf /var/lib/apt/lists/* \ + && useradd -m -d /home/git -s /bin/sh git \ + && mkdir -p /home/git/.ssh /run/sshd +COPY git-ns /usr/local/bin/git-ns +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /usr/local/bin/git-ns /entrypoint.sh +EXPOSE 22 +VOLUME /repos +VOLUME /keys +ENTRYPOINT ["/entrypoint.sh"] diff --git a/ssh/entrypoint.sh b/ssh/entrypoint.sh new file mode 100755 index 0000000..c255adf --- /dev/null +++ b/ssh/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e +mkdir -p /repos /keys /home/git/.ssh /run/sshd +touch /keys/authorized_keys +chown -R git:git /home/git/.ssh 2>/dev/null || true +chmod 700 /home/git/.ssh +ssh-keygen -A +cat > /etc/ssh/sshd_config <<EOF +Port 22 +PermitRootLogin no +PasswordAuthentication no +PubkeyAuthentication yes +AllowUsers git +AuthorizedKeysFile /keys/authorized_keys +X11Forwarding no +AllowTcpForwarding no +AllowAgentForwarding no +EOF +echo "sshd up · authorized_keys=/keys/authorized_keys (пишет бот)" +exec /usr/sbin/sshd -D -e diff --git a/ssh/git-ns b/ssh/git-ns new file mode 100755 index 0000000..f8b6b0b --- /dev/null +++ b/ssh/git-ns @@ -0,0 +1,25 @@ +#!/bin/sh +# Форс-команда из authorized_keys: command="git-ns <namespace>". +# Пускает пользователя только в git-операции над репозиториями ЕГО неймспейса. +NS="$1" +REPOS="${REPOS_DIR:-/repos}" +set -- $SSH_ORIGINAL_COMMAND +cmd="$1" +repo=$(printf '%s' "$2" | tr -d "'\"") +repo="${repo#/}" +case "$cmd" in + git-receive-pack|git-upload-pack|git-upload-archive) ;; + *) echo "разрешены только git-команды" >&2; exit 1 ;; +esac +# без слеша -> свой неймспейс +case "$repo" in + */*) ;; + *) repo="$NS/$repo" ;; +esac +# запрет выхода в чужой неймспейс +case "$repo" in + "$NS/"*) ;; + *) echo "чужой неймспейс запрещён" >&2; exit 1 ;; +esac +repo="${repo%/}" +exec git-shell -c "$cmd '$REPOS/$repo'" diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..c99c212 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,16 @@ +# cgit + nginx + git-http-backend (браузинг + clone/push по HTTP) +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + nginx cgit fcgiwrap spawn-fcgi git ca-certificates \ + && rm -rf /var/lib/apt/lists/* +COPY nginx.conf /etc/nginx/nginx.conf +COPY cgitrc /etc/cgitrc +COPY dark.css /usr/share/cgit/dark.css +COPY MonaspaceKrypton.woff2 /usr/share/cgit/MonaspaceKrypton.woff2 +COPY pg-logo.png /usr/share/cgit/pg-logo.png +COPY pg-favicon.png /usr/share/cgit/pg-favicon.png +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +EXPOSE 80 +VOLUME /repos +ENTRYPOINT ["/entrypoint.sh"] diff --git a/web/MonaspaceKrypton.woff2 b/web/MonaspaceKrypton.woff2 Binary files differnew file mode 100644 index 0000000..1ad7f01 --- /dev/null +++ b/web/MonaspaceKrypton.woff2 diff --git a/web/cgitrc b/web/cgitrc new file mode 100644 index 0000000..18764a1 --- /dev/null +++ b/web/cgitrc @@ -0,0 +1,14 @@ +css=/cgit-static/dark.css?v=krypton5 +logo=/cgit-static/pg-logo.png +favicon=/cgit-static/pg-favicon.png +root-title=Пацанский Гит · ancap.in.ua +root-desc=репозитории, которыми рулит телеграм-бот +enable-http-clone=1 +clone-url=https://$HTTP_HOST/$CGIT_REPO_URL +snapshots=tar.gz zip +enable-index-links=1 +enable-commit-graph=1 +enable-index-owner=0 +readme=:README.md +remove-suffix=1 +scan-path=/repos diff --git a/web/dark.css b/web/dark.css new file mode 100644 index 0000000..3781187 --- /dev/null +++ b/web/dark.css @@ -0,0 +1,983 @@ +div#cgit { + padding: 0em; + margin: 0em; + font-family: sans-serif; + font-size: 10pt; + color: #333; + background: white; + padding: 4px; +} + +div#cgit a { + color: blue; + text-decoration: none; +} + +div#cgit a:hover { + text-decoration: underline; +} + +div#cgit table { + border-collapse: collapse; +} + +div#cgit table#header { + width: 100%; + margin-bottom: 1em; +} + +div#cgit table#header td.logo { + width: 96px; + vertical-align: top; +} + +div#cgit table#header td.main { + font-size: 250%; + padding-left: 10px; + white-space: nowrap; +} + +div#cgit table#header td.main a { + color: #000; +} + +div#cgit table#header td.form { + text-align: right; + vertical-align: bottom; + padding-right: 1em; + padding-bottom: 2px; + white-space: nowrap; +} + +div#cgit table#header td.form form, +div#cgit table#header td.form input, +div#cgit table#header td.form select { + font-size: 90%; +} + +div#cgit table#header td.sub { + color: #777; + border-top: solid 1px #ccc; + padding-left: 10px; +} + +div#cgit table.tabs { + border-bottom: solid 3px #ccc; + border-collapse: collapse; + margin-top: 2em; + margin-bottom: 0px; + width: 100%; +} + +div#cgit table.tabs td { + padding: 0px 1em; + vertical-align: bottom; +} + +div#cgit table.tabs td a { + padding: 2px 0.75em; + color: #777; + font-size: 110%; +} + +div#cgit table.tabs td a.active { + color: #000; + background-color: #ccc; +} + +div#cgit table.tabs a[href^="http://"]:after, div#cgit table.tabs a[href^="https://"]:after { + content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfgAhcJDQY+gm2TAAAAHWlUWHRDb21tZW50AAAAAABDcmVhdGVkIHdpdGggR0lNUGQuZQcAAABbSURBVAhbY2BABs4MU4CwhYHBh2Erww4wrGFQZHjI8B8IgUIscJWyDHcggltQhI4zGDCcRwhChPggHIggP1QoAVmQkSETrGoHsiAEsACtBYN0oDAMbgU6EBcAAL2eHUt4XUU4AAAAAElFTkSuQmCC); + opacity: 0.5; + margin: 0 0 0 5px; +} + +div#cgit table.tabs td.form { + text-align: right; +} + +div#cgit table.tabs td.form form { + padding-bottom: 2px; + font-size: 90%; + white-space: nowrap; +} + +div#cgit table.tabs td.form input, +div#cgit table.tabs td.form select { + font-size: 90%; +} + +div#cgit div.path { + margin: 0px; + padding: 5px 2em 2px 2em; + color: #000; + background-color: #eee; +} + +div#cgit div.content { + margin: 0px; + padding: 2em; + border-bottom: solid 3px #ccc; +} + + +div#cgit table.list { + width: 100%; + border: none; + border-collapse: collapse; +} + +div#cgit table.list tr { + background: white; +} + +div#cgit table.list tr.logheader { + background: #eee; +} + +div#cgit table.list tr:nth-child(even) { + background: #f7f7f7; +} + +div#cgit table.list tr:nth-child(odd) { + background: white; +} + +div#cgit table.list tr:hover { + background: #eee; +} + +div#cgit table.list tr.nohover { + background: white; +} + +div#cgit table.list tr.nohover:hover { + background: white; +} + +div#cgit table.list tr.nohover-highlight:hover:nth-child(even) { + background: #f7f7f7; +} + +div#cgit table.list tr.nohover-highlight:hover:nth-child(odd) { + background: white; +} + +div#cgit table.list th { + font-weight: bold; + /* color: #888; + border-top: dashed 1px #888; + border-bottom: dashed 1px #888; + */ + padding: 0.1em 0.5em 0.05em 0.5em; + vertical-align: baseline; +} + +div#cgit table.list td { + border: none; + padding: 0.1em 0.5em 0.1em 0.5em; +} + +div#cgit table.list td.commitgraph { + font-family: monospace; + white-space: pre; +} + +div#cgit table.list td.commitgraph .column1 { + color: #a00; +} + +div#cgit table.list td.commitgraph .column2 { + color: #0a0; +} + +div#cgit table.list td.commitgraph .column3 { + color: #aa0; +} + +div#cgit table.list td.commitgraph .column4 { + color: #00a; +} + +div#cgit table.list td.commitgraph .column5 { + color: #a0a; +} + +div#cgit table.list td.commitgraph .column6 { + color: #0aa; +} + +div#cgit table.list td.logsubject { + font-family: monospace; + font-weight: bold; +} + +div#cgit table.list td.logmsg { + font-family: monospace; + white-space: pre; + padding: 0 0.5em; +} + +div#cgit table.list td a { + color: black; +} + +div#cgit table.list td a.ls-dir { + font-weight: bold; + color: #00f; +} + +div#cgit table.list td a:hover { + color: #00f; +} + +div#cgit img { + border: none; +} + +div#cgit input#switch-btn { + margin: 2px 0px 0px 0px; +} + +div#cgit td#sidebar input.txt { + width: 100%; + margin: 2px 0px 0px 0px; +} + +div#cgit table#grid { + margin: 0px; +} + +div#cgit td#content { + vertical-align: top; + padding: 1em 2em 1em 1em; + border: none; +} + +div#cgit div#summary { + vertical-align: top; + margin-bottom: 1em; +} + +div#cgit table#downloads { + float: right; + border-collapse: collapse; + border: solid 1px #777; + margin-left: 0.5em; + margin-bottom: 0.5em; +} + +div#cgit table#downloads th { + background-color: #ccc; +} + +div#cgit div#blob { + border: solid 1px black; +} + +div#cgit div.error { + color: red; + font-weight: bold; + margin: 1em 2em; +} + +div#cgit a.ls-blob, div#cgit a.ls-dir, div#cgit .ls-mod { + font-family: monospace; +} + +div#cgit td.ls-size { + text-align: right; + font-family: monospace; + width: 10em; +} + +div#cgit td.ls-mode { + font-family: monospace; + width: 10em; +} + +div#cgit table.blob { + margin-top: 0.5em; + border-top: solid 1px black; +} + +div#cgit table.blob td.hashes, +div#cgit table.blob td.lines { + margin: 0; padding: 0 0 0 0.5em; + vertical-align: top; + color: black; +} + +div#cgit table.blob td.linenumbers { + margin: 0; padding: 0 0.5em 0 0.5em; + vertical-align: top; + text-align: right; + border-right: 1px solid gray; +} + +div#cgit table.blob pre { + padding: 0; margin: 0; +} + +div#cgit table.blob td.linenumbers a, +div#cgit table.ssdiff td.lineno a { + color: gray; + text-align: right; + text-decoration: none; +} + +div#cgit table.blob td.linenumbers a:hover, +div#cgit table.ssdiff td.lineno a:hover { + color: black; +} + +div#cgit table.blame td.hashes, +div#cgit table.blame td.lines, +div#cgit table.blame td.linenumbers { + padding: 0; +} + +div#cgit table.blame td.hashes div.alt, +div#cgit table.blame td.lines div.alt { + padding: 0 0.5em 0 0.5em; +} + +div#cgit table.blame td.linenumbers div.alt { + padding: 0 0.5em 0 0; +} + +div#cgit table.blame div.alt:nth-child(even) { + background: #eee; +} + +div#cgit table.blame div.alt:nth-child(odd) { + background: white; +} + +div#cgit table.blame td.lines > div { + position: relative; +} + +div#cgit table.blame td.lines > div > pre { + padding: 0 0 0 0.5em; + position: absolute; + top: 0; +} + +div#cgit table.blame .oid { + font-size: 100%; +} + +div#cgit table.bin-blob { + margin-top: 0.5em; + border: solid 1px black; +} + +div#cgit table.bin-blob th { + font-family: monospace; + white-space: pre; + border: solid 1px #777; + padding: 0.5em 1em; +} + +div#cgit table.bin-blob td { + font-family: monospace; + white-space: pre; + border-left: solid 1px #777; + padding: 0em 1em; +} + +div#cgit table.nowrap td { + white-space: nowrap; +} + +div#cgit table.commit-info { + border-collapse: collapse; + margin-top: 1.5em; +} + +div#cgit div.cgit-panel { + float: right; + margin-top: 1.5em; +} + +div#cgit div.cgit-panel table { + border-collapse: collapse; + border: solid 1px #aaa; + background-color: #eee; +} + +div#cgit div.cgit-panel th { + text-align: center; +} + +div#cgit div.cgit-panel td { + padding: 0.25em 0.5em; +} + +div#cgit div.cgit-panel td.label { + padding-right: 0.5em; +} + +div#cgit div.cgit-panel td.ctrl { + padding-left: 0.5em; +} + +div#cgit table.commit-info th { + text-align: left; + font-weight: normal; + padding: 0.1em 1em 0.1em 0.1em; + vertical-align: top; +} + +div#cgit table.commit-info td { + font-weight: normal; + padding: 0.1em 1em 0.1em 0.1em; +} + +div#cgit div.commit-subject { + font-weight: bold; + font-size: 125%; + margin: 1.5em 0em 0.5em 0em; + padding: 0em; +} + +div#cgit div.commit-msg { + white-space: pre; + font-family: monospace; +} + +div#cgit div.notes-header { + font-weight: bold; + padding-top: 1.5em; +} + +div#cgit div.notes { + white-space: pre; + font-family: monospace; + border: solid 1px #ee9; + background-color: #ffd; + padding: 0.3em 2em 0.3em 1em; + float: left; +} + +div#cgit div.notes-footer { + clear: left; +} + +div#cgit div.diffstat-header { + font-weight: bold; + padding-top: 1.5em; +} + +div#cgit table.diffstat { + border-collapse: collapse; + border: solid 1px #aaa; + background-color: #eee; +} + +div#cgit table.diffstat th { + font-weight: normal; + text-align: left; + text-decoration: underline; + padding: 0.1em 1em 0.1em 0.1em; + font-size: 100%; +} + +div#cgit table.diffstat td { + padding: 0.2em 0.2em 0.1em 0.1em; + font-size: 100%; + border: none; +} + +div#cgit table.diffstat td.mode { + white-space: nowrap; +} + +div#cgit table.diffstat td span.modechange { + padding-left: 1em; + color: red; +} + +div#cgit table.diffstat td.add a { + color: green; +} + +div#cgit table.diffstat td.del a { + color: red; +} + +div#cgit table.diffstat td.upd a { + color: blue; +} + +div#cgit table.diffstat td.graph { + width: 500px; + vertical-align: middle; +} + +div#cgit table.diffstat td.graph table { + border: none; +} + +div#cgit table.diffstat td.graph td { + padding: 0px; + border: 0px; + height: 7pt; +} + +div#cgit table.diffstat td.graph td.add { + background-color: #5c5; +} + +div#cgit table.diffstat td.graph td.rem { + background-color: #c55; +} + +div#cgit div.diffstat-summary { + color: #888; + padding-top: 0.5em; +} + +div#cgit table.diff { + width: 100%; +} + +div#cgit table.diff td { + font-family: monospace; + white-space: pre; +} + +div#cgit table.diff td div.head { + font-weight: bold; + margin-top: 1em; + color: black; +} + +div#cgit table.diff td div.hunk { + color: #009; +} + +div#cgit table.diff td div.add { + color: green; +} + +div#cgit table.diff td div.del { + color: red; +} + +div#cgit .oid { + font-family: monospace; + font-size: 90%; +} + +div#cgit .left { + text-align: left; +} + +div#cgit .right { + text-align: right; +} + +div#cgit table.list td.reposection { + font-style: italic; + color: #888; +} + +div#cgit a.button { + font-size: 80%; + padding: 0em 0.5em; +} + +div#cgit a.primary { + font-size: 100%; +} + +div#cgit a.secondary { + font-size: 90%; +} + +div#cgit td.toplevel-repo { + +} + +div#cgit table.list td.sublevel-repo { + padding-left: 1.5em; +} + +div#cgit ul.pager { + list-style-type: none; + text-align: center; + margin: 1em 0em 0em 0em; + padding: 0; +} + +div#cgit ul.pager li { + display: inline-block; + margin: 0.25em 0.5em; +} + +div#cgit ul.pager a { + color: #777; +} + +div#cgit ul.pager .current { + font-weight: bold; +} + +div#cgit span.age-mins { + font-weight: bold; + color: #080; +} + +div#cgit span.age-hours { + color: #080; +} + +div#cgit span.age-days { + color: #040; +} + +div#cgit span.age-weeks { + color: #444; +} + +div#cgit span.age-months { + color: #888; +} + +div#cgit span.age-years { + color: #bbb; +} + +div#cgit span.insertions { + color: #080; +} + +div#cgit span.deletions { + color: #800; +} + +div#cgit div.footer { + margin-top: 0.5em; + text-align: center; + font-size: 80%; + color: #ccc; +} + +div#cgit div.footer a { + color: #ccc; + text-decoration: none; +} + +div#cgit div.footer a:hover { + text-decoration: underline; +} + +div#cgit a.branch-deco { + color: #000; + margin: 0px 0.5em; + padding: 0px 0.25em; + background-color: #88ff88; + border: solid 1px #007700; +} + +div#cgit a.tag-deco { + color: #000; + margin: 0px 0.5em; + padding: 0px 0.25em; + background-color: #ffff88; + border: solid 1px #777700; +} + +div#cgit a.tag-annotated-deco { + color: #000; + margin: 0px 0.5em; + padding: 0px 0.25em; + background-color: #ffcc88; + border: solid 1px #777700; +} + +div#cgit a.remote-deco { + color: #000; + margin: 0px 0.5em; + padding: 0px 0.25em; + background-color: #ccccff; + border: solid 1px #000077; +} + +div#cgit a.deco { + color: #000; + margin: 0px 0.5em; + padding: 0px 0.25em; + background-color: #ff8888; + border: solid 1px #770000; +} + +div#cgit div.commit-subject a.branch-deco, +div#cgit div.commit-subject a.tag-deco, +div#cgit div.commit-subject a.tag-annotated-deco, +div#cgit div.commit-subject a.remote-deco, +div#cgit div.commit-subject a.deco { + margin-left: 1em; + font-size: 75%; +} + +div#cgit table.stats { + border: solid 1px black; + border-collapse: collapse; +} + +div#cgit table.stats th { + text-align: left; + padding: 1px 0.5em; + background-color: #eee; + border: solid 1px black; +} + +div#cgit table.stats td { + text-align: right; + padding: 1px 0.5em; + border: solid 1px black; +} + +div#cgit table.stats td.total { + font-weight: bold; + text-align: left; +} + +div#cgit table.stats td.sum { + color: #c00; + font-weight: bold; +/* background-color: #eee; */ +} + +div#cgit table.stats td.left { + text-align: left; +} + +div#cgit table.vgraph { + border-collapse: separate; + border: solid 1px black; + height: 200px; +} + +div#cgit table.vgraph th { + background-color: #eee; + font-weight: bold; + border: solid 1px white; + padding: 1px 0.5em; +} + +div#cgit table.vgraph td { + vertical-align: bottom; + padding: 0px 10px; +} + +div#cgit table.vgraph div.bar { + background-color: #eee; +} + +div#cgit table.hgraph { + border: solid 1px black; + width: 800px; +} + +div#cgit table.hgraph th { + background-color: #eee; + font-weight: bold; + border: solid 1px black; + padding: 1px 0.5em; +} + +div#cgit table.hgraph td { + vertical-align: middle; + padding: 2px 2px; +} + +div#cgit table.hgraph div.bar { + background-color: #eee; + height: 1em; +} + +div#cgit table.ssdiff { + width: 100%; +} + +div#cgit table.ssdiff td { + font-size: 75%; + font-family: monospace; + white-space: pre; + padding: 1px 4px 1px 4px; + border-left: solid 1px #aaa; + border-right: solid 1px #aaa; +} + +div#cgit table.ssdiff td.add { + color: black; + background: #cfc; + min-width: 50%; +} + +div#cgit table.ssdiff td.add_dark { + color: black; + background: #aca; + min-width: 50%; +} + +div#cgit table.ssdiff span.add { + background: #cfc; + font-weight: bold; +} + +div#cgit table.ssdiff td.del { + color: black; + background: #fcc; + min-width: 50%; +} + +div#cgit table.ssdiff td.del_dark { + color: black; + background: #caa; + min-width: 50%; +} + +div#cgit table.ssdiff span.del { + background: #fcc; + font-weight: bold; +} + +div#cgit table.ssdiff td.changed { + color: black; + background: #ffc; + min-width: 50%; +} + +div#cgit table.ssdiff td.changed_dark { + color: black; + background: #cca; + min-width: 50%; +} + +div#cgit table.ssdiff td.lineno { + color: black; + background: #eee; + text-align: right; + width: 3em; + min-width: 3em; +} + +div#cgit table.ssdiff td.hunk { + color: black; + background: #ccf; + border-top: solid 1px #aaa; + border-bottom: solid 1px #aaa; +} + +div#cgit table.ssdiff td.head { + border-top: solid 1px #aaa; + border-bottom: solid 1px #aaa; +} + +div#cgit table.ssdiff td.head div.head { + font-weight: bold; + color: black; +} + +div#cgit table.ssdiff td.foot { + border-top: solid 1px #aaa; + border-left: none; + border-right: none; + border-bottom: none; +} + +div#cgit table.ssdiff td.space { + border: none; +} + +div#cgit table.ssdiff td.space div { + min-height: 3em; +} + +/* Style definition file generated by highlight 3.9, http://www.andre-simon.de/ */ +/* Highlighting theme: Kwrite Editor */ +/* adapted for cgit */ +div#cgit table.blob .num { color:#b07e00; } +div#cgit table.blob .esc { color:#ff00ff; } +div#cgit table.blob .str { color:#bf0303; } +div#cgit table.blob .pps { color:#818100; } +div#cgit table.blob .slc { color:#838183; font-style:italic; } +div#cgit table.blob .com { color:#838183; font-style:italic; } +div#cgit table.blob .ppc { color:#008200; } +div#cgit table.blob .opt { color:#000000; } +div#cgit table.blob .lin { color:#555555; } +div#cgit table.blob .kwa { color:#000000; font-weight:bold; } +div#cgit table.blob .kwb { color:#0057ae; } +div#cgit table.blob .kwc { color:#000000; font-weight:bold; } +div#cgit table.blob .kwd { color:#010181; } + +/* ================= тёмный слой + Monaspace Krypton (Пацанский Гит) ================= */ +/* префикс div#cgit — чтобы перебивать дефолтные правила той же специфичности (идём позже в файле) */ +@font-face { font-family:'Monaspace Krypton'; src:url('/cgit-static/MonaspaceKrypton.woff2') format('woff2'); font-weight:100 800; font-display:swap; } + +div#cgit { background:#0f1115; color:#c9d1d9; + font-family:'Monaspace Krypton',ui-monospace,SFMono-Regular,Menlo,monospace; } +div#cgit table, div#cgit td, div#cgit th, div#cgit pre, div#cgit input, div#cgit select, div#cgit textarea, div#cgit a, +div#cgit td.logsubject, div#cgit td.logmsg, div#cgit td.commitgraph, div#cgit a.ls-blob, div#cgit a.ls-dir, +div#cgit td.ls-size, div#cgit td.ls-mode, div#cgit table.blob pre, div#cgit table.bin-blob th, div#cgit table.bin-blob td { + font-family:'Monaspace Krypton',ui-monospace,SFMono-Regular,Menlo,monospace; } + +div#cgit a { color:#58a6ff; } +div#cgit a:hover { color:#79c0ff; } +div#cgit h2 { color:#e6edf3; } + +div#cgit table#header td.main, div#cgit table#header td.main a { color:#e6edf3; } +div#cgit table#header td.sub { color:#8b949e; border-top:1px solid #21262d; } + +div#cgit table.tabs { background:#161b22; border-bottom:2px solid #30363d; } +div#cgit table.tabs td a { color:#c9d1d9; } +div#cgit table.tabs td a.active { color:#fff; background:#21262d; } + +div#cgit div.path { color:#c9d1d9; background:#161b22; } +div#cgit div.content { border-bottom:1px solid #21262d; } +div#cgit td#content { color:#c9d1d9; } + +div#cgit table.list tr, div#cgit table.list tr:nth-child(odd) { background:#0f1115; } +div#cgit table.list tr:nth-child(even) { background:#0d1117; } +div#cgit table.list tr.logheader, div#cgit table.list th { background:#161b22; color:#8b949e; } +div#cgit table.list tr:hover, +div#cgit table.list tr.nohover-highlight:hover:nth-child(even), +div#cgit table.list tr.nohover-highlight:hover:nth-child(odd) { background:#161b22; } +div#cgit table.list tr.nohover, div#cgit table.list tr.nohover:hover { background:#0f1115; } +div#cgit table.list td a { color:#c9d1d9; } +div#cgit table.list td a.ls-dir { color:#58a6ff; } +div#cgit table.list td a:hover { color:#79c0ff; } + +div#cgit .sha1, div#cgit .oid, div#cgit td.sha1 { color:#8b949e; } +div#cgit div.commit-subject, div#cgit div.commit-msg { color:#e6edf3; } +div#cgit table.commit-info, div#cgit div.commit-info { color:#8b949e; } +div#cgit table.commit-info th, div#cgit table.commit-info td { color:#c9d1d9; } + +div#cgit div#blob { border-color:#30363d; } +div#cgit table.blob td.lines pre, div#cgit table.blob td.hashes { color:#c9d1d9; } +div#cgit table.blob td.linenumbers a { color:#484f58; } +div#cgit table.blob td.linenumbers a:hover { color:#c9d1d9; } +div#cgit table.blob td.linenumbers { border-right:1px solid #30363d; } + +div#cgit table.diff td div.head { color:#e6edf3; } +div#cgit table.diff td div.hunk { color:#58a6ff; background:#161b22; } +div#cgit table.diff td div.add { color:#3fb950; background:#0d2818; } +div#cgit table.diff td div.del { color:#f85149; background:#2a0d0d; } + +div#cgit input, div#cgit select, div#cgit textarea { background:#0d1117; color:#c9d1d9; border:1px solid #30363d; } +div#cgit div.footer { color:#6e7681; border-top:1px solid #21262d; } +div#cgit td, div#cgit th { border-color:#21262d; } + +/* ref-бейджи (HEAD/ветки/теги) под тёмную тему */ +div#cgit a.branch-deco { color:#3fb950; background:#0d2818; border:1px solid #238636; padding:0 5px; border-radius:5px; } +div#cgit a.tag-deco { color:#d29922; background:#2b2000; border:1px solid #9e6a03; padding:0 5px; border-radius:5px; } +div#cgit a.remote-deco { color:#a371f7; background:#1d1533; border:1px solid #6e40c9; padding:0 5px; border-radius:5px; } +div#cgit a.deco { color:#c9d1d9; background:#161b22; border:1px solid #30363d; padding:0 5px; border-radius:5px; } + +/* фикс «белых краёв»: cgit красит только div#cgit, а body/html остаются дефолтно-белыми */ +html, body { background:#0f1115 !important; margin:0; padding:0; } +div#cgit { min-height:100vh; box-sizing:border-box; } diff --git a/web/entrypoint.sh b/web/entrypoint.sh new file mode 100755 index 0000000..f92af30 --- /dev/null +++ b/web/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -e +mkdir -p /repos +# fcgiwrap на unix-сокете (его дёргает nginx для cgit.cgi и git-http-backend) +rm -f /run/fcgiwrap.sock +spawn-fcgi -s /run/fcgiwrap.sock -M 0666 -F 4 -- /usr/sbin/fcgiwrap +exec nginx -g 'daemon off;' diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 0000000..4477a8d --- /dev/null +++ b/web/nginx.conf @@ -0,0 +1,39 @@ +user root; +worker_processes 1; +events { worker_connections 256; } +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + server { + listen 80 default_server; + server_name _; + + # статика cgit (css/иконки) + location /cgit-static/ { + alias /usr/share/cgit/; + access_log off; + } + + # git smart-HTTP: clone/push по /<repo>.git/... + location ~ ^/(?<gitrepo>[a-zA-Z0-9._/-]+\.git)(?<gitrest>/.*)?$ { + client_max_body_size 0; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; + fastcgi_param GIT_HTTP_EXPORT_ALL ""; + fastcgi_param GIT_PROJECT_ROOT /repos; + fastcgi_param PATH_INFO /$gitrepo$gitrest; + fastcgi_pass unix:/run/fcgiwrap.sock; + } + + # cgit — браузинг всего остального + location / { + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME /usr/lib/cgit/cgit.cgi; + fastcgi_param PATH_INFO $uri; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param HTTP_HOST $http_host; + fastcgi_pass unix:/run/fcgiwrap.sock; + } + } +} diff --git a/web/pg-favicon.png b/web/pg-favicon.png Binary files differnew file mode 100644 index 0000000..b90ce12 --- /dev/null +++ b/web/pg-favicon.png diff --git a/web/pg-logo.png b/web/pg-logo.png Binary files differnew file mode 100644 index 0000000..03be6ce --- /dev/null +++ b/web/pg-logo.png |
