Showing preview only (844K chars total). Download the full file or copy to clipboard to get everything.
Repository: charmbracelet/soft-serve
Branch: main
Commit: 80490de86ee9
Files: 298
Total size: 777.0 KB
Directory structure:
gitextract_twsjoh99/
├── .editorconfig
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── build.yml
│ ├── coverage.yml
│ ├── dependabot-sync.yml
│ ├── goreleaser.yml
│ ├── lint-sync.yml
│ ├── lint.yml
│ └── nightly.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── .nfpm/
│ ├── postinstall.sh
│ ├── postremove.sh
│ ├── soft-serve.conf
│ ├── soft-serve.service
│ ├── sysusers.conf
│ └── tmpfiles.conf
├── Dockerfile
├── LICENSE
├── README.md
├── browse.tape
├── cmd/
│ ├── cmd.go
│ └── soft/
│ ├── admin/
│ │ └── admin.go
│ ├── browse/
│ │ └── browse.go
│ ├── hook/
│ │ └── hook.go
│ ├── main.go
│ └── serve/
│ ├── certreloader.go
│ ├── certreloader_test.go
│ ├── serve.go
│ └── server.go
├── codecov.yml
├── demo.tape
├── docker.md
├── git/
│ ├── attr.go
│ ├── attr_test.go
│ ├── command.go
│ ├── commit.go
│ ├── config.go
│ ├── errors.go
│ ├── patch.go
│ ├── reference.go
│ ├── repo.go
│ ├── server.go
│ ├── stash.go
│ ├── tag.go
│ ├── tree.go
│ ├── types.go
│ └── utils.go
├── go.mod
├── go.sum
├── pkg/
│ ├── access/
│ │ ├── access.go
│ │ ├── access_test.go
│ │ ├── context.go
│ │ └── context_test.go
│ ├── backend/
│ │ ├── access_token.go
│ │ ├── auth.go
│ │ ├── auth_test.go
│ │ ├── backend.go
│ │ ├── cache.go
│ │ ├── collab.go
│ │ ├── context.go
│ │ ├── hooks.go
│ │ ├── lfs.go
│ │ ├── repo.go
│ │ ├── settings.go
│ │ ├── user.go
│ │ ├── utils.go
│ │ └── webhooks.go
│ ├── config/
│ │ ├── config.go
│ │ ├── config_test.go
│ │ ├── context.go
│ │ ├── context_test.go
│ │ ├── file.go
│ │ ├── file_test.go
│ │ ├── ssh.go
│ │ ├── ssh_test.go
│ │ └── testdata/
│ │ ├── config.yaml
│ │ └── k1.pub
│ ├── cron/
│ │ ├── cron.go
│ │ └── cron_test.go
│ ├── daemon/
│ │ ├── conn.go
│ │ ├── daemon.go
│ │ └── daemon_test.go
│ ├── db/
│ │ ├── context.go
│ │ ├── context_test.go
│ │ ├── db.go
│ │ ├── db_test.go
│ │ ├── errors.go
│ │ ├── errors_test.go
│ │ ├── handler.go
│ │ ├── internal/
│ │ │ └── test/
│ │ │ └── test.go
│ │ ├── logger.go
│ │ ├── migrate/
│ │ │ ├── 0001_create_tables.go
│ │ │ ├── 0001_create_tables_postgres.down.sql
│ │ │ ├── 0001_create_tables_postgres.up.sql
│ │ │ ├── 0001_create_tables_sqlite.down.sql
│ │ │ ├── 0001_create_tables_sqlite.up.sql
│ │ │ ├── 0002_webhooks.go
│ │ │ ├── 0002_webhooks_postgres.down.sql
│ │ │ ├── 0002_webhooks_postgres.up.sql
│ │ │ ├── 0002_webhooks_sqlite.down.sql
│ │ │ ├── 0002_webhooks_sqlite.up.sql
│ │ │ ├── 0003_migrate_lfs_objects.go
│ │ │ ├── migrate.go
│ │ │ ├── migrate_test.go
│ │ │ └── migrations.go
│ │ └── models/
│ │ ├── access_token.go
│ │ ├── collab.go
│ │ ├── lfs.go
│ │ ├── public_key.go
│ │ ├── repo.go
│ │ ├── settings.go
│ │ ├── user.go
│ │ └── webhook.go
│ ├── git/
│ │ ├── errors.go
│ │ ├── git.go
│ │ ├── git_test.go
│ │ ├── lfs.go
│ │ ├── lfs_auth.go
│ │ ├── lfs_log.go
│ │ └── service.go
│ ├── hooks/
│ │ ├── gen.go
│ │ ├── gen_test.go
│ │ └── hooks.go
│ ├── jobs/
│ │ ├── jobs.go
│ │ └── mirror.go
│ ├── jwk/
│ │ ├── jwk.go
│ │ └── jwk_test.go
│ ├── lfs/
│ │ ├── basic_transfer.go
│ │ ├── client.go
│ │ ├── common.go
│ │ ├── endpoint.go
│ │ ├── http_client.go
│ │ ├── pointer.go
│ │ ├── pointer_test.go
│ │ ├── scanner.go
│ │ ├── ssh_client.go
│ │ └── transfer.go
│ ├── log/
│ │ ├── log.go
│ │ └── log_test.go
│ ├── proto/
│ │ ├── access_token.go
│ │ ├── context.go
│ │ ├── errors.go
│ │ ├── repo.go
│ │ └── user.go
│ ├── ssh/
│ │ ├── cmd/
│ │ │ ├── blob.go
│ │ │ ├── branch.go
│ │ │ ├── cmd.go
│ │ │ ├── collab.go
│ │ │ ├── commit.go
│ │ │ ├── create.go
│ │ │ ├── delete.go
│ │ │ ├── description.go
│ │ │ ├── git.go
│ │ │ ├── hidden.go
│ │ │ ├── import.go
│ │ │ ├── info.go
│ │ │ ├── jwt.go
│ │ │ ├── list.go
│ │ │ ├── mirror.go
│ │ │ ├── private.go
│ │ │ ├── project_name.go
│ │ │ ├── pubkey.go
│ │ │ ├── rename.go
│ │ │ ├── repo.go
│ │ │ ├── set_username.go
│ │ │ ├── settings.go
│ │ │ ├── tag.go
│ │ │ ├── token.go
│ │ │ ├── tree.go
│ │ │ ├── user.go
│ │ │ └── webhooks.go
│ │ ├── middleware.go
│ │ ├── middleware_test.go
│ │ ├── session.go
│ │ ├── session_test.go
│ │ ├── ssh.go
│ │ └── ui.go
│ ├── sshutils/
│ │ ├── utils.go
│ │ └── utils_test.go
│ ├── ssrf/
│ │ ├── ssrf.go
│ │ └── ssrf_test.go
│ ├── stats/
│ │ └── stats.go
│ ├── storage/
│ │ ├── local.go
│ │ └── storage.go
│ ├── store/
│ │ ├── access_token.go
│ │ ├── collab.go
│ │ ├── context.go
│ │ ├── database/
│ │ │ ├── access_token.go
│ │ │ ├── collab.go
│ │ │ ├── database.go
│ │ │ ├── lfs.go
│ │ │ ├── repo.go
│ │ │ ├── settings.go
│ │ │ ├── user.go
│ │ │ └── webhooks.go
│ │ ├── lfs.go
│ │ ├── repo.go
│ │ ├── settings.go
│ │ ├── store.go
│ │ ├── user.go
│ │ └── webhooks.go
│ ├── sync/
│ │ ├── workqueue.go
│ │ └── workqueue_test.go
│ ├── task/
│ │ └── manager.go
│ ├── test/
│ │ └── test.go
│ ├── ui/
│ │ ├── common/
│ │ │ ├── common.go
│ │ │ ├── common_test.go
│ │ │ ├── component.go
│ │ │ ├── error.go
│ │ │ ├── format.go
│ │ │ ├── style.go
│ │ │ └── utils.go
│ │ ├── components/
│ │ │ ├── code/
│ │ │ │ └── code.go
│ │ │ ├── footer/
│ │ │ │ └── footer.go
│ │ │ ├── header/
│ │ │ │ └── header.go
│ │ │ ├── selector/
│ │ │ │ └── selector.go
│ │ │ ├── statusbar/
│ │ │ │ └── statusbar.go
│ │ │ ├── tabs/
│ │ │ │ └── tabs.go
│ │ │ └── viewport/
│ │ │ └── viewport.go
│ │ ├── keymap/
│ │ │ └── keymap.go
│ │ ├── pages/
│ │ │ ├── repo/
│ │ │ │ ├── empty.go
│ │ │ │ ├── files.go
│ │ │ │ ├── filesitem.go
│ │ │ │ ├── log.go
│ │ │ │ ├── logitem.go
│ │ │ │ ├── readme.go
│ │ │ │ ├── refs.go
│ │ │ │ ├── refsitem.go
│ │ │ │ ├── repo.go
│ │ │ │ ├── stash.go
│ │ │ │ └── stashitem.go
│ │ │ └── selection/
│ │ │ ├── item.go
│ │ │ └── selection.go
│ │ └── styles/
│ │ └── styles.go
│ ├── utils/
│ │ ├── utils.go
│ │ └── utils_test.go
│ ├── version/
│ │ └── version.go
│ ├── web/
│ │ ├── auth.go
│ │ ├── context.go
│ │ ├── git.go
│ │ ├── git_lfs.go
│ │ ├── goget.go
│ │ ├── health.go
│ │ ├── http.go
│ │ ├── logging.go
│ │ ├── server.go
│ │ └── util.go
│ └── webhook/
│ ├── branch_tag.go
│ ├── collaborator.go
│ ├── common.go
│ ├── content_type.go
│ ├── content_type_test.go
│ ├── event.go
│ ├── push.go
│ ├── repository.go
│ ├── ssrf_test.go
│ ├── validator.go
│ ├── validator_test.go
│ └── webhook.go
├── systemd.md
└── testscript/
├── script_test.go
└── testdata/
├── anon-access.txtar
├── auth-bypass-regression.txtar
├── config-servers-git_disabled.txtar
├── config-servers-http_disabled.txtar
├── config-servers-ssh_disabled.txtar
├── config-servers-stats_disabled.txtar
├── help.txtar
├── http-cors.txtar
├── http.txtar
├── jwt.txtar
├── mirror.txtar
├── repo-blob.txtar
├── repo-collab.txtar
├── repo-commit.txtar
├── repo-create.txtar
├── repo-delete.txtar
├── repo-import-local-path.txtar
├── repo-import.txtar
├── repo-perms.txtar
├── repo-push.txtar
├── repo-tree.txtar
├── repo-webhook-ssrf.txtar
├── repo-webhooks.txtar
├── set-username.txtar
├── settings.txtar
├── soft-browse.txtar
├── soft-manpages.txtar
├── ssh-lfs.txtar
├── ssh.txtar
├── token.txtar
├── ui-home.txtar
└── user_management.txtar
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=true
trim_trailing_whitespace=true
indent_size=2
indent_style=space
[*.go]
indent_size=4
indent_style=tab
================================================
FILE: .github/CODEOWNERS
================================================
* @aymanbagabas
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS: [e.g. Linux]
- Terminal [e.g. kitty, iterm2, gnome-terminal]
- Version [e.g. v0.4.0]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
ignore:
- dependency-name: github.com/charmbracelet/bubbletea/v2
versions:
- v2.0.0-beta1
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
================================================
FILE: .github/workflows/build.yml
================================================
name: build
on:
push:
branches:
- "main"
pull_request:
jobs:
build:
uses: charmbracelet/meta/.github/workflows/build.yml@main
snapshot:
uses: charmbracelet/meta/.github/workflows/snapshot.yml@main
secrets:
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
test_postgres:
services:
postgres:
image: postgres
ports:
- 5432:5432
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: ^1
cache: true
- name: Download Go modules
run: go mod download
- name: Test
run: go test ./...
env:
SOFT_SERVE_DB_DRIVER: postgres
SOFT_SERVE_DB_DATA_SOURCE: postgres://postgres:postgres@localhost/postgres?sslmode=disable
================================================
FILE: .github/workflows/coverage.yml
================================================
name: coverage
on:
push:
branches:
- "main"
pull_request:
jobs:
coverage:
strategy:
matrix:
os: [ubuntu-latest] # TODO: add macos & windows
services:
postgres:
image: postgres
ports:
- 5432:5432
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: ^1
- name: Test
run: |
# We collect coverage data from two sources,
# 1) unit tests 2) integration tests
#
# https://go.dev/testing/coverage/
# https://dustinspecker.com/posts/go-combined-unit-integration-code-coverage/
# https://github.com/golang/go/issues/51430#issuecomment-1344711300
mkdir -p coverage/unit
mkdir -p coverage/int
mkdir -p coverage/int2
# Collect unit tests coverage
go test -failfast -race -timeout 5m -skip=^TestScript -cover ./... -args -test.gocoverdir=$PWD/coverage/unit
# Collect integration tests coverage
GOCOVERDIR=$PWD/coverage/int go test -failfast -race -timeout 5m -run=^TestScript ./...
SOFT_SERVE_DB_DRIVER=postgres \
SOFT_SERVE_DB_DATA_SOURCE=postgres://postgres:postgres@localhost/postgres?sslmode=disable \
GOCOVERDIR=$PWD/coverage/int2 go test -failfast -race -timeout 5m -run=^TestScript ./...
# Convert coverage data to legacy textfmt format to upload
go tool covdata textfmt -i=coverage/unit,coverage/int,coverage/int2 -o=coverage.txt
- uses: codecov/codecov-action@v5
with:
file: ./coverage.txt
================================================
FILE: .github/workflows/dependabot-sync.yml
================================================
name: dependabot-sync
on:
schedule:
- cron: "0 0 * * 0" # every Sunday at midnight
workflow_dispatch: # allows manual triggering
permissions:
contents: write
pull-requests: write
jobs:
dependabot-sync:
uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main
with:
repo_name: ${{ github.event.repository.name }}
secrets:
gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
================================================
FILE: .github/workflows/goreleaser.yml
================================================
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: goreleaser
on:
push:
tags:
- v*.*.*
concurrency:
group: goreleaser
cancel-in-progress: true
jobs:
goreleaser:
uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
fury_token: ${{ secrets.FURY_TOKEN }}
nfpm_gpg_key: ${{ secrets.NFPM_GPG_KEY }}
nfpm_passphrase: ${{ secrets.NFPM_PASSPHRASE }}
macos_sign_p12: ${{ secrets.MACOS_SIGN_P12 }}
macos_sign_password: ${{ secrets.MACOS_SIGN_PASSWORD }}
macos_notary_issuer_id: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
macos_notary_key_id: ${{ secrets.MACOS_NOTARY_KEY_ID }}
macos_notary_key: ${{ secrets.MACOS_NOTARY_KEY }}
================================================
FILE: .github/workflows/lint-sync.yml
================================================
name: lint-sync
on:
# schedule:
# # every Sunday at midnight
# - cron: "0 0 * * 0"
workflow_dispatch: # allows manual triggering
permissions:
contents: write
pull-requests: write
jobs:
lint:
uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main
================================================
FILE: .github/workflows/lint.yml
================================================
name: lint
on:
push:
pull_request:
jobs:
lint:
uses: charmbracelet/meta/.github/workflows/lint.yml@main
with:
golangci_path: .golangci.yml
golangci_version: latest
timeout: 10m
================================================
FILE: .github/workflows/nightly.yml
================================================
name: nightly
on:
push:
branches:
- main
jobs:
nightly:
uses: charmbracelet/meta/.github/workflows/nightly.yml@main
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
macos_sign_p12: ${{ secrets.MACOS_SIGN_P12 }}
macos_sign_password: ${{ secrets.MACOS_SIGN_PASSWORD }}
macos_notary_issuer_id: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
macos_notary_key_id: ${{ secrets.MACOS_NOTARY_KEY_ID }}
macos_notary_key: ${{ secrets.MACOS_NOTARY_KEY }}
================================================
FILE: .gitignore
================================================
cmd/soft/soft
./soft
.ssh
.repos
dist
data/
completions/
manpages/
soft_serve_ed25519*
================================================
FILE: .golangci.yml
================================================
version: "2"
linters:
enable:
- bodyclose
# - exhaustive
# - goconst
# - godot
# - godox
# - gomoddirectives
- goprintffuncname
# - gosec
- misspell
# - nakedret
# - nestif
# - nilerr
- noctx
- nolintlint
# - prealloc
# - revive
- rowserrcheck
- sqlclosecheck
- tparallel
# - unconvert
# - unparam
- whitespace
# - wrapcheck
disable:
- errcheck
- ineffassign
- unused
- staticcheck
exclusions:
generated: lax
presets:
- common-false-positives
rules:
- text: '(slog|log)\.\w+'
linters:
- noctx
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
================================================
FILE: .goreleaser.yml
================================================
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-soft-serve.yaml
variables:
main: "./cmd/soft"
binary_name: soft
description: "A tasty, self-hostable Git server for the command line🍦"
github_url: "https://github.com/charmbracelet/soft-serve"
maintainer: "Ayman Bagabas <ayman@charm.sh>"
brew_commit_author_name: "Ayman Bagabas"
brew_commit_author_email: "ayman@charm.sh"
================================================
FILE: .nfpm/postinstall.sh
================================================
#!/bin/sh
set -e
if ! command -V systemctl >/dev/null 2>&1; then
echo "Not running SystemD, ignoring"
exit 0
fi
systemd-sysusers
systemd-tmpfiles --create
systemctl daemon-reload
systemctl unmask soft-serve.service
systemctl preset soft-serve.service
================================================
FILE: .nfpm/postremove.sh
================================================
#!/bin/sh
set -e
if ! command -V systemctl >/dev/null 2>&1; then
echo "Not running SystemD, ignoring"
exit 0
fi
systemctl daemon-reload
systemctl reset-failed
echo "WARN: the soft-serve user/group and /var/lib/soft-serve directory were not removed"
================================================
FILE: .nfpm/soft-serve.conf
================================================
# Config defined here will override the config in /var/lib/soft-serve/config.yaml
# Keys defined in `SOFT_SERVE_INITIAL_ADMIN_KEYS` will be merged with
# the `initial_admin_keys` from /var/lib/soft-serve/config.yaml.
#
#SOFT_SERVE_GIT_LISTEN_ADDR=:9418
#SOFT_SERVE_HTTP_LISTEN_ADDR=:23232
#SOFT_SERVE_SSH_LISTEN_ADDR=:23231
#SOFT_SERVE_SSH_KEY_PATH=ssh/soft_serve_host_ed25519
#SOFT_SERVE_INITIAL_ADMIN_KEYS='ssh-ed25519 AAAAC3NzaC1lZDI1...'
================================================
FILE: .nfpm/soft-serve.service
================================================
[Unit]
Description=Soft Serve git server 🍦
Documentation=https://github.com/charmbracelet/soft-serve
Requires=network-online.target
After=network-online.target
[Service]
Type=simple
User=soft-serve
Group=soft-serve
Restart=always
RestartSec=1
ExecStart=/usr/bin/soft serve
Environment=SOFT_SERVE_DATA_PATH=/var/lib/soft-serve
EnvironmentFile=-/etc/soft-serve.conf
WorkingDirectory=/var/lib/soft-serve
# Hardening
ReadWritePaths=/var/lib/soft-serve
UMask=0027
NoNewPrivileges=true
LimitNOFILE=1048576
ProtectSystem=strict
ProtectHome=true
PrivateUsers=yes
PrivateTmp=true
PrivateDevices=true
ProtectHostname=true
ProtectClock=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
RemoveIPC=true
CapabilityBoundingSet=
AmbientCapabilities=
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
SystemCallArchitectures=native
[Install]
WantedBy=multi-user.target
================================================
FILE: .nfpm/sysusers.conf
================================================
u soft-serve - "Soft Serve daemon user" /var/lib/soft-serve
================================================
FILE: .nfpm/tmpfiles.conf
================================================
d /var/lib/soft-serve 0750 soft-serve soft-serve
================================================
FILE: Dockerfile
================================================
FROM alpine:latest
# Create directories
WORKDIR /soft-serve
# Expose data volume
VOLUME /soft-serve
# Environment variables
ENV SOFT_SERVE_DATA_PATH "/soft-serve"
ENV SOFT_SERVE_INITIAL_ADMIN_KEYS ""
# workaround to prevent slowness in docker when running with a tty
ENV CI "1"
# Expose ports
# SSH
EXPOSE 23231/tcp
# HTTP
EXPOSE 23232/tcp
# Stats
EXPOSE 23233/tcp
# Git
EXPOSE 9418/tcp
# Set the default command
ENTRYPOINT [ "/usr/local/bin/soft", "serve" ]
RUN apk update && apk add --update git bash openssh && rm -rf /var/cache/apk/*
COPY soft /usr/local/bin/soft
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021-2023 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Soft Serve
<p>
<img style="width: 451px" src="https://stuff.charm.sh/soft-serve/soft-serve-header.png?0" alt="A nice rendering of some melting ice cream with the words ‘Charm Soft Serve’ next to it"><br>
<a href="https://github.com/charmbracelet/soft-serve/releases"><img src="https://img.shields.io/github/release/charmbracelet/soft-serve.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/github.com/charmbracelet/soft-serve?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
<a href="https://github.com/charmbracelet/soft-serve/actions"><img src="https://github.com/charmbracelet/soft-serve/workflows/build/badge.svg" alt="Build Status"></a>
<a href="https://nightly.link/charmbracelet/soft-serve/workflows/nightly/main"><img src="https://shields.io/badge/-Nightly%20Builds-orange?logo=hackthebox&logoColor=fff&style=appveyor"/></a>
</p>
A tasty, self-hostable Git server for the command line. 🍦
<picture>
<source media="(max-width: 750px)" srcset="https://github.com/charmbracelet/soft-serve/assets/42545625/c754c746-dc4c-44a6-9c39-28649264cbf2">
<source media="(min-width: 750px)" width="750" srcset="https://github.com/charmbracelet/soft-serve/assets/42545625/c754c746-dc4c-44a6-9c39-28649264cbf2">
<img src="https://github.com/charmbracelet/soft-serve/assets/42545625/c754c746-dc4c-44a6-9c39-28649264cbf2" alt="Soft Serve screencast">
</picture>
- Easy to navigate TUI available over SSH
- Clone repos over SSH, HTTP, or Git protocol
- Git LFS support with both HTTP and SSH backends
- Manage repos with SSH
- Create repos on demand with SSH or `git push`
- Browse repos, files and commits with SSH-accessible UI
- Print files over SSH with or without syntax highlighting and line numbers
- Easy access control
- SSH authentication using public keys
- Allow/disallow anonymous access
- Add collaborators with SSH public keys
- Repos can be public or private
- User access tokens
## Where can I see it?
Just run `ssh git.charm.sh` for an example. You can also try some of the following commands:
```bash
# Jump directly to a repo in the TUI
ssh git.charm.sh -t soft-serve
# Print out a directory tree for a repo
ssh git.charm.sh repo tree soft-serve
# Print a specific file
ssh git.charm.sh repo blob soft-serve cmd/soft/main.go
# Print a file with syntax highlighting and line numbers
ssh git.charm.sh repo blob soft-serve cmd/soft/main.go -c -l
```
Or you can use Soft Serve to browse local repositories using `soft browse
[directory]` or running `soft` within a Git repository.
## Installation
Soft Serve is a single binary called `soft`. You can get it from a package
manager:
```bash
# macOS or Linux
brew install charmbracelet/tap/soft-serve
# Windows (with Winget)
winget install charmbracelet.soft-serve
# Arch Linux
pacman -S soft-serve
# Nix
nix-env -iA nixpkgs.soft-serve
# Debian/Ubuntu
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install soft-serve
# Fedora/RHEL
echo '[charm]
name=Charm
baseurl=https://repo.charm.sh/yum/
enabled=1
gpgcheck=1
gpgkey=https://repo.charm.sh/yum/gpg.key' | sudo tee /etc/yum.repos.d/charm.repo
sudo yum install soft-serve
```
You can also download a binary from the [releases][releases] page. Packages are
available in Alpine, Debian, and RPM formats. Binaries are available for Linux,
macOS, and Windows.
[releases]: https://github.com/charmbracelet/soft-serve/releases
Or just install it with `go`:
```bash
go install github.com/charmbracelet/soft-serve/cmd/soft@latest
```
A [Docker image][docker] is also available.
[docker]: https://github.com/charmbracelet/soft-serve/blob/main/docker.md
## Setting up a server
Make sure `git` is installed, then run `soft serve`. That’s it.
This will create a `data` directory that will store all the repos, ssh keys,
and database.
By default, program configuration is stored within the `data` directory. But,
this can be overridden by setting a custom path to a config file with `SOFT_SERVE_CONFIG_LOCATION`
that is pre-created. If a config file pointed to by `SOFT_SERVE_CONFIG_LOCATION`,
the default location within the `data` dir is used for generating a default config.
To change the default data path use `SOFT_SERVE_DATA_PATH` environment variable.
```sh
SOFT_SERVE_DATA_PATH=/var/lib/soft-serve soft serve
```
When you run Soft Serve for the first time, make sure you have the
`SOFT_SERVE_INITIAL_ADMIN_KEYS` environment variable is set to your ssh
authorized key. Any added key to this variable will be treated as admin with
full privileges.
Using this environment variable, Soft Serve will create a new `admin` user that
has full privileges. You can rename and change the user settings later.
Check out [Systemd][systemd] on how to run Soft Serve as a service using
Systemd. Soft Serve packages in our Apt/Yum repositories come with Systemd
service units.
[systemd]: https://github.com/charmbracelet/soft-serve/blob/main/systemd.md
### Server Configuration
Once you start the server for the first time, the settings will be in
`config.yaml` under your data directory. The default `config.yaml` is
self-explanatory and will look like this:
```yaml
# Soft Serve Server configurations
# The name of the server.
# This is the name that will be displayed in the UI.
name: "Soft Serve"
# Log format to use. Valid values are "json", "logfmt", and "text".
log_format: "text"
# The SSH server configuration.
ssh:
# The address on which the SSH server will listen.
listen_addr: ":23231"
# The public URL of the SSH server.
# This is the address that will be used to clone repositories.
public_url: "ssh://localhost:23231"
# The path to the SSH server's private key.
key_path: "ssh/soft_serve_host"
# The path to the SSH server's client private key.
# This key will be used to authenticate the server to make git requests to
# ssh remotes.
client_key_path: "ssh/soft_serve_client"
# The maximum number of seconds a connection can take.
# A value of 0 means no timeout.
max_timeout: 0
# The number of seconds a connection can be idle before it is closed.
idle_timeout: 120
# The Git daemon configuration.
git:
# The address on which the Git daemon will listen.
listen_addr: ":9418"
# The maximum number of seconds a connection can take.
# A value of 0 means no timeout.
max_timeout: 0
# The number of seconds a connection can be idle before it is closed.
idle_timeout: 3
# The maximum number of concurrent connections.
max_connections: 32
# The HTTP server configuration.
http:
# The address on which the HTTP server will listen.
listen_addr: ":23232"
# The path to the TLS private key.
tls_key_path: ""
# The path to the TLS certificate.
tls_cert_path: ""
# The public URL of the HTTP server.
# This is the address that will be used to clone repositories.
# Make sure to use https:// if you are using TLS.
public_url: "http://localhost:23232"
# The cross-origin request security options
cors:
# The allowed cross-origin headers
allowed_headers:
- "Accept"
- "Accept-Language"
- "Content-Language"
- "Content-Type"
- "Origin"
- "X-Requested-With"
- "User-Agent"
- "Authorization"
- "Access-Control-Request-Method"
- "Access-Control-Allow-Origin"
# The allowed cross-origin URLs
allowed_origins:
- "http://localhost:23232" # always allowed
# - "https://example.com"
# The allowed cross-origin methods
allowed_methods:
- "GET"
- "HEAD"
- "POST"
- "PUT"
- "OPTIONS"
# The database configuration.
db:
# The database driver to use.
# Valid values are "sqlite" and "postgres".
driver: "sqlite"
# The database data source name.
# This is driver specific and can be a file path or connection string.
# Make sure foreign key support is enabled when using SQLite.
data_source: "soft-serve.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)"
# Git LFS configuration.
lfs:
# Enable Git LFS.
enabled: true
# Enable Git SSH transfer.
ssh_enabled: false
# Cron job configuration
jobs:
mirror_pull: "@every 10m"
# The stats server configuration.
stats:
# The address on which the stats server will listen.
listen_addr: ":23233"
# Additional admin keys.
#initial_admin_keys:
# - "ssh-rsa AAAAB3NzaC1yc2..."
```
You can also use environment variables, to override these settings. All server
settings environment variables start with `SOFT_SERVE_` followed by the setting
name all in uppercase. Here are some examples:
- `SOFT_SERVE_NAME`: The name of the server that will appear in the TUI
- `SOFT_SERVE_SSH_LISTEN_ADDR`: SSH listen address
- `SOFT_SERVE_SSH_KEY_PATH`: SSH host key-pair path
- `SOFT_SERVE_HTTP_LISTEN_ADDR`: HTTP listen address
- `SOFT_SERVE_HTTP_PUBLIC_URL`: HTTP public URL used for cloning
- `SOFT_SERVE_GIT_MAX_CONNECTIONS`: The number of simultaneous connections to git daemon
#### Database Configuration
Soft Serve supports both SQLite and Postgres for its database. Like all other Soft Serve settings, you can change the database _driver_ and _data source_ using either `config.yaml` or environment variables. The default config uses SQLite as the default database driver.
To use Postgres as your database, first create a Soft Serve database:
```sh
psql -h<hostname> -p<port> -U<user> -c 'CREATE DATABASE soft_serve'
```
Then set the database _data source_ to point to your Postgres database. For instance, if you're running Postgres locally, using the default user `postgres` and using a database name `soft_serve`, you would have this config in your config file or environment variable:
```
db:
driver: "postgres"
data_source: "postgres://postgres@localhost:5432/soft_serve?sslmode=disable"
```
Environment variables equivalent:
```sh
SOFT_SERVE_DB_DRIVER=postgres \
SOFT_SERVE_DB_DATA_SOURCE="postgres://postgres@localhost:5432/soft_serve?sslmode=disable" \
soft serve
```
You can specify a database connection password in the _data source_ url. For example, `postgres://myuser:dbpass@localhost:5432/my_soft_serve_db`.
#### LFS Configuration
Soft Serve supports both Git LFS [HTTP](https://github.com/git-lfs/git-lfs/blob/main/docs/api/README.md) and [SSH](https://github.com/git-lfs/git-lfs/blob/main/docs/proposals/ssh_adapter.md) protocols out of the box, there is no need to do any extra set up.
Use the `lfs` config section to customize your Git LFS server.
> **Note**: The pure-SSH transfer is disabled by default.
## Server Access
Soft Serve at its core manages your server authentication and authorization. Authentication verifies the identity of a user, while authorization determines their access rights to a repository.
To manage the server users, access, and repos, you can use the SSH command line interface.
Try `ssh localhost -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes -p 23231 help` for more info. Make sure
you use your key here.
> **Note** The `IdentitiesOnly` option is used to prevent SSH from using any
> other keys in your `~/.ssh` directory. This is useful when you have multiple
> keys, and you want to use a specific key for Soft Serve.
For ease of use, instead of specifying the key, port, and hostname every time
you SSH into Soft Serve, add your own Soft Serve instance entry to your SSH
config. For instance, to use `ssh soft` instead of typing `ssh localhost -i
~/.ssh/id_ed25519 -o IdentitiesOnly=yes -p 23231`, we can define a `soft` entry in our SSH config
file `~/.ssh/config`.
```conf
Host soft
HostName localhost
Port 23231
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
```
Now, we can do `ssh soft` to SSH into Soft Serve. Since `git` is also aware of
this config, you can use `soft` as the hostname for your clone commands.
```sh
git clone ssh://soft/dotfiles
# make changes
# add & commit
git push origin main
```
> **Note** The `-i` and `-o` parts will be omitted in the examples below for brevity. You
> can add your server settings to your sshconfig for quicker access.
### Authentication
Everything that needs authentication is done using SSH. Make sure you have
added an entry for your Soft Serve instance in your `~/.ssh/config` file.
By default, Soft Serve gives read-only permission to anonymous connections to
any of the above protocols. This is controlled by two settings `anon-access`
and `allow-keyless`.
- `anon-access`: Defines the access level for anonymous users. Available
options are `no-access`, `read-only`, `read-write`, and `admin-access`.
Default is `read-only`.
- `allow-keyless`: Whether to allow connections that doesn't use keys to pass.
Setting this to `false` would disable access to SSH keyboard-interactive,
HTTP, and Git protocol connections. Default is `true`.
```sh
$ ssh -p 23231 localhost settings
Manage server settings
Usage:
ssh -p 23231 localhost settings [command]
Available Commands:
allow-keyless Set or get allow keyless access to repositories
anon-access Set or get the default access level for anonymous users
Flags:
-h, --help help for settings
Use "ssh -p 23231 localhost settings [command] --help" for more information about a command.
```
> **Note** These settings can only be changed by admins.
When `allow-keyless` is disabled, connections that don't use SSH Public Key
authentication will get denied. This means cloning repos over HTTP(s) or git://
will get denied.
Meanwhile, `anon-access` controls the access level granted to connections that
use SSH Public Key authentication but are not registered users. The default
setting for this is `read-only`. This will grant anonymous connections that use
SSH Public Key authentication `read-only` access to public repos.
`anon-access` is also used in combination with `allow-keyless` to determine the
access level for HTTP(s) and git:// clone requests.
#### SSH
Soft Serve doesn't allow duplicate SSH public keys for users. A public key can be associated with one user only. This makes SSH authentication simple and straight forward, add your public key to your Soft Serve user to be able to access Soft Serve.
#### HTTP
You can generate user access tokens through the SSH command line interface. Access tokens can have an optional expiration date. Use your access token as the basic auth user to access your Soft Serve repos through HTTP.
```sh
# Create a user token
ssh -p 23231 localhost token create 'my new token'
ss_1234abc56789012345678901234de246d798fghi
# Or with an expiry date
ssh -p 23231 localhost token create --expires-in 1y 'my other token'
ss_98fghi1234abc56789012345678901234de246d7
```
Now you can access to repos that require `read-write` access.
```sh
git clone http://ss_98fghi1234abc56789012345678901234de246d7@localhost:23232/my-private-repo.git my-private-repo
# Make changes and push
```
### Authorization
Soft Serve offers a simple access control. There are four access levels,
no-access, read-only, read-write, and admin-access.
`admin-access` has full control of the server and can make changes to users and repos.
`read-write` access gets full control of repos.
`read-only` can read public repos.
`no-access` denies access to all repos.
## User Management
Admins can manage users and their keys using the `user` command. Once a user is
created and has access to the server, they can manage their own keys and
settings.
To create a new user simply use `user create`:
```sh
# Create a new user
ssh -p 23231 localhost user create beatrice
# Add user keys
ssh -p 23231 localhost user add-pubkey beatrice ssh-rsa AAAAB3Nz...
ssh -p 23231 localhost user add-pubkey beatrice ssh-ed25519 AAAA...
# Create another user with public key
ssh -p 23231 localhost user create frankie '-k "ssh-ed25519 AAAATzN..."'
# Need help?
ssh -p 23231 localhost user help
```
Once a user is created, they get `read-only` access to public repositories.
They can also create new repositories on the server.
Users can manage their keys using the `pubkey` command:
```sh
# List user keys
ssh -p 23231 localhost pubkey list
# Add key
ssh -p 23231 localhost pubkey add ssh-ed25519 AAAA...
# Wanna change your username?
ssh -p 23231 localhost set-username yolo
# To display user info
ssh -p 23231 localhost info
```
## Repositories
You can manage repositories using the `repo` command.
```sh
# Run repo help
$ ssh -p 23231 localhost repo help
Manage repositories
Usage:
ssh -p 23231 localhost repo [command]
Aliases:
repo, repos, repository, repositories
Available Commands:
blob Print out the contents of file at path
branch Manage repository branches
collab Manage collaborators
create Create a new repository
delete Delete a repository
description Set or get the description for a repository
hide Hide or unhide a repository
import Import a new repository from remote
info Get information about a repository
is-mirror Whether a repository is a mirror
list List repositories
private Set or get a repository private property
project-name Set or get the project name for a repository
rename Rename an existing repository
tag Manage repository tags
tree Print repository tree at path
Flags:
-h, --help help for repo
Use "ssh -p 23231 localhost repo [command] --help" for more information about a command.
```
To use any of the above `repo` commands, a user must be a collaborator in the repository. More on this below.
### Creating Repositories
To create a repository, first make sure you are a registered user. Use the
`repo create <repo>` command to create a new repository:
```sh
# Create a new repository
ssh -p 23231 localhost repo create icecream
# Create a repo with description
ssh -p 23231 localhost repo create icecream '-d "This is an Ice Cream description"'
# ... and project name
ssh -p 23231 localhost repo create icecream '-d "This is an Ice Cream description"' '-n "Ice Cream"'
# I need my repository private!
ssh -p 23231 localhost repo create icecream -p '-d "This is an Ice Cream description"' '-n "Ice Cream"'
# Help?
ssh -p 23231 localhost repo create -h
```
Or you can add your Soft Serve server as a remote to any existing repo, given
you have write access, and push to remote:
```
git remote add origin ssh://localhost:23231/icecream
```
After you’ve added the remote just go ahead and push. If the repo doesn’t exist
on the server it’ll be created.
```
git push origin main
```
### Nested Repositories
Repositories can be nested too:
```sh
# Create a new nested repository
ssh -p 23231 localhost repo create charmbracelet/icecream
# Or ...
git remote add charm ssh://localhost:23231/charmbracelet/icecream
git push charm main
```
### Mirrors
You can also *import* repositories from any public remote. Use the `repo import` command.
```sh
ssh -p 23231 localhost repo import soft-serve https://github.com/charmbracelet/soft-serve
```
Use `--mirror` or `-m` to mark the repository as a *pull* mirror.
### Deleting Repositories
You can delete repositories using the `repo delete <repo>` command.
```sh
ssh -p 23231 localhost repo delete icecream
```
### Renaming Repositories
Use the `repo rename <old> <new>` command to rename existing repositories.
```sh
ssh -p 23231 localhost repo rename icecream vanilla
```
### Repository Collaborators
Sometimes you want to restrict write access to certain repositories. This can
be achieved by adding a collaborator to your repository.
Use the `repo collab <command> <repo>` command to manage repo collaborators.
```sh
# Add collaborator to soft-serve
ssh -p 23231 localhost repo collab add soft-serve frankie
# Add collaborator with a specific access level
ssh -p 23231 localhost repo collab add soft-serve beatrice read-only
# Remove collaborator
ssh -p 23231 localhost repo collab remove soft-serve beatrice
# List collaborators
ssh -p 23231 localhost repo collab list soft-serve
```
### Repository Metadata
You can also change the repo's description, project name, whether it's private,
etc using the `repo <command>` command.
```sh
# Set description for repo
ssh -p 23231 localhost repo description icecream "This is a new description"
# Hide repo from listing
ssh -p 23231 localhost repo hidden icecream true
# List repository info (branches, tags, description, etc)
ssh -p 23231 localhost repo icecream info
```
To make a repository private, use `repo private <repo> [true|false]`. Private
repos can only be accessed by admins and collaborators.
```sh
ssh -p 23231 localhost repo private icecream true
```
### Repository Branches & Tags
Use `repo branch` and `repo tag` to list, and delete branches or tags. You can
also use `repo branch default` to set or get the repository default branch.
### Repository Tree
To print a file tree for the project, just use the `repo tree` command along with
the repo name as the SSH command to your Soft Serve server:
```sh
ssh -p 23231 localhost repo tree soft-serve
```
You can also specify the sub-path and a specific reference or branch.
```sh
ssh -p 23231 localhost repo tree soft-serve server/config
ssh -p 23231 localhost repo tree soft-serve main server/config
```
From there, you can print individual files using the `repo blob` command:
```sh
ssh -p 23231 localhost repo blob soft-serve cmd/soft/main.go
```
You can add the `-c` flag to enable syntax coloring and `-l` to print line
numbers:
```sh
ssh -p 23231 localhost repo blob soft-serve cmd/soft/main.go -c -l
```
Use `--raw` to print raw file contents. This is useful for dumping binary data.
### Repository webhooks
Soft Serve supports repository webhooks using the `repo webhook` command. You
can create and manage webhooks for different repository events such as _push_,
_collaborators_, and _branch_tag_create_ events.
```
Manage repository webhooks
Usage:
ssh -p 23231 localhost repo webhook [command]
Aliases:
webhook, webhooks
Available Commands:
create Create a repository webhook
delete Delete a repository webhook
deliveries Manage webhook deliveries
list List repository webhooks
update Update a repository webhook
Flags:
-h, --help help for webhook
```
## The Soft Serve TUI
<img src="https://stuff.charm.sh/soft-serve/soft-serve-demo-commit.png" width="750" alt="TUI example showing a diff">
Soft Serve TUI is mainly used to browse repos over SSH. You can also use it to
browse local repositories with `soft browse` or running `soft` within a Git
repository.
```sh
ssh localhost -p 23231
```
It's also possible to “link” to a specific repo:
```sh
ssh -p 23231 localhost -t soft-serve
```
You can copy text to your clipboard over SSH. For instance, you can press
<kbd>c</kbd> on the highlighted repo in the menu to copy the clone command
[^osc52].
[^osc52]:
Copying over SSH depends on your terminal support of OSC52. Refer to
[go-osc52](https://github.com/aymanbagabas/go-osc52) for more information.
## Hooks
Soft Serve supports git server-side hooks `pre-receive`, `update`,
`post-update`, and `post-receive`. This means you can define your own hooks to
run on repository push events. Hooks can be defined as a per-repository hook,
and/or global hooks that run for all repositories.
You can find per-repository hooks under the repository `hooks` directory.
Globs hooks can be found in your `SOFT_SERVE_DATA_PATH` directory under
`hooks`. Defining global hooks is useful if you want to run CI/CD for example.
Here's an example of sending a message after receiving a push event. Create an
executable file `<data path>/hooks/update`:
```sh
#!/bin/sh
#
# An example hook script to echo information about the push
# and send it to the client.
refname="$1"
oldrev="$2"
newrev="$3"
# Safety check
if [ -z "$GIT_DIR" ]; then
echo "Don't run this script from the command line." >&2
echo " (if you want, you could supply GIT_DIR then run" >&2
echo " $0 <ref> <oldrev> <newrev>)" >&2
exit 1
fi
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
exit 1
fi
# Check types
# if $newrev is 0000...0000, it's a commit to delete a ref.
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
if [ "$newrev" = "$zero" ]; then
newrev_type=delete
else
newrev_type=$(git cat-file -t $newrev)
fi
echo "Hi from Soft Serve update hook!"
echo
echo "RefName: $refname"
echo "Change Type: $newrev_type"
echo "Old SHA1: $oldrev"
echo "New SHA1: $newrev"
exit 0
```
Now, you should get a message after pushing changes to any repository.
## A note about RSA keys
Unfortunately, due to a shortcoming in Go’s `x/crypto/ssh` package, Soft Serve
does not currently support access via new SSH RSA keys: only the old SHA-1
ones will work.
Until we sort this out you’ll either need an SHA-1 RSA key or a key with
another algorithm, e.g. Ed25519. Not sure what type of keys you have?
You can check with the following:
```sh
$ find ~/.ssh/id_*.pub -exec ssh-keygen -l -f {} \;
```
If you’re curious about the inner workings of this problem have a look at:
- https://github.com/golang/go/issues/37278
- https://go-review.googlesource.com/c/crypto/+/220037
- https://github.com/golang/crypto/pull/197
## Contributing
See [contributing][contribute].
[contribute]: https://github.com/charmbracelet/soft-serve/contribute
## Feedback
We’d love to hear your thoughts on this project. Feel free to drop us a note!
- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)
## License
[MIT](https://github.com/charmbracelet/soft-serve/raw/main/LICENSE)
---
Part of [Charm](https://charm.sh).
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source
================================================
FILE: browse.tape
================================================
Set Width 1600
Set Height 900
Set FontSize 22
Output soft-serve-browse.gif
Output soft-serve-frames/
Type@300ms "soft"
Enter
Sleep 2s
Type@1s "ddd"
Sleep 2s
Type@1s "uuu"
Sleep 2s
Tab@1s
Sleep 1s
Down@300ms 4
Enter
Sleep 1s
Down@300ms 13
Enter
Sleep 1s
Down@300ms 5
Enter
Down@300ms 20
Sleep 2s
Type@500ms "b"
Sleep 2.5s
Down@300ms 50
Sleep 2.5s
Tab@1s
Down@500ms 4
Up@500ms 2
Enter
Down@250ms 50
Sleep 1s
Tab@1s
Down@500ms 8
Enter
Down@250ms 30
Tab@2s
Down@500ms 5
Up@500ms 2
Sleep 2.5s
Tab@2s
Down@500ms 8
Sleep 2s
================================================
FILE: cmd/cmd.go
================================================
package cmd
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"github.com/charmbracelet/soft-serve/pkg/backend"
"github.com/charmbracelet/soft-serve/pkg/config"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/hooks"
"github.com/charmbracelet/soft-serve/pkg/store"
"github.com/charmbracelet/soft-serve/pkg/store/database"
"github.com/spf13/cobra"
)
// InitBackendContext initializes the backend context.
func InitBackendContext(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
cfg := config.FromContext(ctx)
if _, err := os.Stat(cfg.DataPath); errors.Is(err, fs.ErrNotExist) {
if err := os.MkdirAll(cfg.DataPath, os.ModePerm); err != nil {
return fmt.Errorf("create data directory: %w", err)
}
}
dbx, err := db.Open(ctx, cfg.DB.Driver, cfg.DB.DataSource)
if err != nil {
return fmt.Errorf("open database: %w", err)
}
ctx = db.WithContext(ctx, dbx)
dbstore := database.New(ctx, dbx)
ctx = store.WithContext(ctx, dbstore)
be := backend.New(ctx, cfg, dbx, dbstore)
ctx = backend.WithContext(ctx, be)
cmd.SetContext(ctx)
return nil
}
// CloseDBContext closes the database context.
func CloseDBContext(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
dbx := db.FromContext(ctx)
if dbx != nil {
if err := dbx.Close(); err != nil {
return fmt.Errorf("close database: %w", err)
}
}
return nil
}
// InitializeHooks initializes the hooks.
func InitializeHooks(ctx context.Context, cfg *config.Config, be *backend.Backend) error {
repos, err := be.Repositories(ctx)
if err != nil {
return err
}
for _, repo := range repos {
if err := hooks.GenerateHooks(ctx, cfg, repo.Name()); err != nil {
return err
}
}
return nil
}
================================================
FILE: cmd/soft/admin/admin.go
================================================
package admin
import (
"fmt"
"github.com/charmbracelet/soft-serve/cmd"
"github.com/charmbracelet/soft-serve/pkg/backend"
"github.com/charmbracelet/soft-serve/pkg/config"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/db/migrate"
"github.com/spf13/cobra"
)
var (
// Command is the admin command.
Command = &cobra.Command{
Use: "admin",
Short: "Administrate the server",
}
migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Migrate the database to the latest version",
PersistentPreRunE: cmd.InitBackendContext,
PersistentPostRunE: cmd.CloseDBContext,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
db := db.FromContext(ctx)
if err := migrate.Migrate(ctx, db); err != nil {
return fmt.Errorf("migration: %w", err)
}
return nil
},
}
rollbackCmd = &cobra.Command{
Use: "rollback",
Short: "Rollback the database to the previous version",
PersistentPreRunE: cmd.InitBackendContext,
PersistentPostRunE: cmd.CloseDBContext,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
db := db.FromContext(ctx)
if err := migrate.Rollback(ctx, db); err != nil {
return fmt.Errorf("rollback: %w", err)
}
return nil
},
}
syncHooksCmd = &cobra.Command{
Use: "sync-hooks",
Short: "Update repository hooks",
PersistentPreRunE: cmd.InitBackendContext,
PersistentPostRunE: cmd.CloseDBContext,
RunE: func(c *cobra.Command, _ []string) error {
ctx := c.Context()
cfg := config.FromContext(ctx)
be := backend.FromContext(ctx)
if err := cmd.InitializeHooks(ctx, cfg, be); err != nil {
return fmt.Errorf("initialize hooks: %w", err)
}
return nil
},
}
)
func init() {
Command.AddCommand(
syncHooksCmd,
migrateCmd,
rollbackCmd,
)
}
================================================
FILE: cmd/soft/browse/browse.go
================================================
package browse
import (
"fmt"
"path/filepath"
"time"
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/soft-serve/git"
"github.com/charmbracelet/soft-serve/pkg/proto"
"github.com/charmbracelet/soft-serve/pkg/ui/common"
"github.com/charmbracelet/soft-serve/pkg/ui/components/footer"
"github.com/charmbracelet/soft-serve/pkg/ui/pages/repo"
"github.com/spf13/cobra"
)
// Command is the browse command.
var Command = &cobra.Command{
Use: "browse PATH",
Short: "Browse a repository",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
rp := "."
if len(args) > 0 {
rp = args[0]
}
abs, err := filepath.Abs(rp)
if err != nil {
return err
}
r, err := git.Open(abs)
if err != nil {
return fmt.Errorf("failed to open repository: %w", err)
}
// Bubble Tea uses Termenv default output so we have to use the same
// thing here.
ctx := cmd.Context()
c := common.NewCommon(ctx, 0, 0)
c.HideCloneCmd = true
comps := []common.TabComponent{
repo.NewReadme(c),
repo.NewFiles(c),
repo.NewLog(c),
}
if !r.IsBare {
comps = append(comps, repo.NewStash(c))
}
comps = append(comps, repo.NewRefs(c, git.RefsHeads), repo.NewRefs(c, git.RefsTags))
m := &model{
model: repo.New(c, comps...),
repo: repository{r},
common: c,
}
m.footer = footer.New(c, m)
p := tea.NewProgram(m)
_, err = p.Run()
return err
},
}
type state int
const (
startState state = iota
errorState
)
type model struct {
model *repo.Repo
footer *footer.Footer
repo proto.Repository
common common.Common
state state
showFooter bool
error error
}
var _ tea.Model = &model{}
func (m *model) SetSize(w, h int) {
m.common.SetSize(w, h)
style := m.common.Styles.App
wm := style.GetHorizontalFrameSize()
hm := style.GetVerticalFrameSize()
if m.showFooter {
hm += m.footer.Height()
}
m.footer.SetSize(w-wm, h-hm)
m.model.SetSize(w-wm, h-hm)
}
// ShortHelp implements help.KeyMap.
func (m model) ShortHelp() []key.Binding {
switch m.state {
case errorState:
return []key.Binding{
m.common.KeyMap.Back,
m.common.KeyMap.Quit,
m.common.KeyMap.Help,
}
default:
return m.model.ShortHelp()
}
}
// FullHelp implements help.KeyMap.
func (m model) FullHelp() [][]key.Binding {
switch m.state {
case errorState:
return [][]key.Binding{
{
m.common.KeyMap.Back,
},
{
m.common.KeyMap.Quit,
m.common.KeyMap.Help,
},
}
default:
return m.model.FullHelp()
}
}
// Init implements tea.Model.
func (m *model) Init() tea.Cmd {
return tea.Batch(
m.model.Init(),
m.footer.Init(),
func() tea.Msg {
return repo.RepoMsg(m.repo)
},
repo.UpdateRefCmd(m.repo),
)
}
// Update implements tea.Model.
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.common.Logger.Debugf("msg received: %T", msg)
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.SetSize(msg.Width, msg.Height)
case tea.KeyPressMsg:
switch {
case key.Matches(msg, m.common.KeyMap.Back) && m.error != nil:
m.error = nil
m.state = startState
// Always show the footer on error.
m.showFooter = m.footer.ShowAll()
case key.Matches(msg, m.common.KeyMap.Help):
cmds = append(cmds, footer.ToggleFooterCmd)
case key.Matches(msg, m.common.KeyMap.Quit):
// Stop bubblezone background workers.
m.common.Zone.Close()
return m, tea.Quit
}
case tea.MouseClickMsg:
mouse := msg.Mouse()
switch mouse.Button {
case tea.MouseLeft:
switch {
case m.common.Zone.Get("footer").InBounds(msg):
cmds = append(cmds, footer.ToggleFooterCmd)
}
}
case footer.ToggleFooterMsg:
m.footer.SetShowAll(!m.footer.ShowAll())
m.showFooter = !m.showFooter
case common.ErrorMsg:
m.error = msg
m.state = errorState
m.showFooter = true
}
f, cmd := m.footer.Update(msg)
m.footer = f.(*footer.Footer)
if cmd != nil {
cmds = append(cmds, cmd)
}
r, cmd := m.model.Update(msg)
m.model = r.(*repo.Repo)
if cmd != nil {
cmds = append(cmds, cmd)
}
// This fixes determining the height margin of the footer.
m.SetSize(m.common.Width, m.common.Height)
return m, tea.Batch(cmds...)
}
// View implements tea.Model.
func (m *model) View() tea.View {
var v tea.View
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion
style := m.common.Styles.App
wm, hm := style.GetHorizontalFrameSize(), style.GetVerticalFrameSize()
if m.showFooter {
hm += m.footer.Height()
}
var view string
switch m.state {
case startState:
view = m.model.View()
case errorState:
err := m.common.Styles.ErrorTitle.Render("Bummer")
err += m.common.Styles.ErrorBody.Render(m.error.Error())
view = m.common.Styles.Error.
Width(m.common.Width -
wm -
m.common.Styles.ErrorBody.GetHorizontalFrameSize()).
Height(m.common.Height -
hm -
m.common.Styles.Error.GetVerticalFrameSize()).
Render(err)
}
if m.showFooter {
view = lipgloss.JoinVertical(lipgloss.Left, view, m.footer.View())
}
v.Content = m.common.Zone.Scan(style.Render(view))
return v
}
type repository struct {
r *git.Repository
}
var _ proto.Repository = repository{}
// Description implements proto.Repository.
func (r repository) Description() string {
return ""
}
// ID implements proto.Repository.
func (r repository) ID() int64 {
return 0
}
// IsHidden implements proto.Repository.
func (repository) IsHidden() bool {
return false
}
// IsMirror implements proto.Repository.
func (repository) IsMirror() bool {
return false
}
// IsPrivate implements proto.Repository.
func (repository) IsPrivate() bool {
return false
}
// Name implements proto.Repository.
func (r repository) Name() string {
return filepath.Base(r.r.Path)
}
// Open implements proto.Repository.
func (r repository) Open() (*git.Repository, error) {
return r.r, nil
}
// ProjectName implements proto.Repository.
func (r repository) ProjectName() string {
return r.Name()
}
// UpdatedAt implements proto.Repository.
func (r repository) UpdatedAt() time.Time {
t, err := r.r.LatestCommitTime()
if err != nil {
return time.Time{}
}
return t
}
// UserID implements proto.Repository.
func (r repository) UserID() int64 {
return 0
}
// CreatedAt implements proto.Repository.
func (r repository) CreatedAt() time.Time {
return time.Time{}
}
================================================
FILE: cmd/soft/hook/hook.go
================================================
package hook
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"charm.land/log/v2"
"github.com/charmbracelet/soft-serve/cmd"
"github.com/charmbracelet/soft-serve/pkg/backend"
"github.com/charmbracelet/soft-serve/pkg/config"
"github.com/charmbracelet/soft-serve/pkg/hooks"
"github.com/spf13/cobra"
)
var (
// ErrInternalServerError indicates that an internal server error occurred.
ErrInternalServerError = errors.New("internal server error")
// Deprecated: this flag is ignored.
configPath string
// Command is the hook command.
Command = &cobra.Command{
Use: "hook",
Short: "Run git server hooks",
Long: "Handles Soft Serve git server hooks.",
Hidden: true,
PersistentPreRunE: func(c *cobra.Command, args []string) error {
logger := log.FromContext(c.Context())
if err := cmd.InitBackendContext(c, args); err != nil {
logger.Error("failed to initialize backend context", "err", err)
return ErrInternalServerError
}
return nil
},
PersistentPostRunE: func(c *cobra.Command, args []string) error {
logger := log.FromContext(c.Context())
if err := cmd.CloseDBContext(c, args); err != nil {
logger.Error("failed to close backend", "err", err)
return ErrInternalServerError
}
return nil
},
}
// Git hooks read the config from the environment, based on
// $SOFT_SERVE_DATA_PATH. We already parse the config when the binary
// starts, so we don't need to do it again.
// The --config flag is now deprecated.
hooksRunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
hks := backend.FromContext(ctx)
cfg := config.FromContext(ctx)
// This is set in the server before invoking git-receive-pack/git-upload-pack
repoName := os.Getenv("SOFT_SERVE_REPO_NAME")
logger := log.FromContext(ctx).With("repo", repoName)
stdin := cmd.InOrStdin()
stdout := cmd.OutOrStdout()
stderr := cmd.ErrOrStderr()
cmdName := cmd.Name()
customHookPath := filepath.Join(cfg.DataPath, "hooks", cmdName)
var buf bytes.Buffer
opts := make([]hooks.HookArg, 0)
switch cmdName {
case hooks.PreReceiveHook, hooks.PostReceiveHook:
scanner := bufio.NewScanner(stdin)
for scanner.Scan() {
buf.Write(scanner.Bytes())
buf.WriteByte('\n')
fields := strings.Fields(scanner.Text())
if len(fields) != 3 {
logger.Error(fmt.Sprintf("invalid %s hook input", cmdName), "input", scanner.Text())
continue
}
opts = append(opts, hooks.HookArg{
OldSha: fields[0],
NewSha: fields[1],
RefName: fields[2],
})
}
switch cmdName {
case hooks.PreReceiveHook:
hks.PreReceive(ctx, stdout, stderr, repoName, opts)
case hooks.PostReceiveHook:
hks.PostReceive(ctx, stdout, stderr, repoName, opts)
}
case hooks.UpdateHook:
if len(args) != 3 {
logger.Error("invalid update hook input", "input", args)
break
}
hks.Update(ctx, stdout, stderr, repoName, hooks.HookArg{
RefName: args[0],
OldSha: args[1],
NewSha: args[2],
})
case hooks.PostUpdateHook:
hks.PostUpdate(ctx, stdout, stderr, repoName, args...)
}
// Custom hooks
if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 {
// If the custom hook is executable, run it
if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil {
logger.Error("failed to run custom hook", "err", err)
}
}
return nil
}
preReceiveCmd = &cobra.Command{
Use: "pre-receive",
Short: "Run git pre-receive hook",
RunE: hooksRunE,
}
updateCmd = &cobra.Command{
Use: "update",
Short: "Run git update hook",
Args: cobra.ExactArgs(3),
RunE: hooksRunE,
}
postReceiveCmd = &cobra.Command{
Use: "post-receive",
Short: "Run git post-receive hook",
RunE: hooksRunE,
}
postUpdateCmd = &cobra.Command{
Use: "post-update",
Short: "Run git post-update hook",
RunE: hooksRunE,
}
)
func init() {
Command.PersistentFlags().StringVar(&configPath, "config", "", "path to config file (deprecated)")
Command.AddCommand(
preReceiveCmd,
updateCmd,
postReceiveCmd,
postUpdateCmd,
)
}
func runCommand(ctx context.Context, in io.Reader, out io.Writer, err io.Writer, name string, args ...string) error {
cmd := exec.CommandContext(ctx, name, args...)
cmd.Stdin = in
cmd.Stdout = out
cmd.Stderr = err
return cmd.Run()
}
================================================
FILE: cmd/soft/main.go
================================================
package main
import (
"context"
"fmt"
"os"
"runtime/debug"
"strconv"
"charm.land/log/v2"
"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/soft-serve/cmd/soft/admin"
"github.com/charmbracelet/soft-serve/cmd/soft/browse"
"github.com/charmbracelet/soft-serve/cmd/soft/hook"
"github.com/charmbracelet/soft-serve/cmd/soft/serve"
"github.com/charmbracelet/soft-serve/pkg/config"
logr "github.com/charmbracelet/soft-serve/pkg/log"
"github.com/charmbracelet/soft-serve/pkg/ui/common"
"github.com/charmbracelet/soft-serve/pkg/version"
mcobra "github.com/muesli/mango-cobra"
"github.com/muesli/roff"
"github.com/spf13/cobra"
"go.uber.org/automaxprocs/maxprocs"
)
var (
// Version contains the application version number. It's set via ldflags
// when building.
Version = ""
// CommitSHA contains the SHA of the commit that this application was built
// against. It's set via ldflags when building.
CommitSHA = ""
// CommitDate contains the date of the commit that this application was
// built against. It's set via ldflags when building.
CommitDate = ""
rootCmd = &cobra.Command{
Use: "soft",
Short: "A self-hostable Git server for the command line",
Long: "Soft Serve is a self-hostable Git server for the command line.",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return browse.Command.RunE(cmd, args)
},
}
manCmd = &cobra.Command{
Use: "man",
Short: "Generate man pages",
Args: cobra.NoArgs,
Hidden: true,
RunE: func(_ *cobra.Command, _ []string) error {
manPage, err := mcobra.NewManPage(1, rootCmd) //.
if err != nil {
return err
}
manPage = manPage.WithSection("Copyright", "(C) 2021-2023 Charmbracelet, Inc.\n"+
"Released under MIT license.")
fmt.Println(manPage.Build(roff.NewDocument()))
return nil
},
}
)
func init() {
if noColor, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_NO_COLOR")); noColor {
common.DefaultColorProfile = colorprofile.NoTTY
}
rootCmd.AddCommand(
manCmd,
serve.Command,
hook.Command,
admin.Command,
browse.Command,
)
rootCmd.CompletionOptions.HiddenDefaultCmd = true
if len(CommitSHA) >= 7 {
vt := rootCmd.VersionTemplate()
rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n")
}
if Version == "" {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
Version = info.Main.Version
} else {
Version = "unknown (built from source)"
}
}
rootCmd.Version = Version
version.Version = Version
version.CommitSHA = CommitSHA
version.CommitDate = CommitDate
}
func main() {
ctx := context.Background()
cfg := config.DefaultConfig()
if cfg.Exist() {
if err := cfg.Parse(); err != nil {
log.Fatal(err)
}
}
if err := cfg.ParseEnv(); err != nil {
log.Fatal(err)
}
ctx = config.WithContext(ctx, cfg)
logger, f, err := logr.NewLogger(cfg)
if err != nil {
log.Errorf("failed to create logger: %v", err)
}
ctx = log.WithContext(ctx, logger)
if f != nil {
defer f.Close() //nolint: errcheck
}
// Set global logger
log.SetDefault(logger)
var opts []maxprocs.Option
if config.IsVerbose() {
opts = append(opts, maxprocs.Logger(log.Debugf))
}
// Set the max number of processes to the number of CPUs
// This is useful when running soft serve in a container
if _, err := maxprocs.Set(opts...); err != nil {
log.Warn("couldn't set automaxprocs", "error", err)
}
if err := rootCmd.ExecuteContext(ctx); err != nil {
os.Exit(1)
}
}
================================================
FILE: cmd/soft/serve/certreloader.go
================================================
package serve
import (
"crypto/tls"
"sync"
"charm.land/log/v2"
)
// CertReloader is responsible for reloading TLS certificates when a SIGHUP signal is received.
type CertReloader struct {
certMu sync.RWMutex
cert *tls.Certificate
certPath string
keyPath string
}
// NewCertReloader creates a new CertReloader that watches for SIGHUP signals.
func NewCertReloader(certPath, keyPath string, logger *log.Logger) (*CertReloader, error) {
reloader := &CertReloader{
certPath: certPath,
keyPath: keyPath,
}
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}
reloader.cert = &cert
return reloader, nil
}
// Reload attempts to reload the certificate and key.
func (cr *CertReloader) Reload() error {
newCert, err := tls.LoadX509KeyPair(cr.certPath, cr.keyPath)
if err != nil {
return err
}
cr.certMu.Lock()
defer cr.certMu.Unlock()
cr.cert = &newCert
return nil
}
// GetCertificateFunc returns a function that can be used with tls.Config.GetCertificate.
func (cr *CertReloader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cr.certMu.RLock()
defer cr.certMu.RUnlock()
return cr.cert, nil
}
}
================================================
FILE: cmd/soft/serve/certreloader_test.go
================================================
//go:build unix
package serve
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"os"
"os/signal"
"path/filepath"
"syscall"
"testing"
"time"
"charm.land/log/v2"
)
func generateTestCert(t *testing.T, certPath, keyPath, cn string) {
t.Helper()
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
template := x509.Certificate{
SerialNumber: nil,
Subject: pkix.Name{
CommonName: cn,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
}
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
t.Fatal(err)
}
certFile, err := os.Create(certPath)
if err != nil {
t.Fatal(err)
}
defer certFile.Close()
pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
keyFile, err := os.Create(keyPath)
if err != nil {
t.Fatal(err)
}
defer keyFile.Close()
pem.Encode(keyFile, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
})
}
func TestCertReloader(t *testing.T) {
dir := t.TempDir()
certPath := filepath.Join(dir, "/cert.pem")
keyPath := filepath.Join(dir, "/key.pem")
// Initial cert
generateTestCert(t, certPath, keyPath, "cert-v1")
logger := log.New(os.Stderr)
certReloader, err := NewCertReloader(certPath, keyPath, logger)
if err != nil {
t.Fatalf("failed to create reloader: %v", err)
}
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP)
for range sigCh {
if err := certReloader.Reload(); err != nil {
logger.Error("failed to reload certificate", "err", err)
} else {
logger.Info("certificate reloaded successfully")
}
}
}()
getCert := certReloader.GetCertificateFunc()
cert1, err := getCert(nil)
if err != nil {
t.Fatal(err)
}
// Replace cert on disk
generateTestCert(t, certPath, keyPath, "cert-v2")
// Trigger reload
if err := syscall.Kill(os.Getpid(), syscall.SIGHUP); err != nil {
t.Fatalf("failed to send SIGHUP: %v", err)
}
// Allow async goroutine to reload
time.Sleep(100 * time.Millisecond)
cert2, err := getCert(nil)
if err != nil {
t.Fatal(err)
}
if cert1 == cert2 {
t.Fatal("certificate was not reloaded after SIGHUP")
}
}
================================================
FILE: cmd/soft/serve/serve.go
================================================
package serve
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"sync"
"syscall"
"time"
"github.com/charmbracelet/soft-serve/cmd"
"github.com/charmbracelet/soft-serve/pkg/backend"
"github.com/charmbracelet/soft-serve/pkg/config"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/db/migrate"
"github.com/spf13/cobra"
)
var (
syncHooks bool
// Command is the serve command.
Command = &cobra.Command{
Use: "serve",
Short: "Start the server",
Args: cobra.NoArgs,
PersistentPreRunE: cmd.InitBackendContext,
PersistentPostRunE: cmd.CloseDBContext,
RunE: func(c *cobra.Command, _ []string) error {
ctx := c.Context()
cfg := config.DefaultConfig()
if cfg.Exist() {
if err := cfg.ParseFile(); err != nil {
return fmt.Errorf("parse config file: %w", err)
}
} else {
if err := cfg.WriteConfig(); err != nil {
return fmt.Errorf("write config file: %w", err)
}
}
if err := cfg.ParseEnv(); err != nil {
return fmt.Errorf("parse environment variables: %w", err)
}
// Create custom hooks directory if it doesn't exist
customHooksPath := filepath.Join(cfg.DataPath, "hooks")
if _, err := os.Stat(customHooksPath); err != nil && os.IsNotExist(err) {
os.MkdirAll(customHooksPath, os.ModePerm) //nolint: errcheck
// Generate update hook example without executable permissions
hookPath := filepath.Join(customHooksPath, "update.sample")
//nolint: gosec
if err := os.WriteFile(hookPath, []byte(updateHookExample), 0o744); err != nil {
return fmt.Errorf("failed to generate update hook example: %w", err)
}
}
// Create log directory if it doesn't exist
logPath := filepath.Join(cfg.DataPath, "log")
if _, err := os.Stat(logPath); err != nil && os.IsNotExist(err) {
os.MkdirAll(logPath, os.ModePerm) //nolint: errcheck
}
db := db.FromContext(ctx)
if err := migrate.Migrate(ctx, db); err != nil {
return fmt.Errorf("migration error: %w", err)
}
s, err := NewServer(ctx)
if err != nil {
return fmt.Errorf("start server: %w", err)
}
if syncHooks {
be := backend.FromContext(ctx)
if err := cmd.InitializeHooks(ctx, cfg, be); err != nil {
return fmt.Errorf("initialize hooks: %w", err)
}
}
lch := make(chan error, 1)
done := make(chan os.Signal, 1)
doneOnce := sync.OnceFunc(func() { close(done) })
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
// This endpoint is added for testing purposes
// It allows us to stop the server from the test suite.
// This is needed since Windows doesn't support signals.
if testRun, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_TESTRUN")); testRun {
h := s.HTTPServer.Server.Handler
s.HTTPServer.Server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/__stop" && r.Method == http.MethodHead {
doneOnce()
return
}
h.ServeHTTP(w, r)
})
}
go func() {
lch <- s.Start()
doneOnce()
}()
for {
select {
case err := <-lch:
if err != nil {
return fmt.Errorf("server error: %w", err)
}
case sig := <-done:
if sig == syscall.SIGHUP {
s.logger.Info("received SIGHUP signal, reloading TLS certificates if enabled")
if err := s.ReloadCertificates(); err != nil {
s.logger.Error("failed to reload TLS certificates", "err", err)
}
continue
}
}
break
}
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := s.Shutdown(ctx); err != nil {
return err
}
return nil
},
}
)
func init() {
Command.Flags().BoolVarP(&syncHooks, "sync-hooks", "", false, "synchronize hooks for all repositories before running the server")
}
const updateHookExample = `#!/bin/sh
#
# An example hook script to echo information about the push
# and send it to the client.
#
# To enable this hook, rename this file to "update" and make it executable.
refname="$1"
oldrev="$2"
newrev="$3"
# Safety check
if [ -z "$GIT_DIR" ]; then
echo "Don't run this script from the command line." >&2
echo " (if you want, you could supply GIT_DIR then run" >&2
echo " $0 <ref> <oldrev> <newrev>)" >&2
exit 1
fi
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
exit 1
fi
# Check types
# if $newrev is 0000...0000, it's a commit to delete a ref.
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
if [ "$newrev" = "$zero" ]; then
newrev_type=delete
else
newrev_type=$(git cat-file -t $newrev)
fi
echo "Hi from Soft Serve update hook!"
echo
echo "Repository: $SOFT_SERVE_REPO_NAME"
echo "RefName: $refname"
echo "Change Type: $newrev_type"
echo "Old SHA1: $oldrev"
echo "New SHA1: $newrev"
exit 0
`
================================================
FILE: cmd/soft/serve/server.go
================================================
package serve
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
"charm.land/log/v2"
"github.com/charmbracelet/soft-serve/pkg/backend"
"github.com/charmbracelet/soft-serve/pkg/config"
"github.com/charmbracelet/soft-serve/pkg/cron"
"github.com/charmbracelet/soft-serve/pkg/daemon"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/jobs"
sshsrv "github.com/charmbracelet/soft-serve/pkg/ssh"
"github.com/charmbracelet/soft-serve/pkg/stats"
"github.com/charmbracelet/soft-serve/pkg/web"
"github.com/charmbracelet/ssh"
"golang.org/x/sync/errgroup"
)
// Server is the Soft Serve server.
type Server struct {
SSHServer *sshsrv.SSHServer
GitDaemon *daemon.GitDaemon
HTTPServer *web.HTTPServer
StatsServer *stats.StatsServer
CertLoader *CertReloader
Cron *cron.Scheduler
Config *config.Config
Backend *backend.Backend
DB *db.DB
logger *log.Logger
ctx context.Context
}
// NewServer returns a new *Server configured to serve Soft Serve. The SSH
// server key-pair will be created if none exists.
// It expects a context with *backend.Backend, *db.DB, *log.Logger, and
// *config.Config attached.
func NewServer(ctx context.Context) (*Server, error) {
var err error
cfg := config.FromContext(ctx)
be := backend.FromContext(ctx)
db := db.FromContext(ctx)
logger := log.FromContext(ctx).WithPrefix("server")
srv := &Server{
Config: cfg,
Backend: be,
DB: db,
logger: log.FromContext(ctx).WithPrefix("server"),
ctx: ctx,
}
// Add cron jobs.
sched := cron.NewScheduler(ctx)
for n, j := range jobs.List() {
id, err := sched.AddFunc(j.Runner.Spec(ctx), j.Runner.Func(ctx))
if err != nil {
logger.Warn("error adding cron job", "job", n, "err", err)
}
j.ID = id
}
srv.Cron = sched
srv.SSHServer, err = sshsrv.NewSSHServer(ctx)
if err != nil {
return nil, fmt.Errorf("create ssh server: %w", err)
}
srv.GitDaemon, err = daemon.NewGitDaemon(ctx)
if err != nil {
return nil, fmt.Errorf("create git daemon: %w", err)
}
srv.HTTPServer, err = web.NewHTTPServer(ctx)
if err != nil {
return nil, fmt.Errorf("create http server: %w", err)
}
srv.StatsServer, err = stats.NewStatsServer(ctx)
if err != nil {
return nil, fmt.Errorf("create stats server: %w", err)
}
if cfg.HTTP.TLSKeyPath != "" && cfg.HTTP.TLSCertPath != "" {
srv.CertLoader, err = NewCertReloader(cfg.HTTP.TLSCertPath, cfg.HTTP.TLSKeyPath, logger)
if err != nil {
return nil, fmt.Errorf("create cert reloader: %w", err)
}
srv.HTTPServer.SetTLSConfig(&tls.Config{
GetCertificate: srv.CertLoader.GetCertificateFunc(),
})
}
return srv, nil
}
// ReloadCertificates reloads the TLS certificates for the HTTP server.
func (s *Server) ReloadCertificates() error {
if s.CertLoader == nil {
return nil
}
return s.CertLoader.Reload()
}
// Start starts the SSH server.
func (s *Server) Start() error {
errg, _ := errgroup.WithContext(s.ctx)
// optionally start the SSH server
if s.Config.SSH.Enabled {
errg.Go(func() error {
s.logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
if err := s.SSHServer.ListenAndServe(); !errors.Is(err, ssh.ErrServerClosed) {
return err
}
return nil
})
}
// optionally start the git daemon
if s.Config.Git.Enabled {
errg.Go(func() error {
s.logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
if err := s.GitDaemon.ListenAndServe(); !errors.Is(err, daemon.ErrServerClosed) {
return err
}
return nil
})
}
// optionally start the HTTP server
if s.Config.HTTP.Enabled {
errg.Go(func() error {
s.logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
if err := s.HTTPServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
}
// optionally start the Stats server
if s.Config.Stats.Enabled {
errg.Go(func() error {
s.logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
if err := s.StatsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
}
errg.Go(func() error {
s.Cron.Start()
return nil
})
return errg.Wait()
}
// Shutdown lets the server gracefully shutdown.
func (s *Server) Shutdown(ctx context.Context) error {
errg, ctx := errgroup.WithContext(ctx)
errg.Go(func() error {
return s.GitDaemon.Shutdown(ctx)
})
errg.Go(func() error {
return s.HTTPServer.Shutdown(ctx)
})
errg.Go(func() error {
return s.SSHServer.Shutdown(ctx)
})
errg.Go(func() error {
return s.StatsServer.Shutdown(ctx)
})
errg.Go(func() error {
for _, j := range jobs.List() {
s.Cron.Remove(j.ID)
}
s.Cron.Stop()
return nil
})
// defer s.DB.Close() // nolint: errcheck
return errg.Wait()
}
// Close closes the SSH server.
func (s *Server) Close() error {
var errg errgroup.Group
errg.Go(s.GitDaemon.Close)
errg.Go(s.HTTPServer.Close)
errg.Go(s.SSHServer.Close)
errg.Go(s.StatsServer.Close)
errg.Go(func() error {
s.Cron.Stop()
return nil
})
// defer s.DB.Close() // nolint: errcheck
return errg.Wait()
}
================================================
FILE: codecov.yml
================================================
coverage:
status:
project:
default:
target: 50%
patch:
default:
target: 30%
================================================
FILE: demo.tape
================================================
Set Width 1600
Set Height 900
Set FontSize 22
Output soft-serve.gif
Output soft-serve-frames/
Type "ssh git.charm.sh"
Sleep 1s
Enter
Sleep 2s
Type@500ms "jjj"
Sleep 1s
Type@250ms "kkk"
Enter
Sleep 1s
Down@300ms 10
Sleep 1s
Tab@1s 2
Down@300ms 3
Enter
Down@250ms 30
Sleep 1s
Type "h"
Sleep 1s
Tab@1s 4
Sleep 500ms
Down@300ms 4
Enter
Down@300ms 2
Enter
Down
Sleep 1s
Enter
Down@250ms 50
Sleep 2.5s
Escape
Sleep 2s
================================================
FILE: docker.md
================================================
# Running Soft-Serve with Docker
The official Soft Serve Docker images are available at [charmcli/soft-serve][docker]. Development and nightly builds are available at [ghcr.io/charmbracelet/soft-serve][ghcr]
```sh
docker pull charmcli/soft-serve:latest
```
Here’s how you might run `soft-serve` as a container. Keep in mind that
repositories are stored in the `/soft-serve` directory, so you’ll likely want
to mount that directory as a volume in order keep your repositories backed up.
```sh
docker run \
--name=soft-serve \
--volume /path/to/data:/soft-serve \
--publish 23231:23231 \
--publish 23232:23232 \
--publish 23233:23233 \
--publish 9418:9418 \
-e SOFT_SERVE_INITIAL_ADMIN_KEYS="YOUR_ADMIN_KEY_HERE" \
--restart unless-stopped \
charmcli/soft-serve:latest
```
Or by using docker-compose:
```yaml
---
version: "3.1"
services:
soft-serve:
image: charmcli/soft-serve:latest
container_name: soft-serve
volumes:
- /path/to/data:/soft-serve
ports:
- 23231:23231
- 23232:23232
- 23233:23233
- 9418:9418
environment:
SOFT_SERVE_INITIAL_ADMIN_KEYS: "YOUR_ADMIN_KEY_HERE"
restart: unless-stopped
```
[docker]: https://hub.docker.com/r/charmcli/soft-serve
[ghcr]: https://github.com/charmbracelet/soft-serve/pkgs/container/soft-serve
> **Warning**
>
> Make sure to run the image without a TTY, i.e.: do not use the `--tty`/`-t`
> flags.
***
Part of [Charm](https://charm.sh).
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge-unrounded.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source
================================================
FILE: git/attr.go
================================================
package git
import (
"math/rand"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// Attribute represents a Git attribute.
type Attribute struct {
Name string
Value string
}
// CheckAttributes checks the attributes of the given ref and path.
func (r *Repository) CheckAttributes(ref *Reference, path string) ([]Attribute, error) {
rnd := rand.NewSource(time.Now().UnixNano())
fn := "soft-serve-index-" + strconv.Itoa(rand.New(rnd).Int()) //nolint: gosec
tmpindex := filepath.Join(os.TempDir(), fn)
defer os.Remove(tmpindex) //nolint: errcheck
readTree := NewCommand("read-tree", "--reset", "-i", ref.Name().String()).
AddEnvs("GIT_INDEX_FILE=" + tmpindex)
if _, err := readTree.RunInDir(r.Path); err != nil {
return nil, err
}
checkAttr := NewCommand("check-attr", "--cached", "-a", "--", path).
AddEnvs("GIT_INDEX_FILE=" + tmpindex)
out, err := checkAttr.RunInDir(r.Path)
if err != nil {
return nil, err
}
return parseAttributes(path, out), nil
}
func parseAttributes(path string, buf []byte) []Attribute {
attrs := make([]Attribute, 0)
for _, line := range strings.Split(string(buf), "\n") {
if line == "" {
continue
}
line = strings.TrimPrefix(line, path+": ")
parts := strings.SplitN(line, ": ", 2)
if len(parts) != 2 {
continue
}
attrs = append(attrs, Attribute{
Name: parts[0],
Value: parts[1],
})
}
return attrs
}
================================================
FILE: git/attr_test.go
================================================
package git
import (
"testing"
"github.com/matryer/is"
)
func TestParseAttr(t *testing.T) {
cases := []struct {
in string
file string
want []Attribute
}{
{
in: "org/example/MyClass.java: diff: java\n",
file: "org/example/MyClass.java",
want: []Attribute{
{
Name: "diff",
Value: "java",
},
},
},
{
in: `org/example/MyClass.java: crlf: unset
org/example/MyClass.java: diff: java
org/example/MyClass.java: myAttr: set`,
file: "org/example/MyClass.java",
want: []Attribute{
{
Name: "crlf",
Value: "unset",
},
{
Name: "diff",
Value: "java",
},
{
Name: "myAttr",
Value: "set",
},
},
},
{
in: `org/example/MyClass.java: diff: java
org/example/MyClass.java: myAttr: set`,
file: "org/example/MyClass.java",
want: []Attribute{
{
Name: "diff",
Value: "java",
},
{
Name: "myAttr",
Value: "set",
},
},
},
{
in: `README: caveat: unspecified`,
file: "README",
want: []Attribute{
{
Name: "caveat",
Value: "unspecified",
},
},
},
{
in: "",
file: "foo",
want: []Attribute{},
},
{
in: "\n",
file: "foo",
want: []Attribute{},
},
}
is := is.New(t)
for _, c := range cases {
attrs := parseAttributes(c.file, []byte(c.in))
if len(attrs) != len(c.want) {
t.Fatalf("parseAttributes(%q, %q) = %v, want %v", c.file, c.in, attrs, c.want)
}
is.Equal(attrs, c.want)
}
}
================================================
FILE: git/command.go
================================================
package git
import "github.com/aymanbagabas/git-module"
// RunInDirOptions are options for RunInDir.
type RunInDirOptions = git.RunInDirOptions
// NewCommand creates a new git command.
func NewCommand(args ...string) *git.Command {
return git.NewCommand(args...)
}
================================================
FILE: git/commit.go
================================================
package git
import (
"regexp"
"github.com/aymanbagabas/git-module"
)
// ZeroID is the zero hash.
const ZeroID = git.EmptyID
// IsZeroHash returns whether the hash is a zero hash.
func IsZeroHash(h string) bool {
pattern := regexp.MustCompile(`^0{40,}$`)
return pattern.MatchString(h)
}
// Commit is a wrapper around git.Commit with helper methods.
type Commit = git.Commit
// Commits is a list of commits.
type Commits []*Commit
// Len implements sort.Interface.
func (cl Commits) Len() int { return len(cl) }
// Swap implements sort.Interface.
func (cl Commits) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
// Less implements sort.Interface.
func (cl Commits) Less(i, j int) bool {
return cl[i].Author.When.After(cl[j].Author.When)
}
================================================
FILE: git/config.go
================================================
package git
import (
"os"
"path/filepath"
gcfg "github.com/go-git/go-git/v5/plumbing/format/config"
)
// Config returns the repository Git configuration.
func (r *Repository) Config() (*gcfg.Config, error) {
cp := filepath.Join(r.Path, "config")
f, err := os.Open(cp)
if err != nil {
return nil, err
}
defer f.Close() //nolint: errcheck
d := gcfg.NewDecoder(f)
cfg := gcfg.New()
if err := d.Decode(cfg); err != nil {
return nil, err
}
return cfg, nil
}
// SetConfig sets the repository Git configuration.
func (r *Repository) SetConfig(cfg *gcfg.Config) error {
cp := filepath.Join(r.Path, "config")
f, err := os.Create(cp)
if err != nil {
return err
}
defer f.Close() //nolint: errcheck
e := gcfg.NewEncoder(f)
return e.Encode(cfg)
}
================================================
FILE: git/errors.go
================================================
package git
import (
"errors"
"github.com/aymanbagabas/git-module"
)
var (
// ErrFileNotFound is returned when a file is not found.
ErrFileNotFound = errors.New("file not found")
// ErrDirectoryNotFound is returned when a directory is not found.
ErrDirectoryNotFound = errors.New("directory not found")
// ErrReferenceNotExist is returned when a reference does not exist.
ErrReferenceNotExist = git.ErrReferenceNotExist
// ErrRevisionNotExist is returned when a revision is not found.
ErrRevisionNotExist = git.ErrRevisionNotExist
// ErrNotAGitRepository is returned when the given path is not a Git repository.
ErrNotAGitRepository = errors.New("not a git repository")
)
================================================
FILE: git/patch.go
================================================
package git
import (
"bytes"
"fmt"
"math"
"strings"
"sync"
"github.com/aymanbagabas/git-module"
"github.com/dustin/go-humanize/english"
"github.com/sergi/go-diff/diffmatchpatch"
)
// DiffSection is a wrapper to git.DiffSection with helper methods.
type DiffSection struct {
*git.DiffSection
initOnce sync.Once
dmp *diffmatchpatch.DiffMatchPatch
}
// diffFor computes inline diff for the given line.
func (s *DiffSection) diffFor(line *git.DiffLine) string {
fallback := line.Content
// Find equivalent diff line, ignore when not found.
var diff1, diff2 string
switch line.Type {
case git.DiffLineAdd:
compareLine := s.Line(git.DiffLineDelete, line.RightLine)
if compareLine == nil {
return fallback
}
diff1 = compareLine.Content
diff2 = line.Content
case git.DiffLineDelete:
compareLine := s.Line(git.DiffLineAdd, line.LeftLine)
if compareLine == nil {
return fallback
}
diff1 = line.Content
diff2 = compareLine.Content
default:
return fallback
}
s.initOnce.Do(func() {
s.dmp = diffmatchpatch.New()
s.dmp.DiffEditCost = 100
})
diffs := s.dmp.DiffMain(diff1[1:], diff2[1:], true)
diffs = s.dmp.DiffCleanupEfficiency(diffs)
return diffsToString(diffs, line.Type)
}
func diffsToString(diffs []diffmatchpatch.Diff, lineType git.DiffLineType) string {
buf := bytes.NewBuffer(nil)
// Reproduce signs which are cutted for inline diff before.
switch lineType {
case git.DiffLineAdd:
buf.WriteByte('+')
case git.DiffLineDelete:
buf.WriteByte('-')
}
for i := range diffs {
switch {
case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == git.DiffLineAdd:
buf.WriteString(diffs[i].Text)
case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == git.DiffLineDelete:
buf.WriteString(diffs[i].Text)
case diffs[i].Type == diffmatchpatch.DiffEqual:
buf.WriteString(diffs[i].Text)
}
}
return buf.String()
}
// DiffFile is a wrapper to git.DiffFile with helper methods.
type DiffFile struct {
*git.DiffFile
Sections []*DiffSection
}
// DiffFileChange represents a file diff.
type DiffFileChange struct {
hash string
name string
mode git.EntryMode
}
// Hash returns the diff file hash.
func (f *DiffFileChange) Hash() string {
return f.hash
}
// Name returns the diff name.
func (f *DiffFileChange) Name() string {
return f.name
}
// Mode returns the diff file mode.
func (f *DiffFileChange) Mode() git.EntryMode {
return f.mode
}
// Files returns the diff files.
func (f *DiffFile) Files() (from *DiffFileChange, to *DiffFileChange) {
if f.OldIndex != ZeroID {
from = &DiffFileChange{
hash: f.OldIndex,
name: f.OldName(),
mode: f.OldMode(),
}
}
if f.Index != ZeroID {
to = &DiffFileChange{
hash: f.Index,
name: f.Name,
mode: f.Mode(),
}
}
return
}
// FileStats
type FileStats []*DiffFile
// String returns a string representation of file stats.
func (fs FileStats) String() string {
return printStats(fs)
}
func printStats(stats FileStats) string {
padLength := float64(len(" "))
newlineLength := float64(len("\n"))
separatorLength := float64(len("|"))
// Soft line length limit. The text length calculation below excludes
// length of the change number. Adding that would take it closer to 80,
// but probably not more than 80, until it's a huge number.
lineLength := 72.0
// Get the longest filename and longest total change.
var longestLength float64
var longestTotalChange float64
for _, fs := range stats {
if int(longestLength) < len(fs.Name) {
longestLength = float64(len(fs.Name))
}
totalChange := fs.NumAdditions() + fs.NumDeletions()
if int(longestTotalChange) < totalChange {
longestTotalChange = float64(totalChange)
}
}
// Parts of the output:
// <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
// example: " main.go | 10 +++++++--- "
// <pad><filename><pad>
leftTextLength := padLength + longestLength + padLength
// <pad><number><pad><+++++/-----><newline>
// Excluding number length here.
rightTextLength := padLength + padLength + newlineLength
totalTextArea := leftTextLength + separatorLength + rightTextLength
heightOfHistogram := lineLength - totalTextArea
// Scale the histogram.
var scaleFactor float64
if longestTotalChange > heightOfHistogram {
// Scale down to heightOfHistogram.
scaleFactor = longestTotalChange / heightOfHistogram
} else {
scaleFactor = 1.0
}
taddc := 0
tdelc := 0
output := strings.Builder{}
for _, fs := range stats {
taddc += fs.NumAdditions()
tdelc += fs.NumDeletions()
addn := float64(fs.NumAdditions())
deln := float64(fs.NumDeletions())
addc := int(math.Floor(addn / scaleFactor))
delc := int(math.Floor(deln / scaleFactor))
if addc < 0 {
addc = 0
}
if delc < 0 {
delc = 0
}
adds := strings.Repeat("+", addc)
dels := strings.Repeat("-", delc)
diffLines := fmt.Sprint(fs.NumAdditions() + fs.NumDeletions())
totalDiffLines := fmt.Sprint(int(longestTotalChange))
fmt.Fprintf(&output, "%s | %s %s%s\n",
fs.Name+strings.Repeat(" ", int(longestLength)-len(fs.Name)),
strings.Repeat(" ", len(totalDiffLines)-len(diffLines))+diffLines,
adds,
dels)
}
files := len(stats)
fc := fmt.Sprintf("%s changed", english.Plural(files, "file", ""))
ins := fmt.Sprintf("%s(+)", english.Plural(taddc, "insertion", ""))
dels := fmt.Sprintf("%s(-)", english.Plural(tdelc, "deletion", ""))
fmt.Fprint(&output, fc)
if taddc > 0 {
fmt.Fprintf(&output, ", %s", ins)
}
if tdelc > 0 {
fmt.Fprintf(&output, ", %s", dels)
}
fmt.Fprint(&output, "\n")
return output.String()
}
// Diff is a wrapper around git.Diff with helper methods.
type Diff struct {
*git.Diff
Files []*DiffFile
}
// FileStats returns the diff file stats.
func (d *Diff) Stats() FileStats {
return d.Files
}
const (
dstPrefix = "b/"
srcPrefix = "a/"
)
func appendPathLines(lines []string, fromPath, toPath string, isBinary bool) []string {
if isBinary {
return append(lines,
fmt.Sprintf("Binary files %s and %s differ", fromPath, toPath),
)
}
return append(lines,
fmt.Sprintf("--- %s", fromPath),
fmt.Sprintf("+++ %s", toPath),
)
}
func writeFilePatchHeader(sb *strings.Builder, filePatch *DiffFile) {
from, to := filePatch.Files()
if from == nil && to == nil {
return
}
isBinary := filePatch.IsBinary()
var lines []string
switch {
case from != nil && to != nil:
hashEquals := from.Hash() == to.Hash()
lines = append(lines,
fmt.Sprintf("diff --git %s%s %s%s",
srcPrefix, from.Name(), dstPrefix, to.Name()),
)
if from.Mode() != to.Mode() {
lines = append(lines,
fmt.Sprintf("old mode %o", from.Mode()),
fmt.Sprintf("new mode %o", to.Mode()),
)
}
if from.Name() != to.Name() {
lines = append(lines,
fmt.Sprintf("rename from %s", from.Name()),
fmt.Sprintf("rename to %s", to.Name()),
)
}
if from.Mode() != to.Mode() && !hashEquals {
lines = append(lines,
fmt.Sprintf("index %s..%s", from.Hash(), to.Hash()),
)
} else if !hashEquals {
lines = append(lines,
fmt.Sprintf("index %s..%s %o", from.Hash(), to.Hash(), from.Mode()),
)
}
if !hashEquals {
lines = appendPathLines(lines, srcPrefix+from.Name(), dstPrefix+to.Name(), isBinary)
}
case from == nil:
lines = append(lines,
fmt.Sprintf("diff --git %s %s", srcPrefix+to.Name(), dstPrefix+to.Name()),
fmt.Sprintf("new file mode %o", to.Mode()),
fmt.Sprintf("index %s..%s", ZeroID, to.Hash()),
)
lines = appendPathLines(lines, "/dev/null", dstPrefix+to.Name(), isBinary)
case to == nil:
lines = append(lines,
fmt.Sprintf("diff --git %s %s", srcPrefix+from.Name(), dstPrefix+from.Name()),
fmt.Sprintf("deleted file mode %o", from.Mode()),
fmt.Sprintf("index %s..%s", from.Hash(), ZeroID),
)
lines = appendPathLines(lines, srcPrefix+from.Name(), "/dev/null", isBinary)
}
sb.WriteString(lines[0])
for _, line := range lines[1:] {
sb.WriteByte('\n')
sb.WriteString(line)
}
sb.WriteByte('\n')
}
// Patch returns the diff as a patch.
func (d *Diff) Patch() string {
var p strings.Builder
for _, f := range d.Files {
writeFilePatchHeader(&p, f)
for _, s := range f.Sections {
for _, l := range s.Lines {
p.WriteString(s.diffFor(l))
p.WriteString("\n")
}
}
}
return p.String()
}
func toDiff(ddiff *git.Diff) *Diff {
files := make([]*DiffFile, 0, len(ddiff.Files))
for _, df := range ddiff.Files {
sections := make([]*DiffSection, 0, len(df.Sections))
for _, ds := range df.Sections {
sections = append(sections, &DiffSection{
DiffSection: ds,
})
}
files = append(files, &DiffFile{
DiffFile: df,
Sections: sections,
})
}
diff := &Diff{
Diff: ddiff,
Files: files,
}
return diff
}
================================================
FILE: git/reference.go
================================================
package git
import (
"strings"
"github.com/aymanbagabas/git-module"
)
const (
// HEAD represents the name of the HEAD reference.
HEAD = "HEAD"
// RefsHeads represents the prefix for branch references.
RefsHeads = git.RefsHeads
// RefsTags represents the prefix for tag references.
RefsTags = git.RefsTags
)
// Reference is a wrapper around git.Reference with helper methods.
type Reference struct {
*git.Reference
path string // repo path
}
// ReferenceName is a Refspec wrapper.
type ReferenceName string
// String returns the reference name i.e. refs/heads/master.
func (r ReferenceName) String() string {
return string(r)
}
// Short returns the short name of the reference i.e. master.
func (r ReferenceName) Short() string {
return git.RefShortName(string(r))
}
// Name returns the reference name i.e. refs/heads/master.
func (r *Reference) Name() ReferenceName {
return ReferenceName(r.Refspec)
}
// IsBranch returns true if the reference is a branch.
func (r *Reference) IsBranch() bool {
return strings.HasPrefix(r.Refspec, git.RefsHeads)
}
// IsTag returns true if the reference is a tag.
func (r *Reference) IsTag() bool {
return strings.HasPrefix(r.Refspec, git.RefsTags)
}
================================================
FILE: git/repo.go
================================================
package git
import (
"path/filepath"
"strings"
"github.com/aymanbagabas/git-module"
)
var (
// DiffMaxFile is the maximum number of files to show in a diff.
DiffMaxFiles = 1000
// DiffMaxFileLines is the maximum number of lines to show in a file diff.
DiffMaxFileLines = 1000
// DiffMaxLineChars is the maximum number of characters to show in a line diff.
DiffMaxLineChars = 1000
)
// Repository is a wrapper around git.Repository with helper methods.
type Repository struct {
*git.Repository
Path string
IsBare bool
}
// Clone clones a repository.
func Clone(src, dst string, opts ...git.CloneOptions) error {
return git.Clone(src, dst, opts...)
}
// Init initializes and opens a new git repository.
func Init(path string, bare bool) (*Repository, error) {
if bare {
path = strings.TrimSuffix(path, ".git") + ".git"
}
err := git.Init(path, git.InitOptions{Bare: bare})
if err != nil {
return nil, err
}
return Open(path)
}
func gitDir(r *git.Repository) (string, error) {
return r.RevParse("--git-dir")
}
// Open opens a git repository at the given path.
func Open(path string) (*Repository, error) {
repo, err := git.Open(path)
if err != nil {
return nil, err
}
gp, err := gitDir(repo)
if err != nil || (gp != "." && gp != ".git") {
return nil, ErrNotAGitRepository
}
return &Repository{
Repository: repo,
Path: path,
IsBare: gp == ".",
}, nil
}
// HEAD returns the HEAD reference for a repository.
func (r *Repository) HEAD() (*Reference, error) {
rn, err := r.Repository.SymbolicRef(git.SymbolicRefOptions{Name: "HEAD"})
if err != nil {
return nil, err
}
hash, err := r.ShowRefVerify(rn)
if err != nil {
return nil, err
}
return &Reference{
Reference: &git.Reference{
ID: hash,
Refspec: rn,
},
path: r.Path,
}, nil
}
// References returns the references for a repository.
func (r *Repository) References() ([]*Reference, error) {
refs, err := r.ShowRef()
if err != nil {
return nil, err
}
rrefs := make([]*Reference, 0, len(refs))
for _, ref := range refs {
rrefs = append(rrefs, &Reference{
Reference: ref,
path: r.Path,
})
}
return rrefs, nil
}
// LsTree returns the tree for the given reference.
func (r *Repository) LsTree(ref string) (*Tree, error) {
tree, err := r.Repository.LsTree(ref)
if err != nil {
return nil, err
}
return &Tree{
Tree: tree,
Path: "",
Repository: r,
}, nil
}
// Tree returns the tree for the given reference.
func (r *Repository) Tree(ref *Reference) (*Tree, error) {
if ref == nil {
rref, err := r.HEAD()
if err != nil {
return nil, err
}
ref = rref
}
return r.LsTree(ref.ID)
}
// TreePath returns the tree for the given path.
func (r *Repository) TreePath(ref *Reference, path string) (*Tree, error) {
path = filepath.Clean(path)
if path == "." {
path = ""
}
if path == "" {
return r.Tree(ref)
}
t, err := r.Tree(ref)
if err != nil {
return nil, err
}
return t.SubTree(path)
}
// Diff returns the diff for the given commit.
func (r *Repository) Diff(commit *Commit) (*Diff, error) {
diff, err := r.Repository.Diff(commit.ID.String(), DiffMaxFiles, DiffMaxFileLines, DiffMaxLineChars, git.DiffOptions{
CommandOptions: git.CommandOptions{
Envs: []string{"GIT_CONFIG_GLOBAL=/dev/null"},
},
})
if err != nil {
return nil, err
}
return toDiff(diff), nil
}
// Patch returns the patch for the given reference.
func (r *Repository) Patch(commit *Commit) (string, error) {
diff, err := r.Diff(commit)
if err != nil {
return "", err
}
return diff.Patch(), err
}
// CountCommits returns the number of commits in the repository.
func (r *Repository) CountCommits(ref *Reference) (int64, error) {
return r.RevListCount([]string{ref.Name().String()})
}
// CommitsByPage returns the commits for a given page and size.
func (r *Repository) CommitsByPage(ref *Reference, page, size int) (Commits, error) {
cs, err := r.Repository.CommitsByPage(ref.Name().String(), page, size)
if err != nil {
return nil, err
}
commits := make(Commits, len(cs))
copy(commits, cs)
return commits, nil
}
// SymbolicRef returns or updates the symbolic reference for the given name.
// Both name and ref can be empty.
func (r *Repository) SymbolicRef(name string, ref string, opts ...git.SymbolicRefOptions) (string, error) {
var opt git.SymbolicRefOptions
if len(opts) > 0 {
opt = opts[0]
}
opt.Name = name
opt.Ref = ref
return r.Repository.SymbolicRef(opt)
}
================================================
FILE: git/server.go
================================================
package git
import (
"context"
"github.com/aymanbagabas/git-module"
)
// UpdateServerInfo updates the server info file for the given repo path.
func UpdateServerInfo(ctx context.Context, path string) error {
if !isGitDir(path) {
return ErrNotAGitRepository
}
cmd := git.NewCommand("update-server-info").WithContext(ctx).WithTimeout(-1)
_, err := cmd.RunInDir(path)
return err
}
================================================
FILE: git/stash.go
================================================
package git
import "github.com/aymanbagabas/git-module"
// StashDiff returns the diff of the given stash index.
func (r *Repository) StashDiff(index int) (*Diff, error) {
diff, err := r.Repository.StashDiff(index, DiffMaxFiles, DiffMaxFileLines, DiffMaxLineChars, git.DiffOptions{
CommandOptions: git.CommandOptions{
Envs: []string{"GIT_CONFIG_GLOBAL=/dev/null"},
},
})
if err != nil {
return nil, err
}
return toDiff(diff), nil
}
================================================
FILE: git/tag.go
================================================
package git
import "github.com/aymanbagabas/git-module"
// Tag is a git tag.
type Tag = git.Tag
================================================
FILE: git/tree.go
================================================
package git
import (
"bufio"
"bytes"
"io"
"io/fs"
"path/filepath"
"sort"
"github.com/aymanbagabas/git-module"
)
// Tree is a wrapper around git.Tree with helper methods.
type Tree struct {
*git.Tree
Path string
Repository *Repository
}
// TreeEntry is a wrapper around git.TreeEntry with helper methods.
type TreeEntry struct {
*git.TreeEntry
// path is the full path of the file
path string
}
// Entries is a wrapper around git.Entries.
type Entries []*TreeEntry
var sorters = []func(t1, t2 *TreeEntry) bool{
func(t1, t2 *TreeEntry) bool {
return (t1.IsTree() || t1.IsCommit()) && !t2.IsTree() && !t2.IsCommit()
},
func(t1, t2 *TreeEntry) bool {
return t1.Name() < t2.Name()
},
}
// Len implements sort.Interface.
func (es Entries) Len() int { return len(es) }
// Swap implements sort.Interface.
func (es Entries) Swap(i, j int) { es[i], es[j] = es[j], es[i] }
// Less implements sort.Interface.
func (es Entries) Less(i, j int) bool {
t1, t2 := es[i], es[j]
var k int
for k = 0; k < len(sorters)-1; k++ {
sorter := sorters[k]
switch {
case sorter(t1, t2):
return true
case sorter(t2, t1):
return false
}
}
return sorters[k](t1, t2)
}
// Sort sorts the entries in the tree.
func (es Entries) Sort() {
sort.Sort(es)
}
// File is a wrapper around git.Blob with helper methods.
type File struct {
*git.Blob
Entry *TreeEntry
}
// Name returns the name of the file.
func (f *File) Name() string {
return f.Entry.Name()
}
// Path returns the full path of the file.
func (f *File) Path() string {
return f.Entry.path
}
// SubTree returns the sub-tree at the given path.
func (t *Tree) SubTree(path string) (*Tree, error) {
tree, err := t.Subtree(path)
if err != nil {
return nil, err
}
return &Tree{
Tree: tree,
Path: path,
Repository: t.Repository,
}, nil
}
// Entries returns the entries in the tree.
func (t *Tree) Entries() (Entries, error) {
entries, err := t.Tree.Entries()
if err != nil {
return nil, err
}
ret := make(Entries, len(entries))
for i, e := range entries {
ret[i] = &TreeEntry{
TreeEntry: e,
path: filepath.Join(t.Path, e.Name()),
}
}
return ret, nil
}
// TreeEntry returns the TreeEntry for the file path.
func (t *Tree) TreeEntry(path string) (*TreeEntry, error) {
entry, err := t.Tree.TreeEntry(path)
if err != nil {
return nil, err
}
return &TreeEntry{
TreeEntry: entry,
path: filepath.Join(t.Path, entry.Name()),
}, nil
}
const sniffLen = 8000
// IsBinary detects if data is a binary value based on:
// http://git.kernel.org/cgit/git/git.git/tree/xdiff-interface.c?id=HEAD#n198
func IsBinary(r io.Reader) (bool, error) {
reader := bufio.NewReader(r)
c := 0
for c < sniffLen {
b, err := reader.ReadByte()
if err == io.EOF {
break
}
if err != nil {
return false, err
}
if b == byte(0) {
return true, nil
}
c++
}
return false, nil
}
// IsBinary returns true if the file is binary.
func (f *File) IsBinary() (bool, error) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
err := f.Pipeline(stdout, stderr)
if err != nil {
return false, err
}
r := bufio.NewReader(stdout)
return IsBinary(r)
}
// Mode returns the mode of the file in fs.FileMode format.
func (e *TreeEntry) Mode() fs.FileMode {
m := e.Blob().Mode()
switch m {
case git.EntryTree:
return fs.ModeDir | fs.ModePerm
default:
return fs.FileMode(m) //nolint:gosec
}
}
// File returns the file for the TreeEntry.
func (e *TreeEntry) File() *File {
b := e.Blob()
return &File{
Blob: b,
Entry: e,
}
}
// Contents returns the contents of the file.
func (e *TreeEntry) Contents() ([]byte, error) {
return e.File().Contents()
}
// Contents returns the contents of the file.
func (f *File) Contents() ([]byte, error) {
return f.Blob.Bytes()
}
================================================
FILE: git/types.go
================================================
package git
import "github.com/aymanbagabas/git-module"
// CommandOptions contain options for running a git command.
type CommandOptions = git.CommandOptions
// CloneOptions contain options for cloning a repository.
type CloneOptions = git.CloneOptions
================================================
FILE: git/utils.go
================================================
package git
import (
"os"
"path/filepath"
"github.com/gobwas/glob"
)
// LatestFile returns the contents of the first file at the specified path pattern in the repository and its file path.
func LatestFile(repo *Repository, ref *Reference, pattern string) (string, string, error) {
g := glob.MustCompile(pattern)
dir := filepath.Dir(pattern)
if ref == nil {
head, err := repo.HEAD()
if err != nil {
return "", "", err
}
ref = head
}
t, err := repo.TreePath(ref, dir)
if err != nil {
return "", "", err
}
ents, err := t.Entries()
if err != nil {
return "", "", err
}
for _, e := range ents {
te := e
fp := filepath.Join(dir, te.Name())
if te.IsTree() {
continue
}
if g.Match(fp) {
if te.IsSymlink() {
bts, err := te.Contents()
if err != nil {
return "", "", err
}
fp = string(bts)
te, err = t.TreeEntry(fp)
if err != nil {
return "", "", err
}
}
bts, err := te.Contents()
if err != nil {
return "", "", err
}
return string(bts), fp, nil
}
}
return "", "", ErrFileNotFound
}
// Returns true if path is a directory containing an `objects` directory and a
// `HEAD` file.
func isGitDir(path string) bool {
stat, err := os.Stat(filepath.Join(path, "objects"))
if err != nil {
return false
}
if !stat.IsDir() {
return false
}
stat, err = os.Stat(filepath.Join(path, "HEAD"))
if err != nil {
return false
}
if stat.IsDir() {
return false
}
return true
}
================================================
FILE: go.mod
================================================
module github.com/charmbracelet/soft-serve
go 1.25.8
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.2
charm.land/glamour/v2 v2.0.0
charm.land/lipgloss/v2 v2.0.2
charm.land/log/v2 v2.0.0
charm.land/wish/v2 v2.0.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/aymanbagabas/git-module v1.8.4-0.20250826192401-1f81c5471e53
github.com/caarlos0/duration v0.0.0-20240108180406-5d492514f3c7
github.com/caarlos0/env/v11 v11.4.0
github.com/charmbracelet/colorprofile v0.4.3
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20240708204110-bacbfdb68d92
github.com/charmbracelet/keygen v0.5.4
github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309
github.com/charmbracelet/x/ansi v0.11.6
github.com/dustin/go-humanize v1.0.1
github.com/go-git/go-git/v5 v5.17.0
github.com/go-jose/go-jose/v3 v3.0.4
github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/go-querystring v1.2.0
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.11.2
github.com/lrstanley/bubblezone/v2 v2.0.0
github.com/matryer/is v1.4.1
github.com/muesli/mango-cobra v1.3.0
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/prometheus/client_golang v1.23.2
github.com/robfig/cron/v3 v3.0.1
github.com/rogpeppe/go-internal v1.14.1
github.com/sergi/go-diff v1.4.0
github.com/spf13/cobra v1.10.2
go.uber.org/automaxprocs v1.6.0
golang.org/x/crypto v0.49.0
golang.org/x/sync v0.20.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.46.1
)
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73 // indirect
github.com/charmbracelet/x/conpty v0.1.1 // indirect
github.com/charmbracelet/x/errors v0.0.0-20251110184232-6ab307057ac7 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // 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/creack/pty v1.1.24 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
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/sahilm/fuzzy v0.1.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
================================================
FILE: go.sum
================================================
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U=
charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w=
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/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=
charm.land/wish/v2 v2.0.0 h1:0vryoDz6G1SdJNIWSkExy88dLAs7H/w0x9y/cay1vno=
charm.land/wish/v2 v2.0.0/go.mod h1:B42DmuVdvQxz215H9aCsbrXVSuAInAqkHAnmwg0nKs8=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/git-module v1.8.4-0.20250826192401-1f81c5471e53 h1:KfKp+gVsQtuM9qb8Putvkx1jjAWqlvI1vdv5x9hdFoQ=
github.com/aymanbagabas/git-module v1.8.4-0.20250826192401-1f81c5471e53/go.mod h1:d4gQ7/3/S2sPq4NnKdtAgUOVr6XtLpWFtxyVV5/+76U=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caarlos0/duration v0.0.0-20240108180406-5d492514f3c7 h1:kJP/C2eL9DCKrCOlX6lPVmAUAb6U4u9xllgws1kP9ds=
github.com/caarlos0/duration v0.0.0-20240108180406-5d492514f3c7/go.mod h1:mSkwb/eZEwOJJJ4tqAKiuhLIPe0e9+FKhlU0oMCpbf8=
github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc=
github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
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.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20240708204110-bacbfdb68d92 h1:KtQlsiHfY3K4AoIEh0yUE/wCLHteZ9EzV1hKmx+p7U8=
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20240708204110-bacbfdb68d92/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
github.com/charmbracelet/keygen v0.5.4 h1:XQYgf6UEaTGgQSSmiPpIQ78WfseNQp4Pz8N/c1OsrdA=
github.com/charmbracelet/keygen v0.5.4/go.mod h1:t4oBRr41bvK7FaJsAaAQhhkUuHslzFXVjOBwA55CZNM=
github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309 h1:dCVbCRRtg9+tsfiTXTp0WupDlHruAXyp+YoxGVofHHc=
github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309/go.mod h1:R9cISUs5kAH4Cq/rguNbSwcR+slE5Dfm8FEs//uoIGE=
github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73 h1:Af/L28Xh+pddhouT/6lJ7IAIYfu5tWJOB0iqt+mXsYM=
github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73/go.mod h1:E6/0abq9uG2SnM8IbLB9Y5SW09uIgfaFETk8aRzgXUQ=
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/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/errors v0.0.0-20251110184232-6ab307057ac7 h1:4EG8pCHK5fa8dIxv97VHC8hdkJAz6QNm1WB9BuD/WhY=
github.com/charmbracelet/x/errors v0.0.0-20251110184232-6ab307057ac7/go.mod h1:O2BTD/aMVQDmrvqroIO3fB6zXUuU07ZpVt21QTmZjRg=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
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/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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0=
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
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-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lrstanley/bubblezone/v2 v2.0.0 h1:pMb9fHKs0slJF6OrzQ2hEgWusqyl9VU/S0UZ5hyh7ZA=
github.com/lrstanley/bubblezone/v2 v2.0.0/go.mod h1:yV/QTjcm4Zu5cqvGvdHi7xVUfnB36w/SafOuDp57dgY=
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/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 h1:YocNLcTBdEdvY3iDK6jfWXvEaM5OCKkjxPKoJRdB3Gg=
github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-cobra v1.3.0 h1:vQy5GvPg3ndOSpduxutqFoINhWk3vD5K2dXo5E8pqec=
github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NCLQDwwGx19E=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
================================================
FILE: pkg/access/access.go
================================================
package access
import (
"encoding"
"errors"
)
// AccessLevel is the level of access allowed to a repo.
type AccessLevel int //nolint: revive
const (
// NoAccess does not allow access to the repo.
NoAccess AccessLevel = iota
// ReadOnlyAccess allows read-only access to the repo.
ReadOnlyAccess
// ReadWriteAccess allows read and write access to the repo.
ReadWriteAccess
// AdminAccess allows read, write, and admin access to the repo.
AdminAccess
)
// String returns the string representation of the access level.
func (a AccessLevel) String() string {
switch a {
case NoAccess:
return "no-access"
case ReadOnlyAccess:
return "read-only"
case ReadWriteAccess:
return "read-write"
case AdminAccess:
return "admin-access"
default:
return "unknown"
}
}
// ParseAccessLevel parses an access level string.
func ParseAccessLevel(s string) AccessLevel {
switch s {
case "no-access":
return NoAccess
case "read-only":
return ReadOnlyAccess
case "read-write":
return ReadWriteAccess
case "admin-access":
return AdminAccess
default:
return AccessLevel(-1)
}
}
var (
_ encoding.TextMarshaler = AccessLevel(0)
_ encoding.TextUnmarshaler = (*AccessLevel)(nil)
)
// ErrInvalidAccessLevel is returned when an invalid access level is provided.
var ErrInvalidAccessLevel = errors.New("invalid access level")
// UnmarshalText implements encoding.TextUnmarshaler.
func (a *AccessLevel) UnmarshalText(text []byte) error {
l := ParseAccessLevel(string(text))
if l < 0 {
return ErrInvalidAccessLevel
}
*a = l
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (a AccessLevel) MarshalText() (text []byte, err error) {
return []byte(a.String()), nil
}
================================================
FILE: pkg/access/access_test.go
================================================
package access
import "testing"
func TestParseAccessLevel(t *testing.T) {
cases := []struct {
in string
out AccessLevel
}{
{"", -1},
{"foo", -1},
{AdminAccess.String(), AdminAccess},
{ReadOnlyAccess.String(), ReadOnlyAccess},
{ReadWriteAccess.String(), ReadWriteAccess},
{NoAccess.String(), NoAccess},
}
for _, c := range cases {
out := ParseAccessLevel(c.in)
if out != c.out {
t.Errorf("ParseAccessLevel(%q) => %d, want %d", c.in, out, c.out)
}
}
}
================================================
FILE: pkg/access/context.go
================================================
package access
import "context"
// ContextKey is the context key for the access level.
var ContextKey = &struct{ string }{"access"}
// FromContext returns the access level from the context.
func FromContext(ctx context.Context) AccessLevel {
if ac, ok := ctx.Value(ContextKey).(AccessLevel); ok {
return ac
}
return -1
}
// WithContext returns a new context with the access level.
func WithContext(ctx context.Context, ac AccessLevel) context.Context {
return context.WithValue(ctx, ContextKey, ac)
}
================================================
FILE: pkg/access/context_test.go
================================================
package access
import (
"context"
"testing"
)
func TestGoodFromContext(t *testing.T) {
ctx := WithContext(context.TODO(), AdminAccess)
if ac := FromContext(ctx); ac != AdminAccess {
t.Errorf("FromContext(ctx) => %d, want %d", ac, AdminAccess)
}
}
func TestBadFromContext(t *testing.T) {
ctx := context.TODO()
if ac := FromContext(ctx); ac != -1 {
t.Errorf("FromContext(ctx) => %d, want %d", ac, -1)
}
}
================================================
FILE: pkg/backend/access_token.go
================================================
package backend
import (
"context"
"errors"
"time"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/proto"
"github.com/charmbracelet/soft-serve/pkg/utils"
)
// CreateAccessToken creates an access token for user.
func (b *Backend) CreateAccessToken(ctx context.Context, user proto.User, name string, expiresAt time.Time) (string, error) {
token := GenerateToken()
tokenHash := HashToken(token)
name = utils.Sanitize(name)
if err := b.db.TransactionContext(ctx, func(tx *db.Tx) error {
_, err := b.store.CreateAccessToken(ctx, tx, name, user.ID(), tokenHash, expiresAt)
if err != nil {
return db.WrapError(err)
}
return nil
}); err != nil {
return "", err
}
return token, nil
}
// DeleteAccessToken deletes an access token for a user.
func (b *Backend) DeleteAccessToken(ctx context.Context, user proto.User, id int64) error {
err := b.db.TransactionContext(ctx, func(tx *db.Tx) error {
_, err := b.store.GetAccessToken(ctx, tx, id)
if err != nil {
return db.WrapError(err)
}
if err := b.store.DeleteAccessTokenForUser(ctx, tx, user.ID(), id); err != nil {
return db.WrapError(err)
}
return nil
})
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
return proto.ErrTokenNotFound
}
return err
}
return nil
}
// ListAccessTokens lists access tokens for a user.
func (b *Backend) ListAccessTokens(ctx context.Context, user proto.User) ([]proto.AccessToken, error) {
accessTokens, err := b.store.GetAccessTokensByUserID(ctx, b.db, user.ID())
if err != nil {
return nil, db.WrapError(err)
}
var tokens []proto.AccessToken
for _, t := range accessTokens {
token := proto.AccessToken{
ID: t.ID,
Name: t.Name,
TokenHash: t.Token,
UserID: t.UserID,
CreatedAt: t.CreatedAt,
}
if t.ExpiresAt.Valid {
token.ExpiresAt = t.ExpiresAt.Time
}
tokens = append(tokens, token)
}
return tokens, nil
}
================================================
FILE: pkg/backend/auth.go
================================================
package backend
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"charm.land/log/v2"
"golang.org/x/crypto/bcrypt"
)
const saltySalt = "salty-soft-serve"
// HashPassword hashes the password using bcrypt.
func HashPassword(password string) (string, error) {
crypt, err := bcrypt.GenerateFromPassword([]byte(password+saltySalt), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(crypt), nil
}
// VerifyPassword verifies the password against the hash.
func VerifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+saltySalt))
return err == nil
}
// GenerateToken returns a random unique token.
func GenerateToken() string {
buf := make([]byte, 20)
if _, err := rand.Read(buf); err != nil {
log.Error("unable to generate access token")
return ""
}
return "ss_" + hex.EncodeToString(buf)
}
// HashToken hashes the token using sha256.
func HashToken(token string) string {
sum := sha256.Sum256([]byte(token + saltySalt))
return hex.EncodeToString(sum[:])
}
================================================
FILE: pkg/backend/auth_test.go
================================================
package backend
import "testing"
func TestHashPassword(t *testing.T) {
hash, err := HashPassword("password")
if err != nil {
t.Fatal(err)
}
if hash == "" {
t.Fatal("hash is empty")
}
}
func TestVerifyPassword(t *testing.T) {
hash, err := HashPassword("password")
if err != nil {
t.Fatal(err)
}
if !VerifyPassword("password", hash) {
t.Fatal("password did not verify")
}
}
func TestGenerateToken(t *testing.T) {
token := GenerateToken()
if token == "" {
t.Fatal("token is empty")
}
}
func TestHashToken(t *testing.T) {
token := GenerateToken()
hash := HashToken(token)
if hash == "" {
t.Fatal("hash is empty")
}
}
================================================
FILE: pkg/backend/backend.go
================================================
package backend
import (
"context"
"charm.land/log/v2"
"github.com/charmbracelet/soft-serve/pkg/config"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/store"
"github.com/charmbracelet/soft-serve/pkg/task"
)
// Backend is the Soft Serve backend that handles users, repositories, and
// server settings management and operations.
type Backend struct {
ctx context.Context
cfg *config.Config
db *db.DB
store store.Store
logger *log.Logger
cache *cache
manager *task.Manager
}
// New returns a new Soft Serve backend.
func New(ctx context.Context, cfg *config.Config, db *db.DB, st store.Store) *Backend {
logger := log.FromContext(ctx).WithPrefix("backend")
b := &Backend{
ctx: ctx,
cfg: cfg,
db: db,
store: st,
logger: logger,
manager: task.NewManager(ctx),
}
// TODO: implement a proper caching interface
cache := newCache(b, 1000)
b.cache = cache
return b
}
================================================
FILE: pkg/backend/cache.go
================================================
package backend
import lru "github.com/hashicorp/golang-lru/v2"
// TODO: implement a caching interface.
type cache struct {
b *Backend
repos *lru.Cache[string, *repo]
}
func newCache(b *Backend, size int) *cache {
if size <= 0 {
size = 1
}
c := &cache{b: b}
cache, _ := lru.New[string, *repo](size)
c.repos = cache
return c
}
func (c *cache) Get(repo string) (*repo, bool) {
return c.repos.Get(repo)
}
func (c *cache) Set(repo string, r *repo) {
c.repos.Add(repo, r)
}
func (c *cache) Delete(repo string) {
c.repos.Remove(repo)
}
func (c *cache) Len() int {
return c.repos.Len()
}
================================================
FILE: pkg/backend/collab.go
================================================
package backend
import (
"context"
"errors"
"strings"
"github.com/charmbracelet/soft-serve/pkg/access"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/db/models"
"github.com/charmbracelet/soft-serve/pkg/proto"
"github.com/charmbracelet/soft-serve/pkg/utils"
"github.com/charmbracelet/soft-serve/pkg/webhook"
)
// AddCollaborator adds a collaborator to a repository.
//
// It implements backend.Backend.
func (d *Backend) AddCollaborator(ctx context.Context, repo string, username string, level access.AccessLevel) error {
username = strings.ToLower(username)
if err := utils.ValidateUsername(username); err != nil {
return err
}
repo = utils.SanitizeRepo(repo)
r, err := d.Repository(ctx, repo)
if err != nil {
return err
}
if err := db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.AddCollabByUsernameAndRepo(ctx, tx, username, repo, level)
}),
); err != nil {
if errors.Is(err, db.ErrDuplicateKey) {
return proto.ErrCollaboratorExist
}
return err
}
wh, err := webhook.NewCollaboratorEvent(ctx, proto.UserFromContext(ctx), r, username, webhook.CollaboratorEventAdded)
if err != nil {
return err
}
return webhook.SendEvent(ctx, wh)
}
// Collaborators returns a list of collaborators for a repository.
//
// It implements backend.Backend.
func (d *Backend) Collaborators(ctx context.Context, repo string) ([]string, error) {
repo = utils.SanitizeRepo(repo)
var users []models.User
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
users, err = d.store.ListCollabsByRepoAsUsers(ctx, tx, repo)
return err
}); err != nil {
return nil, db.WrapError(err)
}
var usernames []string
for _, u := range users {
usernames = append(usernames, u.Username)
}
return usernames, nil
}
// IsCollaborator returns the access level and true if the user is a collaborator of the repository.
//
// It implements backend.Backend.
func (d *Backend) IsCollaborator(ctx context.Context, repo string, username string) (access.AccessLevel, bool, error) {
if username == "" {
return -1, false, nil
}
repo = utils.SanitizeRepo(repo)
var m models.Collab
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
m, err = d.store.GetCollabByUsernameAndRepo(ctx, tx, username, repo)
return err
}); err != nil {
return -1, false, db.WrapError(err)
}
return m.AccessLevel, m.ID > 0, nil
}
// RemoveCollaborator removes a collaborator from a repository.
//
// It implements backend.Backend.
func (d *Backend) RemoveCollaborator(ctx context.Context, repo string, username string) error {
repo = utils.SanitizeRepo(repo)
r, err := d.Repository(ctx, repo)
if err != nil {
return err
}
wh, err := webhook.NewCollaboratorEvent(ctx, proto.UserFromContext(ctx), r, username, webhook.CollaboratorEventRemoved)
if err != nil {
return err
}
if err := db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.RemoveCollabByUsernameAndRepo(ctx, tx, username, repo)
}),
); err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
return proto.ErrCollaboratorNotFound
}
return err
}
return webhook.SendEvent(ctx, wh)
}
================================================
FILE: pkg/backend/context.go
================================================
package backend
import "context"
// ContextKey is the key for the backend in the context.
var ContextKey = &struct{ string }{"backend"}
// FromContext returns the backend from a context.
func FromContext(ctx context.Context) *Backend {
if b, ok := ctx.Value(ContextKey).(*Backend); ok {
return b
}
return nil
}
// WithContext returns a new context with the backend attached.
func WithContext(ctx context.Context, b *Backend) context.Context {
return context.WithValue(ctx, ContextKey, b)
}
================================================
FILE: pkg/backend/hooks.go
================================================
package backend
import (
"context"
"io"
"os"
"sync"
"github.com/charmbracelet/soft-serve/git"
"github.com/charmbracelet/soft-serve/pkg/hooks"
"github.com/charmbracelet/soft-serve/pkg/proto"
"github.com/charmbracelet/soft-serve/pkg/sshutils"
"github.com/charmbracelet/soft-serve/pkg/webhook"
)
var _ hooks.Hooks = (*Backend)(nil)
// PostReceive is called by the git post-receive hook.
//
// It implements Hooks.
func (d *Backend) PostReceive(_ context.Context, _ io.Writer, _ io.Writer, repo string, args []hooks.HookArg) {
d.logger.Debug("post-receive hook called", "repo", repo, "args", args)
}
// PreReceive is called by the git pre-receive hook.
//
// It implements Hooks.
func (d *Backend) PreReceive(_ context.Context, _ io.Writer, _ io.Writer, repo string, args []hooks.HookArg) {
d.logger.Debug("pre-receive hook called", "repo", repo, "args", args)
}
// Update is called by the git update hook.
//
// It implements Hooks.
func (d *Backend) Update(ctx context.Context, _ io.Writer, _ io.Writer, repo string, arg hooks.HookArg) {
d.logger.Debug("update hook called", "repo", repo, "arg", arg)
// Find user
var user proto.User
if pubkey := os.Getenv("SOFT_SERVE_PUBLIC_KEY"); pubkey != "" {
pk, _, err := sshutils.ParseAuthorizedKey(pubkey)
if err != nil {
d.logger.Error("error parsing public key", "err", err)
return
}
user, err = d.UserByPublicKey(ctx, pk)
if err != nil {
d.logger.Error("error finding user from public key", "key", pubkey, "err", err)
return
}
} else if username := os.Getenv("SOFT_SERVE_USERNAME"); username != "" {
var err error
user, err = d.User(ctx, username)
if err != nil {
d.logger.Error("error finding user from username", "username", username, "err", err)
return
}
} else {
d.logger.Error("error finding user")
return
}
// Get repo
r, err := d.Repository(ctx, repo)
if err != nil {
d.logger.Error("error finding repository", "repo", repo, "err", err)
return
}
// TODO: run this async
// This would probably need something like an RPC server to communicate with the hook process.
if git.IsZeroHash(arg.OldSha) || git.IsZeroHash(arg.NewSha) {
wh, err := webhook.NewBranchTagEvent(ctx, user, r, arg.RefName, arg.OldSha, arg.NewSha)
if err != nil {
d.logger.Error("error creating branch_tag webhook", "err", err)
} else if err := webhook.SendEvent(ctx, wh); err != nil {
d.logger.Error("error sending branch_tag webhook", "err", err)
}
}
wh, err := webhook.NewPushEvent(ctx, user, r, arg.RefName, arg.OldSha, arg.NewSha)
if err != nil {
d.logger.Error("error creating push webhook", "err", err)
} else if err := webhook.SendEvent(ctx, wh); err != nil {
d.logger.Error("error sending push webhook", "err", err)
}
}
// PostUpdate is called by the git post-update hook.
//
// It implements Hooks.
func (d *Backend) PostUpdate(ctx context.Context, _ io.Writer, _ io.Writer, repo string, args ...string) {
d.logger.Debug("post-update hook called", "repo", repo, "args", args)
var wg sync.WaitGroup
// Populate last-modified file.
wg.Add(1)
go func() {
defer wg.Done()
if err := populateLastModified(ctx, d, repo); err != nil {
d.logger.Error("error populating last-modified", "repo", repo, "err", err)
return
}
}()
wg.Wait()
}
func populateLastModified(ctx context.Context, d *Backend, name string) error {
var rr *repo
_rr, err := d.Repository(ctx, name)
if err != nil {
return err
}
if r, ok := _rr.(*repo); ok {
rr = r
} else {
return proto.ErrRepoNotFound
}
r, err := rr.Open()
if err != nil {
return err
}
c, err := r.LatestCommitTime()
if err != nil {
return err
}
return rr.writeLastModified(c)
}
================================================
FILE: pkg/backend/lfs.go
================================================
package backend
import (
"context"
"errors"
"io"
"path"
"path/filepath"
"strconv"
"github.com/charmbracelet/soft-serve/pkg/config"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/lfs"
"github.com/charmbracelet/soft-serve/pkg/proto"
"github.com/charmbracelet/soft-serve/pkg/storage"
"github.com/charmbracelet/soft-serve/pkg/store"
)
// StoreRepoMissingLFSObjects stores missing LFS objects for a repository.
func StoreRepoMissingLFSObjects(ctx context.Context, repo proto.Repository, dbx *db.DB, store store.Store, lfsClient lfs.Client) error {
cfg := config.FromContext(ctx)
repoID := strconv.FormatInt(repo.ID(), 10)
lfsRoot := filepath.Join(cfg.DataPath, "lfs", repoID)
// TODO: support S3 storage
strg := storage.NewLocalStorage(lfsRoot)
pointerChan := make(chan lfs.PointerBlob)
errChan := make(chan error, 1)
r, err := repo.Open()
if err != nil {
return err
}
go lfs.SearchPointerBlobs(ctx, r, pointerChan, errChan)
download := func(pointers []lfs.Pointer) error {
return lfsClient.Download(ctx, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error {
if objectError != nil {
return objectError
}
defer content.Close() //nolint: errcheck
return dbx.TransactionContext(ctx, func(tx *db.Tx) error {
if err := store.CreateLFSObject(ctx, tx, repo.ID(), p.Oid, p.Size); err != nil {
return db.WrapError(err)
}
_, err := strg.Put(path.Join("objects", p.RelativePath()), content)
return err
})
})
}
var batch []lfs.Pointer
for pointer := range pointerChan {
obj, err := store.GetLFSObjectByOid(ctx, dbx, repo.ID(), pointer.Oid)
if err != nil && !errors.Is(err, db.ErrRecordNotFound) {
return db.WrapError(err)
}
exist, err := strg.Exists(path.Join("objects", pointer.RelativePath()))
if err != nil {
return err
}
if exist && obj.ID == 0 {
if err := store.CreateLFSObject(ctx, dbx, repo.ID(), pointer.Oid, pointer.Size); err != nil {
return db.WrapError(err)
}
} else {
batch = append(batch, pointer.Pointer)
// Limit batch requests to 20 objects
if len(batch) >= 20 {
if err := download(batch); err != nil {
return err
}
batch = nil
}
}
}
if err, ok := <-errChan; ok {
return err
}
return nil
}
================================================
FILE: pkg/backend/repo.go
================================================
package backend
import (
"bufio"
"context"
"errors"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/charmbracelet/soft-serve/git"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/db/models"
"github.com/charmbracelet/soft-serve/pkg/hooks"
"github.com/charmbracelet/soft-serve/pkg/lfs"
"github.com/charmbracelet/soft-serve/pkg/proto"
"github.com/charmbracelet/soft-serve/pkg/storage"
"github.com/charmbracelet/soft-serve/pkg/task"
"github.com/charmbracelet/soft-serve/pkg/utils"
"github.com/charmbracelet/soft-serve/pkg/webhook"
)
func validateImportRemote(remote string) error {
endpoint, err := lfs.NewEndpoint(remote)
if err != nil || endpoint.Host == "" {
return proto.ErrInvalidRemote
}
return nil
}
// CreateRepository creates a new repository.
//
// It implements backend.Backend.
func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.User, opts proto.RepositoryOptions) (proto.Repository, error) {
name = utils.SanitizeRepo(name)
if err := utils.ValidateRepo(name); err != nil {
return nil, err
}
rp := filepath.Join(d.repoPath(name))
var userID int64
if user != nil {
userID = user.ID()
}
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
if err := d.store.CreateRepo(
ctx,
tx,
name,
userID,
opts.ProjectName,
opts.Description,
opts.Private,
opts.Hidden,
opts.Mirror,
); err != nil {
return err
}
_, err := git.Init(rp, true)
if err != nil {
d.logger.Debug("failed to create repository", "err", err)
return err
}
if err := os.WriteFile(filepath.Join(rp, "description"), []byte(opts.Description), fs.ModePerm); err != nil {
d.logger.Error("failed to write description", "repo", name, "err", err)
return err
}
if !opts.Private {
if err := os.WriteFile(filepath.Join(rp, "git-daemon-export-ok"), []byte{}, fs.ModePerm); err != nil {
d.logger.Error("failed to write git-daemon-export-ok", "repo", name, "err", err)
return err
}
}
return hooks.GenerateHooks(ctx, d.cfg, name)
}); err != nil {
d.logger.Debug("failed to create repository in database", "err", err)
err = db.WrapError(err)
if errors.Is(err, db.ErrDuplicateKey) {
return nil, proto.ErrRepoExist
}
return nil, err
}
return d.Repository(ctx, name)
}
// ImportRepository imports a repository from remote.
// XXX: This a expensive operation and should be run in a goroutine.
func (d *Backend) ImportRepository(_ context.Context, name string, user proto.User, remote string, opts proto.RepositoryOptions) (proto.Repository, error) {
name = utils.SanitizeRepo(name)
if err := utils.ValidateRepo(name); err != nil {
return nil, err
}
remote = utils.Sanitize(remote)
if err := validateImportRemote(remote); err != nil {
return nil, err
}
rp := filepath.Join(d.repoPath(name))
tid := "import:" + name
if d.manager.Exists(tid) {
return nil, task.ErrAlreadyStarted
}
if _, err := os.Stat(rp); err == nil || os.IsExist(err) {
return nil, proto.ErrRepoExist
}
done := make(chan error, 1)
repoc := make(chan proto.Repository, 1)
d.logger.Info("importing repository", "name", name, "remote", remote, "path", rp)
d.manager.Add(tid, func(ctx context.Context) (err error) {
ctx = proto.WithUserContext(ctx, user)
copts := git.CloneOptions{
Bare: true,
Mirror: opts.Mirror,
Quiet: true,
CommandOptions: git.CommandOptions{
Timeout: -1,
Context: ctx,
Envs: []string{
fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"),
d.cfg.SSH.ClientKeyPath,
),
},
},
}
if err := git.Clone(remote, rp, copts); err != nil {
d.logger.Error("failed to clone repository", "err", err, "mirror", opts.Mirror, "remote", remote, "path", rp)
// Cleanup the mess!
if rerr := os.RemoveAll(rp); rerr != nil {
err = errors.Join(err, rerr)
}
return err
}
r, err := d.CreateRepository(ctx, name, user, opts)
if err != nil {
d.logger.Error("failed to create repository", "err", err, "name", name)
return err
}
defer func() {
if err != nil {
if rerr := d.DeleteRepository(ctx, name); rerr != nil {
d.logger.Error("failed to delete repository", "err", rerr, "name", name)
}
}
}()
rr, err := r.Open()
if err != nil {
d.logger.Error("failed to open repository", "err", err, "path", rp)
return err
}
repoc <- r
rcfg, err := rr.Config()
if err != nil {
d.logger.Error("failed to get repository config", "err", err, "path", rp)
return err
}
endpoint := remote
if opts.LFSEndpoint != "" {
endpoint = opts.LFSEndpoint
}
rcfg.Section("lfs").SetOption("url", endpoint)
if err := rr.SetConfig(rcfg); err != nil {
d.logger.Error("failed to set repository config", "err", err, "path", rp)
return err
}
ep, err := lfs.NewEndpoint(endpoint)
if err != nil {
d.logger.Error("failed to create lfs endpoint", "err", err, "path", rp)
return err
}
client := lfs.NewClient(ep)
if client == nil {
d.logger.Warn("failed to create lfs client: unsupported endpoint", "endpoint", endpoint)
return nil
}
if err := StoreRepoMissingLFSObjects(ctx, r, d.db, d.store, client); err != nil {
d.logger.Error("failed to store missing lfs objects", "err", err, "path", rp)
return err
}
return nil
})
go func() {
d.logger.Info("running import", "name", name)
d.manager.Run(tid, done)
}()
return <-repoc, <-done
}
// DeleteRepository deletes a repository.
//
// It implements backend.Backend.
func (d *Backend) DeleteRepository(ctx context.Context, name string) error {
name = utils.SanitizeRepo(name)
rp := filepath.Join(d.repoPath(name))
user := proto.UserFromContext(ctx)
r, err := d.Repository(ctx, name)
if err != nil {
return err
}
// We create the webhook event before deleting the repository so we can
// send the event after deleting the repository.
wh, err := webhook.NewRepositoryEvent(ctx, user, r, webhook.RepositoryEventActionDelete)
if err != nil {
return err
}
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
// Delete repo from cache
defer d.cache.Delete(name)
repom, dberr := d.store.GetRepoByName(ctx, tx, name)
_, ferr := os.Stat(rp)
if dberr != nil && ferr != nil {
return proto.ErrRepoNotFound
}
// If the repo is not in the database but the directory exists, remove it
if dberr != nil && ferr == nil {
return os.RemoveAll(rp)
} else if dberr != nil {
return db.WrapError(dberr)
}
repoID := strconv.FormatInt(repom.ID, 10)
strg := storage.NewLocalStorage(filepath.Join(d.cfg.DataPath, "lfs", repoID))
objs, err := d.store.GetLFSObjectsByName(ctx, tx, name)
if err != nil {
return db.WrapError(err)
}
for _, obj := range objs {
p := lfs.Pointer{
Oid: obj.Oid,
Size: obj.Size,
}
d.logger.Debug("deleting lfs object", "repo", name, "oid", obj.Oid)
if err := strg.Delete(path.Join("objects", p.RelativePath())); err != nil {
d.logger.Error("failed to delete lfs object", "repo", name, "err", err, "oid", obj.Oid)
}
}
if err := d.store.DeleteRepoByName(ctx, tx, name); err != nil {
return db.WrapError(err)
}
return os.RemoveAll(rp)
}); err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
return proto.ErrRepoNotFound
}
return db.WrapError(err)
}
return webhook.SendEvent(ctx, wh)
}
// DeleteUserRepositories deletes all user repositories.
func (d *Backend) DeleteUserRepositories(ctx context.Context, username string) error {
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
user, err := d.store.FindUserByUsername(ctx, tx, username)
if err != nil {
return err
}
repos, err := d.store.GetUserRepos(ctx, tx, user.ID)
if err != nil {
return err
}
for _, repo := range repos {
if err := d.DeleteRepository(ctx, repo.Name); err != nil {
return err
}
}
return nil
}); err != nil {
return db.WrapError(err)
}
return nil
}
// RenameRepository renames a repository.
//
// It implements backend.Backend.
func (d *Backend) RenameRepository(ctx context.Context, oldName string, newName string) error {
oldName = utils.SanitizeRepo(oldName)
if err := utils.ValidateRepo(oldName); err != nil {
return err
}
newName = utils.SanitizeRepo(newName)
if err := utils.ValidateRepo(newName); err != nil {
return err
}
if oldName == newName {
return nil
}
op := filepath.Join(d.repoPath(oldName))
np := filepath.Join(d.repoPath(newName))
if _, err := os.Stat(op); err != nil {
return proto.ErrRepoNotFound
}
if _, err := os.Stat(np); err == nil {
return proto.ErrRepoExist
}
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
// Delete cache
defer d.cache.Delete(oldName)
if err := d.store.SetRepoNameByName(ctx, tx, oldName, newName); err != nil {
return err
}
// Make sure the new repository parent directory exists.
if err := os.MkdirAll(filepath.Dir(np), os.ModePerm); err != nil {
return err
}
return os.Rename(op, np)
}); err != nil {
return db.WrapError(err)
}
user := proto.UserFromContext(ctx)
repo, err := d.Repository(ctx, newName)
if err != nil {
return err
}
wh, err := webhook.NewRepositoryEvent(ctx, user, repo, webhook.RepositoryEventActionRename)
if err != nil {
return err
}
return webhook.SendEvent(ctx, wh)
}
// Repositories returns a list of repositories per page.
//
// It implements backend.Backend.
func (d *Backend) Repositories(ctx context.Context) ([]proto.Repository, error) {
repos := make([]proto.Repository, 0)
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
ms, err := d.store.GetAllRepos(ctx, tx)
if err != nil {
return err
}
for _, m := range ms {
r := &repo{
name: m.Name,
path: filepath.Join(d.repoPath(m.Name)),
repo: m,
}
// Cache repositories
d.cache.Set(m.Name, r)
repos = append(repos, r)
}
return nil
}); err != nil {
return nil, db.WrapError(err)
}
return repos, nil
}
// Repository returns a repository by name.
//
// It implements backend.Backend.
func (d *Backend) Repository(ctx context.Context, name string) (proto.Repository, error) {
var m models.Repo
name = utils.SanitizeRepo(name)
if r, ok := d.cache.Get(name); ok && r != nil {
return r, nil
}
rp := filepath.Join(d.repoPath(name))
if _, err := os.Stat(rp); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
d.logger.Errorf("failed to stat repository path: %v", err)
}
return nil, proto.ErrRepoNotFound
}
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
m, err = d.store.GetRepoByName(ctx, tx, name)
return db.WrapError(err)
}); err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
return nil, proto.ErrRepoNotFound
}
return nil, db.WrapError(err)
}
r := &repo{
name: name,
path: rp,
repo: m,
}
// Add to cache
d.cache.Set(name, r)
return r, nil
}
// Description returns the description of a repository.
//
// It implements backend.Backend.
func (d *Backend) Description(ctx context.Context, name string) (string, error) {
name = utils.SanitizeRepo(name)
var desc string
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
desc, err = d.store.GetRepoDescriptionByName(ctx, tx, name)
return err
}); err != nil {
return "", db.WrapError(err)
}
return desc, nil
}
// IsMirror returns true if the repository is a mirror.
//
// It implements backend.Backend.
func (d *Backend) IsMirror(ctx context.Context, name string) (bool, error) {
name = utils.SanitizeRepo(name)
var mirror bool
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
mirror, err = d.store.GetRepoIsMirrorByName(ctx, tx, name)
return err
}); err != nil {
return false, db.WrapError(err)
}
return mirror, nil
}
// IsPrivate returns true if the repository is private.
//
// It implements backend.Backend.
func (d *Backend) IsPrivate(ctx context.Context, name string) (bool, error) {
name = utils.SanitizeRepo(name)
var private bool
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
private, err = d.store.GetRepoIsPrivateByName(ctx, tx, name)
return err
}); err != nil {
return false, db.WrapError(err)
}
return private, nil
}
// IsHidden returns true if the repository is hidden.
//
// It implements backend.Backend.
func (d *Backend) IsHidden(ctx context.Context, name string) (bool, error) {
name = utils.SanitizeRepo(name)
var hidden bool
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
hidden, err = d.store.GetRepoIsHiddenByName(ctx, tx, name)
return err
}); err != nil {
return false, db.WrapError(err)
}
return hidden, nil
}
// ProjectName returns the project name of a repository.
//
// It implements backend.Backend.
func (d *Backend) ProjectName(ctx context.Context, name string) (string, error) {
name = utils.SanitizeRepo(name)
var pname string
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
pname, err = d.store.GetRepoProjectNameByName(ctx, tx, name)
return err
}); err != nil {
return "", db.WrapError(err)
}
return pname, nil
}
// SetHidden sets the hidden flag of a repository.
//
// It implements backend.Backend.
func (d *Backend) SetHidden(ctx context.Context, name string, hidden bool) error {
name = utils.SanitizeRepo(name)
// Delete cache
d.cache.Delete(name)
return db.WrapError(d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.SetRepoIsHiddenByName(ctx, tx, name, hidden)
}))
}
// SetDescription sets the description of a repository.
//
// It implements backend.Backend.
func (d *Backend) SetDescription(ctx context.Context, name string, desc string) error {
name = utils.SanitizeRepo(name)
desc = utils.Sanitize(desc)
rp := filepath.Join(d.repoPath(name))
// Delete cache
d.cache.Delete(name)
return d.db.TransactionContext(ctx, func(tx *db.Tx) error {
if err := os.WriteFile(filepath.Join(rp, "description"), []byte(desc), fs.ModePerm); err != nil {
d.logger.Error("failed to write description", "repo", name, "err", err)
return err
}
return d.store.SetRepoDescriptionByName(ctx, tx, name, desc)
})
}
// SetPrivate sets the private flag of a repository.
//
// It implements backend.Backend.
func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) error {
name = utils.SanitizeRepo(name)
rp := filepath.Join(d.repoPath(name))
// Delete cache
d.cache.Delete(name)
if err := db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
fp := filepath.Join(rp, "git-daemon-export-ok")
if !private {
if err := os.WriteFile(fp, []byte{}, fs.ModePerm); err != nil {
d.logger.Error("failed to write git-daemon-export-ok", "repo", name, "err", err)
return err
}
} else {
if _, err := os.Stat(fp); err == nil {
if err := os.Remove(fp); err != nil {
d.logger.Error("failed to remove git-daemon-export-ok", "repo", name, "err", err)
return err
}
}
}
return d.store.SetRepoIsPrivateByName(ctx, tx, name, private)
}),
); err != nil {
return err
}
user := proto.UserFromContext(ctx)
repo, err := d.Repository(ctx, name)
if err != nil {
return err
}
if repo.IsPrivate() != !private {
wh, err := webhook.NewRepositoryEvent(ctx, user, repo, webhook.RepositoryEventActionVisibilityChange)
if err != nil {
return err
}
if err := webhook.SendEvent(ctx, wh); err != nil {
return err
}
}
return nil
}
// SetProjectName sets the project name of a repository.
//
// It implements backend.Backend.
func (d *Backend) SetProjectName(ctx context.Context, repo string, name string) error {
repo = utils.SanitizeRepo(repo)
name = utils.Sanitize(name)
// Delete cache
d.cache.Delete(repo)
return db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.SetRepoProjectNameByName(ctx, tx, repo, name)
}),
)
}
// repoPath returns the path to a repository.
func (d *Backend) repoPath(name string) string {
name = utils.SanitizeRepo(name)
rn := strings.ReplaceAll(name, "/", string(os.PathSeparator))
return filepath.Join(filepath.Join(d.cfg.DataPath, "repos"), rn+".git")
}
var _ proto.Repository = (*repo)(nil)
// repo is a Git repository with metadata stored in a SQLite database.
type repo struct {
name string
path string
repo models.Repo
}
// ID returns the repository's ID.
//
// It implements proto.Repository.
func (r *repo) ID() int64 {
return r.repo.ID
}
// UserID returns the repository's owner's user ID.
// If the repository is not owned by anyone, it returns 0.
//
// It implements proto.Repository.
func (r *repo) UserID() int64 {
if r.repo.UserID.Valid {
return r.repo.UserID.Int64
}
return 0
}
// Description returns the repository's description.
//
// It implements backend.Repository.
func (r *repo) Description() string {
return r.repo.Description
}
// IsMirror returns whether the repository is a mirror.
//
// It implements backend.Repository.
func (r *repo) IsMirror() bool {
return r.repo.Mirror
}
// IsPrivate returns whether the repository is private.
//
// It implements backend.Repository.
func (r *repo) IsPrivate() bool {
return r.repo.Private
}
// Name returns the repository's name.
//
// It implements backend.Repository.
func (r *repo) Name() string {
return r.name
}
// Open opens the repository.
//
// It implements backend.Repository.
func (r *repo) Open() (*git.Repository, error) {
return git.Open(r.path)
}
// ProjectName returns the repository's project name.
//
// It implements backend.Repository.
func (r *repo) ProjectName() string {
return r.repo.ProjectName
}
// IsHidden returns whether the repository is hidden.
//
// It implements backend.Repository.
func (r *repo) IsHidden() bool {
return r.repo.Hidden
}
// CreatedAt returns the repository's creation time.
func (r *repo) CreatedAt() time.Time {
return r.repo.CreatedAt
}
// UpdatedAt returns the repository's last update time.
func (r *repo) UpdatedAt() time.Time {
// Try to read the last modified time from the info directory.
if t, err := readOneline(filepath.Join(r.path, "info", "last-modified")); err == nil {
if t, err := time.Parse(time.RFC3339, t); err == nil {
return t
}
}
rr, err := git.Open(r.path)
if err == nil {
t, err := rr.LatestCommitTime()
if err == nil {
return t
}
}
return r.repo.UpdatedAt
}
func (r *repo) writeLastModified(t time.Time) error {
fp := filepath.Join(r.path, "info", "last-modified")
if err := os.MkdirAll(filepath.Dir(fp), os.ModePerm); err != nil {
return err
}
return os.WriteFile(fp, []byte(t.Format(time.RFC3339)), os.ModePerm) //nolint:gosec
}
func readOneline(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() //nolint: errcheck
s := bufio.NewScanner(f)
s.Scan()
return s.Text(), s.Err()
}
================================================
FILE: pkg/backend/settings.go
================================================
package backend
import (
"context"
"github.com/charmbracelet/soft-serve/pkg/access"
"github.com/charmbracelet/soft-serve/pkg/db"
)
// AllowKeyless returns whether or not keyless access is allowed.
//
// It implements backend.Backend.
func (b *Backend) AllowKeyless(ctx context.Context) bool {
var allow bool
if err := b.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
allow, err = b.store.GetAllowKeylessAccess(ctx, tx)
return err
}); err != nil {
return false
}
return allow
}
// SetAllowKeyless sets whether or not keyless access is allowed.
//
// It implements backend.Backend.
func (b *Backend) SetAllowKeyless(ctx context.Context, allow bool) error {
return b.db.TransactionContext(ctx, func(tx *db.Tx) error {
return b.store.SetAllowKeylessAccess(ctx, tx, allow)
})
}
// AnonAccess returns the level of anonymous access.
//
// It implements backend.Backend.
func (b *Backend) AnonAccess(ctx context.Context) access.AccessLevel {
var level access.AccessLevel
if err := b.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
level, err = b.store.GetAnonAccess(ctx, tx)
return err
}); err != nil {
return access.NoAccess
}
return level
}
// SetAnonAccess sets the level of anonymous access.
//
// It implements backend.Backend.
func (b *Backend) SetAnonAccess(ctx context.Context, level access.AccessLevel) error {
return b.db.TransactionContext(ctx, func(tx *db.Tx) error {
return b.store.SetAnonAccess(ctx, tx, level)
})
}
================================================
FILE: pkg/backend/user.go
================================================
package backend
import (
"context"
"errors"
"strings"
"time"
"github.com/charmbracelet/soft-serve/pkg/access"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/db/models"
"github.com/charmbracelet/soft-serve/pkg/proto"
"github.com/charmbracelet/soft-serve/pkg/sshutils"
"github.com/charmbracelet/soft-serve/pkg/utils"
"golang.org/x/crypto/ssh"
)
// AccessLevel returns the access level of a user for a repository.
//
// It implements backend.Backend.
func (d *Backend) AccessLevel(ctx context.Context, repo string, username string) access.AccessLevel {
user, _ := d.User(ctx, username)
return d.AccessLevelForUser(ctx, repo, user)
}
// AccessLevelByPublicKey returns the access level of a user's public key for a repository.
//
// It implements backend.Backend.
func (d *Backend) AccessLevelByPublicKey(ctx context.Context, repo string, pk ssh.PublicKey) access.AccessLevel {
for _, k := range d.cfg.AdminKeys() {
if sshutils.KeysEqual(pk, k) {
return access.AdminAccess
}
}
user, _ := d.UserByPublicKey(ctx, pk)
if user != nil {
return d.AccessLevel(ctx, repo, user.Username())
}
return d.AccessLevel(ctx, repo, "")
}
// AccessLevelForUser returns the access level of a user for a repository.
// TODO: user repository ownership
func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user proto.User) access.AccessLevel {
var username string
anon := d.AnonAccess(ctx)
if user != nil {
username = user.Username()
}
// If the user is an admin, they have admin access.
if user != nil && user.IsAdmin() {
return access.AdminAccess
}
// If the repository exists, check if the user is a collaborator.
r := proto.RepositoryFromContext(ctx)
if r == nil {
r, _ = d.Repository(ctx, repo)
}
if r != nil {
if user != nil {
// If the user is the owner, they have admin access.
if r.UserID() == user.ID() {
return access.AdminAccess
}
}
// If the user is a collaborator, they have return their access level.
collabAccess, isCollab, _ := d.IsCollaborator(ctx, repo, username)
if isCollab {
if anon > collabAccess {
return anon
}
return collabAccess
}
// If the repository is private, the user has no access.
if r.IsPrivate() {
return access.NoAccess
}
// Otherwise, the user has read-only access.
if user == nil {
return anon
}
return access.ReadOnlyAccess
}
if user != nil {
// If the repository doesn't exist, the user has read/write access.
if anon > access.ReadWriteAccess {
return anon
}
return access.ReadWriteAccess
}
// If the user doesn't exist, give them the anonymous access level.
return anon
}
// User finds a user by username.
//
// It implements backend.Backend.
func (d *Backend) User(ctx context.Context, username string) (proto.User, error) {
username = strings.ToLower(username)
if err := utils.ValidateUsername(username); err != nil {
return nil, err
}
var m models.User
var pks []ssh.PublicKey
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
m, err = d.store.FindUserByUsername(ctx, tx, username)
if err != nil {
return err
}
pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
return err
}); err != nil {
err = db.WrapError(err)
if errors.Is(err, db.ErrRecordNotFound) {
return nil, proto.ErrUserNotFound
}
d.logger.Error("error finding user", "username", username, "error", err)
return nil, err
}
return &user{
user: m,
publicKeys: pks,
}, nil
}
// UserByID finds a user by ID.
func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {
var m models.User
var pks []ssh.PublicKey
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
m, err = d.store.GetUserByID(ctx, tx, id)
if err != nil {
return err
}
pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
return err
}); err != nil {
err = db.WrapError(err)
if errors.Is(err, db.ErrRecordNotFound) {
return nil, proto.ErrUserNotFound
}
d.logger.Error("error finding user", "id", id, "error", err)
return nil, err
}
return &user{
user: m,
publicKeys: pks,
}, nil
}
// UserByPublicKey finds a user by public key.
//
// It implements backend.Backend.
func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto.User, error) {
var m models.User
var pks []ssh.PublicKey
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
m, err = d.store.FindUserByPublicKey(ctx, tx, pk)
if err != nil {
return db.WrapError(err)
}
pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
return err
}); err != nil {
err = db.WrapError(err)
if errors.Is(err, db.ErrRecordNotFound) {
return nil, proto.ErrUserNotFound
}
d.logger.Error("error finding user", "pk", sshutils.MarshalAuthorizedKey(pk), "error", err)
return nil, err
}
return &user{
user: m,
publicKeys: pks,
}, nil
}
// UserByAccessToken finds a user by access token.
// This also validates the token for expiration and returns proto.ErrTokenExpired.
func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.User, error) {
var m models.User
var pks []ssh.PublicKey
token = HashToken(token)
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
t, err := d.store.GetAccessTokenByToken(ctx, tx, token)
if err != nil {
return db.WrapError(err)
}
if t.ExpiresAt.Valid && t.ExpiresAt.Time.Before(time.Now()) {
return proto.ErrTokenExpired
}
m, err = d.store.FindUserByAccessToken(ctx, tx, token)
if err != nil {
return db.WrapError(err)
}
pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
return err
}); err != nil {
err = db.WrapError(err)
if errors.Is(err, db.ErrRecordNotFound) {
return nil, proto.ErrUserNotFound
}
d.logger.Error("failed to find user by access token", "err", err, "token", token)
return nil, err
}
return &user{
user: m,
publicKeys: pks,
}, nil
}
// Users returns all users.
//
// It implements backend.Backend.
func (d *Backend) Users(ctx context.Context) ([]string, error) {
var users []string
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
ms, err := d.store.GetAllUsers(ctx, tx)
if err != nil {
return err
}
for _, m := range ms {
users = append(users, m.Username)
}
return nil
}); err != nil {
return nil, db.WrapError(err)
}
return users, nil
}
// AddPublicKey adds a public key to a user.
//
// It implements backend.Backend.
func (d *Backend) AddPublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {
username = strings.ToLower(username)
if err := utils.ValidateUsername(username); err != nil {
return err
}
return db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.AddPublicKeyByUsername(ctx, tx, username, pk)
}),
)
}
// CreateUser creates a new user.
//
// It implements backend.Backend.
func (d *Backend) CreateUser(ctx context.Context, username string, opts proto.UserOptions) (proto.User, error) {
username = utils.Sanitize(username)
username = strings.ToLower(username)
if err := utils.ValidateUsername(username); err != nil {
return nil, err
}
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys)
}); err != nil {
return nil, db.WrapError(err)
}
return d.User(ctx, username)
}
// DeleteUser deletes a user.
//
// It implements backend.Backend.
func (d *Backend) DeleteUser(ctx context.Context, username string) error {
username = strings.ToLower(username)
if err := utils.ValidateUsername(username); err != nil {
return err
}
return d.db.TransactionContext(ctx, func(tx *db.Tx) error {
if err := d.store.DeleteUserByUsername(ctx, tx, username); err != nil {
return db.WrapError(err)
}
return d.DeleteUserRepositories(ctx, username)
})
}
// RemovePublicKey removes a public key from a user.
//
// It implements backend.Backend.
func (d *Backend) RemovePublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {
return db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.RemovePublicKeyByUsername(ctx, tx, username, pk)
}),
)
}
// ListPublicKeys lists the public keys of a user.
func (d *Backend) ListPublicKeys(ctx context.Context, username string) ([]ssh.PublicKey, error) {
username = strings.ToLower(username)
if err := utils.ValidateUsername(username); err != nil {
return nil, err
}
var keys []ssh.PublicKey
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
keys, err = d.store.ListPublicKeysByUsername(ctx, tx, username)
return err
}); err != nil {
return nil, db.WrapError(err)
}
return keys, nil
}
// SetUsername sets the username of a user.
//
// It implements backend.Backend.
func (d *Backend) SetUsername(ctx context.Context, username string, newUsername string) error {
username = strings.ToLower(username)
if err := utils.ValidateUsername(username); err != nil {
return err
}
return db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.SetUsernameByUsername(ctx, tx, username, newUsername)
}),
)
}
// SetAdmin sets the admin flag of a user.
//
// It implements backend.Backend.
func (d *Backend) SetAdmin(ctx context.Context, username string, admin bool) error {
username = strings.ToLower(username)
if err := utils.ValidateUsername(username); err != nil {
return err
}
return db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.SetAdminByUsername(ctx, tx, username, admin)
}),
)
}
// SetPassword sets the password of a user.
func (d *Backend) SetPassword(ctx context.Context, username string, rawPassword string) error {
username = strings.ToLower(username)
if err := utils.ValidateUsername(username); err != nil {
return err
}
password, err := HashPassword(rawPassword)
if err != nil {
return err
}
return db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.SetUserPasswordByUsername(ctx, tx, username, password)
}),
)
}
type user struct {
user models.User
publicKeys []ssh.PublicKey
}
var _ proto.User = (*user)(nil)
// IsAdmin implements proto.User
func (u *user) IsAdmin() bool {
return u.user.Admin
}
// PublicKeys implements proto.User
func (u *user) PublicKeys() []ssh.PublicKey {
return u.publicKeys
}
// Username implements proto.User
func (u *user) Username() string {
return u.user.Username
}
// ID implements proto.User.
func (u *user) ID() int64 {
return u.user.ID
}
// Password implements proto.User.
func (u *user) Password() string {
if u.user.Password.Valid {
return u.user.Password.String
}
return ""
}
================================================
FILE: pkg/backend/utils.go
================================================
package backend
import (
"github.com/charmbracelet/soft-serve/git"
"github.com/charmbracelet/soft-serve/pkg/proto"
)
// LatestFile returns the contents of the latest file at the specified path in
// the repository and its file path.
func LatestFile(r proto.Repository, ref *git.Reference, pattern string) (string, string, error) {
repo, err := r.Open()
if err != nil {
return "", "", err
}
return git.LatestFile(repo, ref, pattern)
}
// Readme returns the repository's README.
func Readme(r proto.Repository, ref *git.Reference) (readme string, path string, err error) {
pattern := "[rR][eE][aA][dD][mM][eE]*"
readme, path, err = LatestFile(r, ref, pattern)
return
}
================================================
FILE: pkg/backend/webhooks.go
================================================
package backend
import (
"context"
"encoding/json"
"charm.land/log/v2"
"github.com/charmbracelet/soft-serve/pkg/db"
"github.com/charmbracelet/soft-serve/pkg/db/models"
"github.com/charmbracelet/soft-serve/pkg/proto"
"github.com/charmbracelet/soft-serve/pkg/store"
"github.com/charmbracelet/soft-serve/pkg/utils"
"github.com/charmbracelet/soft-serve/pkg/webhook"
"github.com/google/uuid"
)
// CreateWebhook creates a webhook for a repository.
func (b *Backend) CreateWebhook(ctx context.Context, repo proto.Repository, url string, contentType webhook.ContentType, secret string, events []webhook.Event, active bool) error {
dbx := db.FromContext(ctx)
datastore := store.FromContext(ctx)
url = utils.Sanitize(url)
// Validate webhook URL to prevent SSRF attacks
if err := webhook.ValidateWebhookURL(url); err != nil {
return err //nolint:wrapcheck
}
return dbx.TransactionContext(ctx, func(tx *db.Tx) error {
lastID, err := datastore.CreateWebhook(ctx, tx, repo.ID(), url, secret, int(contentType), active)
if err != nil {
return db.WrapError(err)
}
evs := make([]int, len(events))
for i, e := range events {
evs[i] = int(e)
}
if err := datastore.CreateWebhookEvents(ctx, tx, lastID, evs); err != nil {
return db.WrapError(err)
}
return nil
})
}
// Webhook returns a webhook for a repository.
func (b *Backend) Webhook(ctx context.Context, repo proto.Repository, id int64) (webhook.Hook, error) {
dbx := db.FromContext(ctx)
datastore := store.FromContext(ctx)
var wh webhook.Hook
if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
h, err := datastore.GetWebhookByID(ctx, tx, repo.ID(), id)
if err != nil {
return db.WrapError(err)
}
events, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, id)
if err != nil {
return db.WrapError(err)
}
wh = webhook.Hook{
Webhook: h,
ContentType: webhook.ContentType(h.ContentType), //nolint:gosec
Events: make([]webhook.Event, len(events)),
}
for i, e := range events {
wh.Events[i] = webhook.Event(e.Event)
}
return nil
}); err != nil {
return webhook.Hook{}, db.WrapError(err)
}
return wh, nil
}
// ListWebhooks lists webhooks for a repository.
func (b *Backend) ListWebhooks(ctx context.Context, repo proto.Repository) ([]webhook.Hook, error) {
dbx := db.FromContext(ctx)
datastore := store.FromContext(ctx)
var webhooks []models.Webhook
webhookEvents := map[int64][]models.WebhookEvent{}
if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
webhooks, err = datastore.GetWebhooksByRepoID(ctx, tx, repo.ID())
if err != nil {
return err
}
for _, h := range webhooks {
events, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, h.ID)
if err != nil {
return err
}
webhookEvents[h.ID] = events
}
return nil
}); err != nil {
return nil, db.WrapError(err)
}
hooks := make([]webhook.Hook, len(webhooks))
for i, h := range webhooks {
events := make([]webhook.Event, len(webhookEvents[h.ID]))
for i, e := range webhookEvents[h.ID] {
events[i] = webhook.Event(e.Event)
}
hooks[i] = webhook.Hook{
Webhook: h,
ContentType: webhook.ContentType(h.ContentType), //nolint:gosec
Events: events,
}
}
return hooks, nil
}
// UpdateWebhook updates a webhook.
func (b *Backend) UpdateWebhook(ctx context.Context, repo proto.Repository, id int64, url string, contentType webhook.ContentType, secret string, updatedEvents []webhook.Event, active bool) error {
dbx := db.FromContext(ctx)
datastore := store.FromContext(ctx)
// Validate webhook URL to prevent SSRF attacks
if err := webhook.ValidateWebhookURL(url); err != nil {
return err
}
return dbx.TransactionContext(ctx, func(tx *db.Tx) error {
if err := datastore.UpdateWebhookByID(ctx, tx, repo.ID(), id, url, secret, int(contentType), active); err != nil {
return db.WrapError(err)
}
currentEvents, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, id)
if err != nil {
return db.WrapError(err)
}
// Delete events that are no longer in the list.
toBeDeleted := make([]int64, 0)
for _, e := range currentEvents {
found := false
for _, ne := range updatedEvents {
if int(ne) == e.Event {
found = true
break
}
}
if !found {
toBeDeleted = append(toBeDeleted, e.ID)
}
}
if err := datastore.DeleteWebhookEventsByID(ctx, tx, toBeDeleted); err != nil {
return db.WrapError(err)
}
// Prune events that are already in the list.
newEvents := make([]int, 0)
for _, e := range updatedEvents {
found := false
for _, ne := range currentEvents {
if int(e) == ne.Event {
found = true
break
}
}
if !found {
newEvents = append(newEvents, int(e))
}
}
if err := datastore.CreateWebhookEvents(ctx, tx, id, newEvents); err != nil {
return db.WrapError(err)
}
return nil
})
}
// DeleteWebhook deletes a webhook for a repository.
func (b *Backend) DeleteWebhook(ctx context.Context, repo proto.Repository, id int64) error {
dbx := db.FromContext(ctx)
datastore := store.FromContext(ctx)
return dbx.TransactionContext(ctx, func(tx *db.Tx) error {
_, err := datastore.GetWebhookByID(ctx, tx, repo.ID(), id)
if err != nil {
return db.WrapError(err)
}
if err := datastore.DeleteWebhookForRepoByID(ctx, tx, repo.ID(), id); err != nil {
return db.WrapError(err)
}
return nil
})
}
// ListWebhookDeliveries lists webhook deliveries for a webhook.
func (b *Backend) ListWebhookDeliveries(ctx context.Context, id int64) ([]webhook.Delivery, error) {
dbx := db.FromContext(ctx)
datastore := store.FromContext(ctx)
var deliveries []models.WebhookDelivery
if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
deliveries, err = datastore.ListWebhookDeliveriesByWebhookID(ctx, tx, id)
if err != nil {
return db.WrapError(err)
}
return nil
}); err != nil {
return nil, db.WrapError(err)
}
ds := make([]webhook.Delivery, len(deliveries))
for i, d := range deliveries {
ds[i] = webhook.Delivery{
WebhookDelivery: d,
Event: webhook.Event(d.Event),
}
}
return ds, nil
}
// RedeliverWebhookDelivery redelivers a webhook delivery.
func (b *Backend) RedeliverWebhookDelivery(ctx context.Context, repo proto.Repository, id int64, delID uuid.UUID) error {
dbx := db.FromContext(ctx)
datastore := store.FromContext(ctx)
var delivery models.WebhookDelivery
var wh models.Webhook
if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
wh, err = datastore.GetWebhookByID(ctx, tx, repo.ID(), id)
if err != nil {
log.Errorf("error getting webhook: %v", err)
return db.WrapError(err)
}
delivery, err = datastore.GetWebhookDeliveryByID(ctx, tx, id, delID)
if err != nil {
return db.WrapError(err)
}
return nil
}); err != nil {
return db.WrapError(err)
}
log.Infof("redelivering webhook delivery %s for webhook %d\n\n%s\n\n", delID, id, delivery.RequestBody)
var payload json.RawMessage
if err := json.Unmars
gitextract_twsjoh99/
├── .editorconfig
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── build.yml
│ ├── coverage.yml
│ ├── dependabot-sync.yml
│ ├── goreleaser.yml
│ ├── lint-sync.yml
│ ├── lint.yml
│ └── nightly.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── .nfpm/
│ ├── postinstall.sh
│ ├── postremove.sh
│ ├── soft-serve.conf
│ ├── soft-serve.service
│ ├── sysusers.conf
│ └── tmpfiles.conf
├── Dockerfile
├── LICENSE
├── README.md
├── browse.tape
├── cmd/
│ ├── cmd.go
│ └── soft/
│ ├── admin/
│ │ └── admin.go
│ ├── browse/
│ │ └── browse.go
│ ├── hook/
│ │ └── hook.go
│ ├── main.go
│ └── serve/
│ ├── certreloader.go
│ ├── certreloader_test.go
│ ├── serve.go
│ └── server.go
├── codecov.yml
├── demo.tape
├── docker.md
├── git/
│ ├── attr.go
│ ├── attr_test.go
│ ├── command.go
│ ├── commit.go
│ ├── config.go
│ ├── errors.go
│ ├── patch.go
│ ├── reference.go
│ ├── repo.go
│ ├── server.go
│ ├── stash.go
│ ├── tag.go
│ ├── tree.go
│ ├── types.go
│ └── utils.go
├── go.mod
├── go.sum
├── pkg/
│ ├── access/
│ │ ├── access.go
│ │ ├── access_test.go
│ │ ├── context.go
│ │ └── context_test.go
│ ├── backend/
│ │ ├── access_token.go
│ │ ├── auth.go
│ │ ├── auth_test.go
│ │ ├── backend.go
│ │ ├── cache.go
│ │ ├── collab.go
│ │ ├── context.go
│ │ ├── hooks.go
│ │ ├── lfs.go
│ │ ├── repo.go
│ │ ├── settings.go
│ │ ├── user.go
│ │ ├── utils.go
│ │ └── webhooks.go
│ ├── config/
│ │ ├── config.go
│ │ ├── config_test.go
│ │ ├── context.go
│ │ ├── context_test.go
│ │ ├── file.go
│ │ ├── file_test.go
│ │ ├── ssh.go
│ │ ├── ssh_test.go
│ │ └── testdata/
│ │ ├── config.yaml
│ │ └── k1.pub
│ ├── cron/
│ │ ├── cron.go
│ │ └── cron_test.go
│ ├── daemon/
│ │ ├── conn.go
│ │ ├── daemon.go
│ │ └── daemon_test.go
│ ├── db/
│ │ ├── context.go
│ │ ├── context_test.go
│ │ ├── db.go
│ │ ├── db_test.go
│ │ ├── errors.go
│ │ ├── errors_test.go
│ │ ├── handler.go
│ │ ├── internal/
│ │ │ └── test/
│ │ │ └── test.go
│ │ ├── logger.go
│ │ ├── migrate/
│ │ │ ├── 0001_create_tables.go
│ │ │ ├── 0001_create_tables_postgres.down.sql
│ │ │ ├── 0001_create_tables_postgres.up.sql
│ │ │ ├── 0001_create_tables_sqlite.down.sql
│ │ │ ├── 0001_create_tables_sqlite.up.sql
│ │ │ ├── 0002_webhooks.go
│ │ │ ├── 0002_webhooks_postgres.down.sql
│ │ │ ├── 0002_webhooks_postgres.up.sql
│ │ │ ├── 0002_webhooks_sqlite.down.sql
│ │ │ ├── 0002_webhooks_sqlite.up.sql
│ │ │ ├── 0003_migrate_lfs_objects.go
│ │ │ ├── migrate.go
│ │ │ ├── migrate_test.go
│ │ │ └── migrations.go
│ │ └── models/
│ │ ├── access_token.go
│ │ ├── collab.go
│ │ ├── lfs.go
│ │ ├── public_key.go
│ │ ├── repo.go
│ │ ├── settings.go
│ │ ├── user.go
│ │ └── webhook.go
│ ├── git/
│ │ ├── errors.go
│ │ ├── git.go
│ │ ├── git_test.go
│ │ ├── lfs.go
│ │ ├── lfs_auth.go
│ │ ├── lfs_log.go
│ │ └── service.go
│ ├── hooks/
│ │ ├── gen.go
│ │ ├── gen_test.go
│ │ └── hooks.go
│ ├── jobs/
│ │ ├── jobs.go
│ │ └── mirror.go
│ ├── jwk/
│ │ ├── jwk.go
│ │ └── jwk_test.go
│ ├── lfs/
│ │ ├── basic_transfer.go
│ │ ├── client.go
│ │ ├── common.go
│ │ ├── endpoint.go
│ │ ├── http_client.go
│ │ ├── pointer.go
│ │ ├── pointer_test.go
│ │ ├── scanner.go
│ │ ├── ssh_client.go
│ │ └── transfer.go
│ ├── log/
│ │ ├── log.go
│ │ └── log_test.go
│ ├── proto/
│ │ ├── access_token.go
│ │ ├── context.go
│ │ ├── errors.go
│ │ ├── repo.go
│ │ └── user.go
│ ├── ssh/
│ │ ├── cmd/
│ │ │ ├── blob.go
│ │ │ ├── branch.go
│ │ │ ├── cmd.go
│ │ │ ├── collab.go
│ │ │ ├── commit.go
│ │ │ ├── create.go
│ │ │ ├── delete.go
│ │ │ ├── description.go
│ │ │ ├── git.go
│ │ │ ├── hidden.go
│ │ │ ├── import.go
│ │ │ ├── info.go
│ │ │ ├── jwt.go
│ │ │ ├── list.go
│ │ │ ├── mirror.go
│ │ │ ├── private.go
│ │ │ ├── project_name.go
│ │ │ ├── pubkey.go
│ │ │ ├── rename.go
│ │ │ ├── repo.go
│ │ │ ├── set_username.go
│ │ │ ├── settings.go
│ │ │ ├── tag.go
│ │ │ ├── token.go
│ │ │ ├── tree.go
│ │ │ ├── user.go
│ │ │ └── webhooks.go
│ │ ├── middleware.go
│ │ ├── middleware_test.go
│ │ ├── session.go
│ │ ├── session_test.go
│ │ ├── ssh.go
│ │ └── ui.go
│ ├── sshutils/
│ │ ├── utils.go
│ │ └── utils_test.go
│ ├── ssrf/
│ │ ├── ssrf.go
│ │ └── ssrf_test.go
│ ├── stats/
│ │ └── stats.go
│ ├── storage/
│ │ ├── local.go
│ │ └── storage.go
│ ├── store/
│ │ ├── access_token.go
│ │ ├── collab.go
│ │ ├── context.go
│ │ ├── database/
│ │ │ ├── access_token.go
│ │ │ ├── collab.go
│ │ │ ├── database.go
│ │ │ ├── lfs.go
│ │ │ ├── repo.go
│ │ │ ├── settings.go
│ │ │ ├── user.go
│ │ │ └── webhooks.go
│ │ ├── lfs.go
│ │ ├── repo.go
│ │ ├── settings.go
│ │ ├── store.go
│ │ ├── user.go
│ │ └── webhooks.go
│ ├── sync/
│ │ ├── workqueue.go
│ │ └── workqueue_test.go
│ ├── task/
│ │ └── manager.go
│ ├── test/
│ │ └── test.go
│ ├── ui/
│ │ ├── common/
│ │ │ ├── common.go
│ │ │ ├── common_test.go
│ │ │ ├── component.go
│ │ │ ├── error.go
│ │ │ ├── format.go
│ │ │ ├── style.go
│ │ │ └── utils.go
│ │ ├── components/
│ │ │ ├── code/
│ │ │ │ └── code.go
│ │ │ ├── footer/
│ │ │ │ └── footer.go
│ │ │ ├── header/
│ │ │ │ └── header.go
│ │ │ ├── selector/
│ │ │ │ └── selector.go
│ │ │ ├── statusbar/
│ │ │ │ └── statusbar.go
│ │ │ ├── tabs/
│ │ │ │ └── tabs.go
│ │ │ └── viewport/
│ │ │ └── viewport.go
│ │ ├── keymap/
│ │ │ └── keymap.go
│ │ ├── pages/
│ │ │ ├── repo/
│ │ │ │ ├── empty.go
│ │ │ │ ├── files.go
│ │ │ │ ├── filesitem.go
│ │ │ │ ├── log.go
│ │ │ │ ├── logitem.go
│ │ │ │ ├── readme.go
│ │ │ │ ├── refs.go
│ │ │ │ ├── refsitem.go
│ │ │ │ ├── repo.go
│ │ │ │ ├── stash.go
│ │ │ │ └── stashitem.go
│ │ │ └── selection/
│ │ │ ├── item.go
│ │ │ └── selection.go
│ │ └── styles/
│ │ └── styles.go
│ ├── utils/
│ │ ├── utils.go
│ │ └── utils_test.go
│ ├── version/
│ │ └── version.go
│ ├── web/
│ │ ├── auth.go
│ │ ├── context.go
│ │ ├── git.go
│ │ ├── git_lfs.go
│ │ ├── goget.go
│ │ ├── health.go
│ │ ├── http.go
│ │ ├── logging.go
│ │ ├── server.go
│ │ └── util.go
│ └── webhook/
│ ├── branch_tag.go
│ ├── collaborator.go
│ ├── common.go
│ ├── content_type.go
│ ├── content_type_test.go
│ ├── event.go
│ ├── push.go
│ ├── repository.go
│ ├── ssrf_test.go
│ ├── validator.go
│ ├── validator_test.go
│ └── webhook.go
├── systemd.md
└── testscript/
├── script_test.go
└── testdata/
├── anon-access.txtar
├── auth-bypass-regression.txtar
├── config-servers-git_disabled.txtar
├── config-servers-http_disabled.txtar
├── config-servers-ssh_disabled.txtar
├── config-servers-stats_disabled.txtar
├── help.txtar
├── http-cors.txtar
├── http.txtar
├── jwt.txtar
├── mirror.txtar
├── repo-blob.txtar
├── repo-collab.txtar
├── repo-commit.txtar
├── repo-create.txtar
├── repo-delete.txtar
├── repo-import-local-path.txtar
├── repo-import.txtar
├── repo-perms.txtar
├── repo-push.txtar
├── repo-tree.txtar
├── repo-webhook-ssrf.txtar
├── repo-webhooks.txtar
├── set-username.txtar
├── settings.txtar
├── soft-browse.txtar
├── soft-manpages.txtar
├── ssh-lfs.txtar
├── ssh.txtar
├── token.txtar
├── ui-home.txtar
└── user_management.txtar
SYMBOL INDEX (1333 symbols across 222 files)
FILE: cmd/cmd.go
function InitBackendContext (line 20) | func InitBackendContext(cmd *cobra.Command, _ []string) error {
function CloseDBContext (line 45) | func CloseDBContext(cmd *cobra.Command, _ []string) error {
function InitializeHooks (line 58) | func InitializeHooks(ctx context.Context, cfg *config.Config, be *backen...
FILE: cmd/soft/admin/admin.go
function init (line 71) | func init() {
FILE: cmd/soft/browse/browse.go
type state (line 68) | type state
constant startState (line 71) | startState state = iota
constant errorState (line 72) | errorState
type model (line 75) | type model struct
method SetSize (line 87) | func (m *model) SetSize(w, h int) {
method ShortHelp (line 101) | func (m model) ShortHelp() []key.Binding {
method FullHelp (line 115) | func (m model) FullHelp() [][]key.Binding {
method Init (line 133) | func (m *model) Init() tea.Cmd {
method Update (line 145) | func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 202) | func (m *model) View() tea.View {
type repository (line 238) | type repository struct
method Description (line 245) | func (r repository) Description() string {
method ID (line 250) | func (r repository) ID() int64 {
method IsHidden (line 255) | func (repository) IsHidden() bool {
method IsMirror (line 260) | func (repository) IsMirror() bool {
method IsPrivate (line 265) | func (repository) IsPrivate() bool {
method Name (line 270) | func (r repository) Name() string {
method Open (line 275) | func (r repository) Open() (*git.Repository, error) {
method ProjectName (line 280) | func (r repository) ProjectName() string {
method UpdatedAt (line 285) | func (r repository) UpdatedAt() time.Time {
method UserID (line 295) | func (r repository) UserID() int64 {
method CreatedAt (line 300) | func (r repository) CreatedAt() time.Time {
FILE: cmd/soft/hook/hook.go
function init (line 156) | func init() {
function runCommand (line 166) | func runCommand(ctx context.Context, in io.Reader, out io.Writer, err io...
FILE: cmd/soft/main.go
function init (line 68) | func init() {
function main (line 100) | func main() {
FILE: cmd/soft/serve/certreloader.go
type CertReloader (line 11) | type CertReloader struct
method Reload (line 35) | func (cr *CertReloader) Reload() error {
method GetCertificateFunc (line 48) | func (cr *CertReloader) GetCertificateFunc() func(*tls.ClientHelloInfo...
function NewCertReloader (line 19) | func NewCertReloader(certPath, keyPath string, logger *log.Logger) (*Cer...
FILE: cmd/soft/serve/certreloader_test.go
function generateTestCert (line 21) | func generateTestCert(t *testing.T, certPath, keyPath, cn string) {
function TestCertReloader (line 63) | func TestCertReloader(t *testing.T) {
FILE: cmd/soft/serve/serve.go
function init (line 140) | func init() {
constant updateHookExample (line 144) | updateHookExample = `#!/bin/sh
FILE: cmd/soft/serve/server.go
type Server (line 26) | type Server struct
method ReloadCertificates (line 107) | func (s *Server) ReloadCertificates() error {
method Start (line 115) | func (s *Server) Start() error {
method Shutdown (line 170) | func (s *Server) Shutdown(ctx context.Context) error {
method Close (line 196) | func (s *Server) Close() error {
function NewServer (line 45) | func NewServer(ctx context.Context) (*Server, error) {
FILE: git/attr.go
type Attribute (line 13) | type Attribute struct
method CheckAttributes (line 19) | func (r *Repository) CheckAttributes(ref *Reference, path string) ([]Att...
function parseAttributes (line 42) | func parseAttributes(path string, buf []byte) []Attribute {
FILE: git/attr_test.go
function TestParseAttr (line 9) | func TestParseAttr(t *testing.T) {
FILE: git/command.go
function NewCommand (line 9) | func NewCommand(args ...string) *git.Command {
FILE: git/commit.go
constant ZeroID (line 10) | ZeroID = git.EmptyID
function IsZeroHash (line 13) | func IsZeroHash(h string) bool {
type Commits (line 22) | type Commits
method Len (line 25) | func (cl Commits) Len() int { return len(cl) }
method Swap (line 28) | func (cl Commits) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
method Less (line 31) | func (cl Commits) Less(i, j int) bool {
FILE: git/config.go
method Config (line 11) | func (r *Repository) Config() (*gcfg.Config, error) {
method SetConfig (line 29) | func (r *Repository) SetConfig(cfg *gcfg.Config) error {
FILE: git/patch.go
type DiffSection (line 16) | type DiffSection struct
method diffFor (line 24) | func (s *DiffSection) diffFor(line *git.DiffLine) string {
function diffsToString (line 63) | func diffsToString(diffs []diffmatchpatch.Diff, lineType git.DiffLineTyp...
type DiffFile (line 89) | type DiffFile struct
method Files (line 117) | func (f *DiffFile) Files() (from *DiffFileChange, to *DiffFileChange) {
type DiffFileChange (line 95) | type DiffFileChange struct
method Hash (line 102) | func (f *DiffFileChange) Hash() string {
method Name (line 107) | func (f *DiffFileChange) Name() string {
method Mode (line 112) | func (f *DiffFileChange) Mode() git.EntryMode {
type FileStats (line 136) | type FileStats
method String (line 139) | func (fs FileStats) String() string {
function printStats (line 143) | func printStats(stats FileStats) string {
type Diff (line 231) | type Diff struct
method Stats (line 237) | func (d *Diff) Stats() FileStats {
method Patch (line 322) | func (d *Diff) Patch() string {
constant dstPrefix (line 242) | dstPrefix = "b/"
constant srcPrefix (line 243) | srcPrefix = "a/"
function appendPathLines (line 246) | func appendPathLines(lines []string, fromPath, toPath string, isBinary b...
function writeFilePatchHeader (line 258) | func writeFilePatchHeader(sb *strings.Builder, filePatch *DiffFile) {
function toDiff (line 336) | func toDiff(ddiff *git.Diff) *Diff {
FILE: git/reference.go
constant HEAD (line 11) | HEAD = "HEAD"
constant RefsHeads (line 13) | RefsHeads = git.RefsHeads
constant RefsTags (line 15) | RefsTags = git.RefsTags
type Reference (line 19) | type Reference struct
method Name (line 38) | func (r *Reference) Name() ReferenceName {
method IsBranch (line 43) | func (r *Reference) IsBranch() bool {
method IsTag (line 48) | func (r *Reference) IsTag() bool {
type ReferenceName (line 25) | type ReferenceName
method String (line 28) | func (r ReferenceName) String() string {
method Short (line 33) | func (r ReferenceName) Short() string {
FILE: git/repo.go
type Repository (line 20) | type Repository struct
method HEAD (line 66) | func (r *Repository) HEAD() (*Reference, error) {
method References (line 85) | func (r *Repository) References() ([]*Reference, error) {
method LsTree (line 101) | func (r *Repository) LsTree(ref string) (*Tree, error) {
method Tree (line 114) | func (r *Repository) Tree(ref *Reference) (*Tree, error) {
method TreePath (line 126) | func (r *Repository) TreePath(ref *Reference, path string) (*Tree, err...
method Diff (line 142) | func (r *Repository) Diff(commit *Commit) (*Diff, error) {
method Patch (line 155) | func (r *Repository) Patch(commit *Commit) (string, error) {
method CountCommits (line 164) | func (r *Repository) CountCommits(ref *Reference) (int64, error) {
method CommitsByPage (line 169) | func (r *Repository) CommitsByPage(ref *Reference, page, size int) (Co...
method SymbolicRef (line 181) | func (r *Repository) SymbolicRef(name string, ref string, opts ...git....
function Clone (line 27) | func Clone(src, dst string, opts ...git.CloneOptions) error {
function Init (line 32) | func Init(path string, bare bool) (*Repository, error) {
function gitDir (line 44) | func gitDir(r *git.Repository) (string, error) {
function Open (line 49) | func Open(path string) (*Repository, error) {
FILE: git/server.go
function UpdateServerInfo (line 10) | func UpdateServerInfo(ctx context.Context, path string) error {
FILE: git/stash.go
method StashDiff (line 6) | func (r *Repository) StashDiff(index int) (*Diff, error) {
FILE: git/tree.go
type Tree (line 15) | type Tree struct
method SubTree (line 84) | func (t *Tree) SubTree(path string) (*Tree, error) {
method Entries (line 97) | func (t *Tree) Entries() (Entries, error) {
method TreeEntry (line 113) | func (t *Tree) TreeEntry(path string) (*TreeEntry, error) {
type TreeEntry (line 22) | type TreeEntry struct
method Mode (line 163) | func (e *TreeEntry) Mode() fs.FileMode {
method File (line 174) | func (e *TreeEntry) File() *File {
method Contents (line 183) | func (e *TreeEntry) Contents() ([]byte, error) {
type Entries (line 29) | type Entries
method Len (line 41) | func (es Entries) Len() int { return len(es) }
method Swap (line 44) | func (es Entries) Swap(i, j int) { es[i], es[j] = es[j], es[i] }
method Less (line 47) | func (es Entries) Less(i, j int) bool {
method Sort (line 63) | func (es Entries) Sort() {
type File (line 68) | type File struct
method Name (line 74) | func (f *File) Name() string {
method Path (line 79) | func (f *File) Path() string {
method IsBinary (line 151) | func (f *File) IsBinary() (bool, error) {
method Contents (line 188) | func (f *File) Contents() ([]byte, error) {
constant sniffLen (line 124) | sniffLen = 8000
function IsBinary (line 128) | func IsBinary(r io.Reader) (bool, error) {
FILE: git/utils.go
function LatestFile (line 11) | func LatestFile(repo *Repository, ref *Reference, pattern string) (strin...
function isGitDir (line 59) | func isGitDir(path string) bool {
FILE: pkg/access/access.go
type AccessLevel (line 9) | type AccessLevel
method String (line 26) | func (a AccessLevel) String() string {
method UnmarshalText (line 66) | func (a *AccessLevel) UnmarshalText(text []byte) error {
method MarshalText (line 78) | func (a AccessLevel) MarshalText() (text []byte, err error) {
constant NoAccess (line 13) | NoAccess AccessLevel = iota
constant ReadOnlyAccess (line 16) | ReadOnlyAccess
constant ReadWriteAccess (line 19) | ReadWriteAccess
constant AdminAccess (line 22) | AdminAccess
function ParseAccessLevel (line 42) | func ParseAccessLevel(s string) AccessLevel {
FILE: pkg/access/access_test.go
function TestParseAccessLevel (line 5) | func TestParseAccessLevel(t *testing.T) {
FILE: pkg/access/context.go
function FromContext (line 9) | func FromContext(ctx context.Context) AccessLevel {
function WithContext (line 18) | func WithContext(ctx context.Context, ac AccessLevel) context.Context {
FILE: pkg/access/context_test.go
function TestGoodFromContext (line 8) | func TestGoodFromContext(t *testing.T) {
function TestBadFromContext (line 15) | func TestBadFromContext(t *testing.T) {
FILE: pkg/backend/access_token.go
method CreateAccessToken (line 14) | func (b *Backend) CreateAccessToken(ctx context.Context, user proto.User...
method DeleteAccessToken (line 34) | func (b *Backend) DeleteAccessToken(ctx context.Context, user proto.User...
method ListAccessTokens (line 57) | func (b *Backend) ListAccessTokens(ctx context.Context, user proto.User)...
FILE: pkg/backend/auth.go
constant saltySalt (line 12) | saltySalt = "salty-soft-serve"
function HashPassword (line 15) | func HashPassword(password string) (string, error) {
function VerifyPassword (line 25) | func VerifyPassword(password, hash string) bool {
function GenerateToken (line 31) | func GenerateToken() string {
function HashToken (line 42) | func HashToken(token string) string {
FILE: pkg/backend/auth_test.go
function TestHashPassword (line 5) | func TestHashPassword(t *testing.T) {
function TestVerifyPassword (line 15) | func TestVerifyPassword(t *testing.T) {
function TestGenerateToken (line 25) | func TestGenerateToken(t *testing.T) {
function TestHashToken (line 32) | func TestHashToken(t *testing.T) {
FILE: pkg/backend/backend.go
type Backend (line 15) | type Backend struct
function New (line 26) | func New(ctx context.Context, cfg *config.Config, db *db.DB, st store.St...
FILE: pkg/backend/cache.go
type cache (line 6) | type cache struct
method Get (line 21) | func (c *cache) Get(repo string) (*repo, bool) {
method Set (line 25) | func (c *cache) Set(repo string, r *repo) {
method Delete (line 29) | func (c *cache) Delete(repo string) {
method Len (line 33) | func (c *cache) Len() int {
function newCache (line 11) | func newCache(b *Backend, size int) *cache {
FILE: pkg/backend/collab.go
method AddCollaborator (line 19) | func (d *Backend) AddCollaborator(ctx context.Context, repo string, user...
method Collaborators (line 54) | func (d *Backend) Collaborators(ctx context.Context, repo string) ([]str...
method IsCollaborator (line 76) | func (d *Backend) IsCollaborator(ctx context.Context, repo string, usern...
method RemoveCollaborator (line 97) | func (d *Backend) RemoveCollaborator(ctx context.Context, repo string, u...
FILE: pkg/backend/context.go
function FromContext (line 9) | func FromContext(ctx context.Context) *Backend {
function WithContext (line 18) | func WithContext(ctx context.Context, b *Backend) context.Context {
FILE: pkg/backend/hooks.go
method PostReceive (line 21) | func (d *Backend) PostReceive(_ context.Context, _ io.Writer, _ io.Write...
method PreReceive (line 28) | func (d *Backend) PreReceive(_ context.Context, _ io.Writer, _ io.Writer...
method Update (line 35) | func (d *Backend) Update(ctx context.Context, _ io.Writer, _ io.Writer, ...
method PostUpdate (line 92) | func (d *Backend) PostUpdate(ctx context.Context, _ io.Writer, _ io.Writ...
function populateLastModified (line 110) | func populateLastModified(ctx context.Context, d *Backend, name string) ...
FILE: pkg/backend/lfs.go
function StoreRepoMissingLFSObjects (line 20) | func StoreRepoMissingLFSObjects(ctx context.Context, repo proto.Reposito...
FILE: pkg/backend/repo.go
function validateImportRemote (line 28) | func validateImportRemote(remote string) error {
method CreateRepository (line 40) | func (d *Backend) CreateRepository(ctx context.Context, name string, use...
method ImportRepository (line 102) | func (d *Backend) ImportRepository(_ context.Context, name string, user ...
method DeleteRepository (line 227) | func (d *Backend) DeleteRepository(ctx context.Context, name string) err...
method DeleteUserRepositories (line 297) | func (d *Backend) DeleteUserRepositories(ctx context.Context, username s...
method RenameRepository (line 326) | func (d *Backend) RenameRepository(ctx context.Context, oldName string, ...
method Repositories (line 386) | func (d *Backend) Repositories(ctx context.Context) ([]proto.Repository,...
method Repository (line 419) | func (d *Backend) Repository(ctx context.Context, name string) (proto.Re...
method Description (line 461) | func (d *Backend) Description(ctx context.Context, name string) (string,...
method IsMirror (line 478) | func (d *Backend) IsMirror(ctx context.Context, name string) (bool, erro...
method IsPrivate (line 494) | func (d *Backend) IsPrivate(ctx context.Context, name string) (bool, err...
method IsHidden (line 511) | func (d *Backend) IsHidden(ctx context.Context, name string) (bool, erro...
method ProjectName (line 528) | func (d *Backend) ProjectName(ctx context.Context, name string) (string,...
method SetHidden (line 545) | func (d *Backend) SetHidden(ctx context.Context, name string, hidden boo...
method SetDescription (line 559) | func (d *Backend) SetDescription(ctx context.Context, name string, desc ...
method SetPrivate (line 580) | func (d *Backend) SetPrivate(ctx context.Context, name string, private b...
method SetProjectName (line 633) | func (d *Backend) SetProjectName(ctx context.Context, repo string, name ...
method repoPath (line 648) | func (d *Backend) repoPath(name string) string {
type repo (line 657) | type repo struct
method ID (line 666) | func (r *repo) ID() int64 {
method UserID (line 674) | func (r *repo) UserID() int64 {
method Description (line 684) | func (r *repo) Description() string {
method IsMirror (line 691) | func (r *repo) IsMirror() bool {
method IsPrivate (line 698) | func (r *repo) IsPrivate() bool {
method Name (line 705) | func (r *repo) Name() string {
method Open (line 712) | func (r *repo) Open() (*git.Repository, error) {
method ProjectName (line 719) | func (r *repo) ProjectName() string {
method IsHidden (line 726) | func (r *repo) IsHidden() bool {
method CreatedAt (line 731) | func (r *repo) CreatedAt() time.Time {
method UpdatedAt (line 736) | func (r *repo) UpdatedAt() time.Time {
method writeLastModified (line 755) | func (r *repo) writeLastModified(t time.Time) error {
function readOneline (line 764) | func readOneline(path string) (string, error) {
FILE: pkg/backend/settings.go
method AllowKeyless (line 13) | func (b *Backend) AllowKeyless(ctx context.Context) bool {
method SetAllowKeyless (line 29) | func (b *Backend) SetAllowKeyless(ctx context.Context, allow bool) error {
method AnonAccess (line 38) | func (b *Backend) AnonAccess(ctx context.Context) access.AccessLevel {
method SetAnonAccess (line 54) | func (b *Backend) SetAnonAccess(ctx context.Context, level access.Access...
FILE: pkg/backend/user.go
method AccessLevel (line 21) | func (d *Backend) AccessLevel(ctx context.Context, repo string, username...
method AccessLevelByPublicKey (line 29) | func (d *Backend) AccessLevelByPublicKey(ctx context.Context, repo strin...
method AccessLevelForUser (line 46) | func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, u...
method User (line 110) | func (d *Backend) User(ctx context.Context, username string) (proto.User...
method UserByID (line 143) | func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, e...
method UserByPublicKey (line 173) | func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey)...
method UserByAccessToken (line 202) | func (d *Backend) UserByAccessToken(ctx context.Context, token string) (...
method Users (line 242) | func (d *Backend) Users(ctx context.Context) ([]string, error) {
method AddPublicKey (line 265) | func (d *Backend) AddPublicKey(ctx context.Context, username string, pk ...
method CreateUser (line 281) | func (d *Backend) CreateUser(ctx context.Context, username string, opts ...
method DeleteUser (line 300) | func (d *Backend) DeleteUser(ctx context.Context, username string) error {
method RemovePublicKey (line 318) | func (d *Backend) RemovePublicKey(ctx context.Context, username string, ...
method ListPublicKeys (line 327) | func (d *Backend) ListPublicKeys(ctx context.Context, username string) (...
method SetUsername (line 348) | func (d *Backend) SetUsername(ctx context.Context, username string, newU...
method SetAdmin (line 364) | func (d *Backend) SetAdmin(ctx context.Context, username string, admin b...
method SetPassword (line 378) | func (d *Backend) SetPassword(ctx context.Context, username string, rawP...
type user (line 396) | type user struct
method IsAdmin (line 404) | func (u *user) IsAdmin() bool {
method PublicKeys (line 409) | func (u *user) PublicKeys() []ssh.PublicKey {
method Username (line 414) | func (u *user) Username() string {
method ID (line 419) | func (u *user) ID() int64 {
method Password (line 424) | func (u *user) Password() string {
FILE: pkg/backend/utils.go
function LatestFile (line 10) | func LatestFile(r proto.Repository, ref *git.Reference, pattern string) ...
function Readme (line 19) | func Readme(r proto.Repository, ref *git.Reference) (readme string, path...
FILE: pkg/backend/webhooks.go
method CreateWebhook (line 18) | func (b *Backend) CreateWebhook(ctx context.Context, repo proto.Reposito...
method Webhook (line 47) | func (b *Backend) Webhook(ctx context.Context, repo proto.Repository, id...
method ListWebhooks (line 80) | func (b *Backend) ListWebhooks(ctx context.Context, repo proto.Repositor...
method UpdateWebhook (line 124) | func (b *Backend) UpdateWebhook(ctx context.Context, repo proto.Reposito...
method DeleteWebhook (line 186) | func (b *Backend) DeleteWebhook(ctx context.Context, repo proto.Reposito...
method ListWebhookDeliveries (line 204) | func (b *Backend) ListWebhookDeliveries(ctx context.Context, id int64) (...
method RedeliverWebhookDelivery (line 233) | func (b *Backend) RedeliverWebhookDelivery(ctx context.Context, repo pro...
method WebhookDelivery (line 269) | func (b *Backend) WebhookDelivery(ctx context.Context, webhookID int64, ...
FILE: pkg/config/config.go
type SSHConfig (line 20) | type SSHConfig struct
type GitConfig (line 44) | type GitConfig struct
type CORSConfig (line 65) | type CORSConfig struct
type HTTPConfig (line 74) | type HTTPConfig struct
type StatsConfig (line 95) | type StatsConfig struct
type LogConfig (line 104) | type LogConfig struct
type DBConfig (line 119) | type DBConfig struct
type LFSConfig (line 128) | type LFSConfig struct
type JobsConfig (line 138) | type JobsConfig struct
type Config (line 143) | type Config struct
method Environ (line 179) | func (c *Config) Environ() []string {
method ParseFile (line 259) | func (c *Config) ParseFile() error {
method ParseEnv (line 285) | func (c *Config) ParseEnv() error {
method Parse (line 291) | func (c *Config) Parse() error {
method WriteConfig (line 308) | func (c *Config) WriteConfig() error {
method ConfigPath (line 325) | func (c *Config) ConfigPath() string { //nolint:revive
method Exist (line 341) | func (c *Config) Exist() bool {
method Validate (line 404) | func (c *Config) Validate() error {
method AdminKeys (line 472) | func (c *Config) AdminKeys() []ssh.PublicKey {
function IsDebug (line 229) | func IsDebug() bool {
function IsVerbose (line 236) | func IsVerbose() bool {
function parseFile (line 243) | func parseFile(cfg *Config, path string) error {
function parseEnv (line 264) | func parseEnv(cfg *Config) error {
function writeConfig (line 300) | func writeConfig(cfg *Config, path string) error {
function DefaultDataPath (line 315) | func DefaultDataPath() string {
function exist (line 335) | func exist(path string) bool {
function DefaultConfig (line 348) | func DefaultConfig() *Config {
function parseAuthKeys (line 452) | func parseAuthKeys(aks []string) []ssh.PublicKey {
function init (line 476) | func init() {
FILE: pkg/config/config_test.go
function TestParseMultipleKeys (line 10) | func TestParseMultipleKeys(t *testing.T) {
function TestMergeInitAdminKeys (line 27) | func TestMergeInitAdminKeys(t *testing.T) {
function TestValidateInitAdminKeys (line 43) | func TestValidateInitAdminKeys(t *testing.T) {
function TestCustomConfigLocation (line 60) | func TestCustomConfigLocation(t *testing.T) {
function TestParseMultipleHeaders (line 83) | func TestParseMultipleHeaders(t *testing.T) {
function TestParseMultipleOrigins (line 98) | func TestParseMultipleOrigins(t *testing.T) {
function TestParseMultipleMethods (line 113) | func TestParseMultipleMethods(t *testing.T) {
FILE: pkg/config/context.go
function WithContext (line 9) | func WithContext(ctx context.Context, cfg *Config) context.Context {
function FromContext (line 14) | func FromContext(ctx context.Context) *Config {
FILE: pkg/config/context_test.go
function TestBadFromContext (line 9) | func TestBadFromContext(t *testing.T) {
function TestGoodFromContext (line 16) | func TestGoodFromContext(t *testing.T) {
function TestGoodFromContextWithDefaultConfig (line 23) | func TestGoodFromContextWithDefaultConfig(t *testing.T) {
FILE: pkg/config/file.go
function newConfigFile (line 152) | func newConfigFile(cfg *Config) string {
FILE: pkg/config/file_test.go
function TestNewConfigFile (line 5) | func TestNewConfigFile(t *testing.T) {
FILE: pkg/config/ssh.go
function KeyPair (line 18) | func KeyPair(cfg *Config) (*keygen.SSHKeyPair, error) {
FILE: pkg/config/ssh_test.go
function TestBadSSHKeyPair (line 5) | func TestBadSSHKeyPair(t *testing.T) {
function TestGoodSSHKeyPair (line 16) | func TestGoodSSHKeyPair(t *testing.T) {
FILE: pkg/cron/cron.go
type Scheduler (line 12) | type Scheduler struct
method Shutdown (line 41) | func (s *Scheduler) Shutdown() {
method Start (line 48) | func (s *Scheduler) Start() {
method AddFunc (line 53) | func (s *Scheduler) AddFunc(spec string, fn func()) (int, error) {
method Remove (line 59) | func (s *Scheduler) Remove(id int) {
type cronLogger (line 18) | type cronLogger struct
method Info (line 23) | func (l cronLogger) Info(msg string, keysAndValues ...interface{}) {
method Error (line 28) | func (l cronLogger) Error(err error, msg string, keysAndValues ...inte...
function NewScheduler (line 33) | func NewScheduler(ctx context.Context) *Scheduler {
FILE: pkg/cron/cron_test.go
function TestCronLogger (line 12) | func TestCronLogger(t *testing.T) {
function TestSchedularAddRemove (line 24) | func TestSchedularAddRemove(t *testing.T) {
FILE: pkg/daemon/conn.go
type connections (line 12) | type connections struct
method Add (line 17) | func (m *connections) Add(c net.Conn) {
method Close (line 23) | func (m *connections) Close(c net.Conn) error {
method Size (line 31) | func (m *connections) Size() int {
method CloseAll (line 37) | func (m *connections) CloseAll() error {
type serverConn (line 51) | type serverConn struct
method Write (line 62) | func (c *serverConn) Write(p []byte) (n int, err error) {
method Read (line 71) | func (c *serverConn) Read(b []byte) (n int, err error) {
method Close (line 80) | func (c *serverConn) Close() (err error) {
method updateDeadline (line 88) | func (c *serverConn) updateDeadline() {
FILE: pkg/daemon/daemon.go
type GitDaemon (line 46) | type GitDaemon struct
method ListenAndServe (line 78) | func (d *GitDaemon) ListenAndServe() error {
method Serve (line 91) | func (d *GitDaemon) Serve(listener net.Listener) error {
method fatal (line 142) | func (d *GitDaemon) fatal(c net.Conn, err error) {
method handleClient (line 150) | func (d *GitDaemon) handleClient(conn net.Conn) {
method Close (line 319) | func (d *GitDaemon) Close() error {
method closeListener (line 326) | func (d *GitDaemon) closeListener() error {
method Shutdown (line 347) | func (d *GitDaemon) Shutdown(ctx context.Context) error {
function NewGitDaemon (line 62) | func NewGitDaemon(ctx context.Context) (*GitDaemon, error) {
FILE: pkg/daemon/daemon_test.go
function TestMain (line 27) | func TestMain(m *testing.M) {
function TestIdleTimeout (line 73) | func TestIdleTimeout(t *testing.T) {
function TestInvalidRepo (line 96) | func TestInvalidRepo(t *testing.T) {
function readPktline (line 110) | func readPktline(c net.Conn) (string, error) {
FILE: pkg/db/context.go
function FromContext (line 9) | func FromContext(ctx context.Context) *DB {
function WithContext (line 17) | func WithContext(ctx context.Context, db *DB) context.Context {
FILE: pkg/db/context_test.go
function TestBadFromContext (line 11) | func TestBadFromContext(t *testing.T) {
function TestGoodFromContext (line 18) | func TestGoodFromContext(t *testing.T) {
FILE: pkg/db/db.go
type DB (line 17) | type DB struct
method Close (line 42) | func (d *DB) Close() error {
method Transaction (line 53) | func (d *DB) Transaction(fn func(tx *Tx) error) error {
method TransactionContext (line 58) | func (d *DB) TransactionContext(ctx context.Context, fn func(tx *Tx) e...
function Open (line 23) | func Open(ctx context.Context, driverName string, dsn string) (*DB, erro...
type Tx (line 47) | type Tx struct
function rollback (line 80) | func rollback(tx *Tx, err error) error {
FILE: pkg/db/db_test.go
function TestOpenUnknownDriver (line 9) | func TestOpenUnknownDriver(t *testing.T) {
FILE: pkg/db/errors.go
function WrapError (line 22) | func WrapError(err error) error {
FILE: pkg/db/errors_test.go
function TestWrapErrorBadNoRows (line 10) | func TestWrapErrorBadNoRows(t *testing.T) {
function TestWrapErrorGoodNoRows (line 21) | func TestWrapErrorGoodNoRows(t *testing.T) {
FILE: pkg/db/handler.go
type Handler (line 11) | type Handler interface
FILE: pkg/db/internal/test/test.go
function OpenSqlite (line 14) | func OpenSqlite(ctx context.Context, tb testing.TB) (*db.DB, error) {
FILE: pkg/db/logger.go
function trace (line 12) | func trace(l *log.Logger, query string, args ...interface{}) {
method Select (line 22) | func (d *DB) Select(dest interface{}, query string, args ...interface{})...
method Get (line 28) | func (d *DB) Get(dest interface{}, query string, args ...interface{}) er...
method Queryx (line 34) | func (d *DB) Queryx(query string, args ...interface{}) (*sqlx.Rows, erro...
method QueryRowx (line 40) | func (d *DB) QueryRowx(query string, args ...interface{}) *sqlx.Row {
method Exec (line 48) | func (d *DB) Exec(query string, args ...interface{}) (sql.Result, error) {
method SelectContext (line 54) | func (d *DB) SelectContext(ctx context.Context, dest interface{}, query ...
method GetContext (line 60) | func (d *DB) GetContext(ctx context.Context, dest interface{}, query str...
method QueryxContext (line 66) | func (d *DB) QueryxContext(ctx context.Context, query string, args ...in...
method QueryRowxContext (line 72) | func (d *DB) QueryRowxContext(ctx context.Context, query string, args .....
method ExecContext (line 78) | func (d *DB) ExecContext(ctx context.Context, query string, args ...inte...
method Select (line 84) | func (t *Tx) Select(dest interface{}, query string, args ...interface{})...
method Get (line 90) | func (t *Tx) Get(dest interface{}, query string, args ...interface{}) er...
method Queryx (line 96) | func (t *Tx) Queryx(query string, args ...interface{}) (*sqlx.Rows, erro...
method QueryRowx (line 102) | func (t *Tx) QueryRowx(query string, args ...interface{}) *sqlx.Row {
method Exec (line 110) | func (t *Tx) Exec(query string, args ...interface{}) (sql.Result, error) {
method SelectContext (line 116) | func (t *Tx) SelectContext(ctx context.Context, dest interface{}, query ...
method GetContext (line 122) | func (t *Tx) GetContext(ctx context.Context, dest interface{}, query str...
method QueryxContext (line 128) | func (t *Tx) QueryxContext(ctx context.Context, query string, args ...in...
method QueryRowxContext (line 134) | func (t *Tx) QueryRowxContext(ctx context.Context, query string, args .....
method ExecContext (line 140) | func (t *Tx) ExecContext(ctx context.Context, query string, args ...inte...
FILE: pkg/db/migrate/0001_create_tables.go
constant createTablesName (line 16) | createTablesName = "create tables"
constant createTablesVersion (line 17) | createTablesVersion = 1
FILE: pkg/db/migrate/0001_create_tables_postgres.up.sql
type settings (line 1) | CREATE TABLE IF NOT EXISTS settings (
type users (line 9) | CREATE TABLE IF NOT EXISTS users (
type public_keys (line 18) | CREATE TABLE IF NOT EXISTS public_keys (
type repos (line 30) | CREATE TABLE IF NOT EXISTS repos (
type collabs (line 47) | CREATE TABLE IF NOT EXISTS collabs (
type lfs_objects (line 65) | CREATE TABLE IF NOT EXISTS lfs_objects (
type lfs_locks (line 79) | CREATE TABLE IF NOT EXISTS lfs_locks (
type access_tokens (line 98) | CREATE TABLE IF NOT EXISTS access_tokens (
FILE: pkg/db/migrate/0001_create_tables_sqlite.up.sql
type settings (line 1) | CREATE TABLE IF NOT EXISTS settings (
type users (line 9) | CREATE TABLE IF NOT EXISTS users (
type public_keys (line 18) | CREATE TABLE IF NOT EXISTS public_keys (
type repos (line 30) | CREATE TABLE IF NOT EXISTS repos (
type collabs (line 47) | CREATE TABLE IF NOT EXISTS collabs (
type lfs_objects (line 65) | CREATE TABLE IF NOT EXISTS lfs_objects (
type lfs_locks (line 79) | CREATE TABLE IF NOT EXISTS lfs_locks (
type access_tokens (line 98) | CREATE TABLE IF NOT EXISTS access_tokens (
FILE: pkg/db/migrate/0002_webhooks.go
constant webhooksName (line 10) | webhooksName = "webhooks"
constant webhooksVersion (line 11) | webhooksVersion = 2
FILE: pkg/db/migrate/0002_webhooks_postgres.up.sql
type webhooks (line 1) | CREATE TABLE IF NOT EXISTS webhooks (
type webhook_events (line 17) | CREATE TABLE IF NOT EXISTS webhook_events (
type webhook_deliveries (line 29) | CREATE TABLE IF NOT EXISTS webhook_deliveries (
FILE: pkg/db/migrate/0002_webhooks_sqlite.up.sql
type webhooks (line 1) | CREATE TABLE IF NOT EXISTS webhooks (
type webhook_events (line 17) | CREATE TABLE IF NOT EXISTS webhook_events (
type webhook_deliveries (line 29) | CREATE TABLE IF NOT EXISTS webhook_deliveries (
FILE: pkg/db/migrate/0003_migrate_lfs_objects.go
constant migrateLfsObjectsName (line 16) | migrateLfsObjectsName = "migrate_lfs_objects"
constant migrateLfsObjectsVersion (line 17) | migrateLfsObjectsVersion = 3
function goodRelativePath (line 58) | func goodRelativePath(oid string) string {
function badRelativePath (line 65) | func badRelativePath(oid string) string {
FILE: pkg/db/migrate/migrate.go
type MigrateFunc (line 14) | type MigrateFunc
type Migration (line 18) | type Migration struct
type Migrations (line 26) | type Migrations struct
method schema (line 32) | func (Migrations) schema(driverName string) string {
function Migrate (line 63) | func Migrate(ctx context.Context, dbx *db.DB) error {
function Rollback (line 99) | func Rollback(ctx context.Context, dbx *db.DB) error {
function hasTable (line 127) | func hasTable(tx *db.Tx, tableName string) bool {
FILE: pkg/db/migrate/migrate_test.go
function TestMigrate (line 11) | func TestMigrate(t *testing.T) {
FILE: pkg/db/migrate/migrations.go
function execMigration (line 23) | func execMigration(ctx context.Context, tx *db.Tx, version int, name str...
function migrateUp (line 47) | func migrateUp(ctx context.Context, tx *db.Tx, version int, name string)...
function migrateDown (line 51) | func migrateDown(ctx context.Context, tx *db.Tx, version int, name strin...
function toSnakeCase (line 60) | func toSnakeCase(str string) string {
FILE: pkg/db/models/access_token.go
type AccessToken (line 9) | type AccessToken struct
FILE: pkg/db/models/collab.go
type Collab (line 10) | type Collab struct
FILE: pkg/db/models/lfs.go
type LFSObject (line 6) | type LFSObject struct
type LFSLock (line 16) | type LFSLock struct
FILE: pkg/db/models/public_key.go
type PublicKey (line 4) | type PublicKey struct
FILE: pkg/db/models/repo.go
type Repo (line 9) | type Repo struct
FILE: pkg/db/models/settings.go
type Settings (line 4) | type Settings struct
FILE: pkg/db/models/user.go
type User (line 9) | type User struct
FILE: pkg/db/models/webhook.go
type Webhook (line 11) | type Webhook struct
type WebhookEvent (line 23) | type WebhookEvent struct
type WebhookDelivery (line 31) | type WebhookDelivery struct
FILE: pkg/git/git.go
function WritePktline (line 21) | func WritePktline(w io.Writer, v ...interface{}) error {
function WritePktlineErr (line 35) | func WritePktlineErr(w io.Writer, err error) error {
function EnsureWithin (line 40) | func EnsureWithin(reposDir string, repo string) error {
function EnsureDefaultBranch (line 64) | func EnsureDefaultBranch(ctx context.Context, repoPath string) error {
FILE: pkg/git/git_test.go
function TestPktline (line 13) | func TestPktline(t *testing.T) {
function TestEnsureWithinBad (line 62) | func TestEnsureWithinBad(t *testing.T) {
function TestEnsureWithinGood (line 74) | func TestEnsureWithinGood(t *testing.T) {
function TestEnsureDefaultBranchEmpty (line 87) | func TestEnsureDefaultBranchEmpty(t *testing.T) {
FILE: pkg/git/lfs.go
type lfsTransfer (line 26) | type lfsTransfer struct
method Batch (line 90) | func (t *lfsTransfer) Batch(_ string, pointers []transfer.BatchItem, _...
method Download (line 113) | func (t *lfsTransfer) Download(oid string, _ transfer.Args) (io.ReadCl...
method Upload (line 130) | func (t *lfsTransfer) Upload(oid string, size int64, r io.Reader, _ tr...
method Verify (line 180) | func (t *lfsTransfer) Verify(oid string, size int64, _ transfer.Args) ...
method LockBackend (line 207) | func (t *lfsTransfer) LockBackend(args transfer.Args) transfer.LockBac...
function LFSTransfer (line 43) | func LFSTransfer(ctx context.Context, cmd ServiceCommand) error {
type lfsLockBackend (line 198) | type lfsLockBackend struct
method Create (line 218) | func (l *lfsLockBackend) Create(path string, refname string) (transfer...
method FromID (line 248) | func (l *lfsLockBackend) FromID(id string) (transfer.Lock, error) {
method FromPath (line 278) | func (l *lfsLockBackend) FromPath(path string) (transfer.Lock, error) {
method Range (line 304) | func (l *lfsLockBackend) Range(cursor string, limit int, fn func(trans...
method Unlock (line 360) | func (l *lfsLockBackend) Unlock(lock transfer.Lock) error {
type LFSLock (line 384) | type LFSLock struct
method AsArguments (line 393) | func (l *LFSLock) AsArguments() []string {
method AsLockSpec (line 403) | func (l *LFSLock) AsLockSpec(ownerID bool) ([]string, error) {
method FormattedTimestamp (line 425) | func (l *LFSLock) FormattedTimestamp() string {
method ID (line 430) | func (l *LFSLock) ID() string {
method OwnerName (line 435) | func (l *LFSLock) OwnerName() string {
method Path (line 440) | func (l *LFSLock) Path() string {
method Unlock (line 445) | func (l *LFSLock) Unlock() error {
FILE: pkg/git/lfs_auth.go
function LFSAuthenticate (line 21) | func LFSAuthenticate(ctx context.Context, cmd ServiceCommand) error {
FILE: pkg/git/lfs_log.go
type lfsLogger (line 8) | type lfsLogger struct
method Log (line 15) | func (l *lfsLogger) Log(msg string, kv ...interface{}) {
FILE: pkg/git/service.go
type Service (line 17) | type Service
method String (line 33) | func (s Service) String() string {
method Name (line 38) | func (s Service) Name() string {
method Handler (line 43) | func (s Service) Handler(ctx context.Context, cmd ServiceCommand) error {
constant UploadPackService (line 21) | UploadPackService Service = "git-upload-pack"
constant UploadArchiveService (line 23) | UploadArchiveService Service = "git-upload-archive"
constant ReceivePackService (line 25) | ReceivePackService Service = "git-receive-pack"
constant LFSTransferService (line 27) | LFSTransferService Service = "git-lfs-transfer"
constant LFSAuthenticateService (line 29) | LFSAuthenticateService = "git-lfs-authenticate"
type ServiceHandler (line 57) | type ServiceHandler
function gitServiceHandler (line 60) | func gitServiceHandler(ctx context.Context, svc Service, scmd ServiceCom...
type ServiceCommand (line 177) | type ServiceCommand struct
function UploadPack (line 190) | func UploadPack(ctx context.Context, cmd ServiceCommand) error {
function UploadArchive (line 195) | func UploadArchive(ctx context.Context, cmd ServiceCommand) error {
function ReceivePack (line 200) | func ReceivePack(ctx context.Context, cmd ServiceCommand) error {
FILE: pkg/hooks/gen.go
constant PreReceiveHook (line 17) | PreReceiveHook = "pre-receive"
constant UpdateHook (line 18) | UpdateHook = "update"
constant PostReceiveHook (line 19) | PostReceiveHook = "post-receive"
constant PostUpdateHook (line 20) | PostUpdateHook = "post-update"
function GenerateHooks (line 31) | func GenerateHooks(_ context.Context, cfg *config.Config, repo string) e...
constant hookTemplate (line 96) | hookTemplate = `#!/usr/bin/env bash
FILE: pkg/hooks/gen_test.go
function TestGenerateHooks (line 13) | func TestGenerateHooks(t *testing.T) {
FILE: pkg/hooks/hooks.go
type HookArg (line 9) | type HookArg struct
type Hooks (line 16) | type Hooks interface
FILE: pkg/jobs/jobs.go
type Job (line 9) | type Job struct
type Runner (line 15) | type Runner interface
function Register (line 26) | func Register(name string, runner Runner) {
function List (line 33) | func List() map[string]*Job {
FILE: pkg/jobs/mirror.go
function init (line 20) | func init() {
type mirrorPull (line 24) | type mirrorPull struct
method Spec (line 27) | func (m mirrorPull) Spec(ctx context.Context) string {
method Func (line 36) | func (m mirrorPull) Func(ctx context.Context) func() {
FILE: pkg/jwk/jwk.go
type Pair (line 18) | type Pair struct
method PrivateKey (line 24) | func (p Pair) PrivateKey() crypto.PrivateKey {
method JWK (line 29) | func (p Pair) JWK() jose.JSONWebKey {
function NewPair (line 34) | func NewPair(cfg *config.Config) (Pair, error) {
FILE: pkg/jwk/jwk_test.go
function TestBadNewPair (line 10) | func TestBadNewPair(t *testing.T) {
function TestGoodNewPair (line 17) | func TestGoodNewPair(t *testing.T) {
FILE: pkg/lfs/basic_transfer.go
type BasicTransferAdapter (line 16) | type BasicTransferAdapter struct
method Name (line 21) | func (a *BasicTransferAdapter) Name() string {
method Download (line 26) | func (a *BasicTransferAdapter) Download(ctx context.Context, _ Pointer...
method Upload (line 35) | func (a *BasicTransferAdapter) Upload(ctx context.Context, p Pointer, ...
method Verify (line 54) | func (a *BasicTransferAdapter) Verify(ctx context.Context, p Pointer, ...
method performRequest (line 71) | func (a *BasicTransferAdapter) performRequest(ctx context.Context, met...
function handleErrorResponse (line 107) | func handleErrorResponse(resp *http.Response) error {
function decodeResponseError (line 117) | func decodeResponseError(r io.Reader) (ErrorResponse, error) {
FILE: pkg/lfs/client.go
type DownloadCallback (line 9) | type DownloadCallback
type UploadCallback (line 12) | type UploadCallback
type Client (line 15) | type Client interface
function NewClient (line 21) | func NewClient(e Endpoint) Client {
FILE: pkg/lfs/common.go
constant MediaType (line 9) | MediaType = "application/vnd.git-lfs+json"
constant OperationDownload (line 12) | OperationDownload = "download"
constant OperationUpload (line 15) | OperationUpload = "upload"
constant ActionDownload (line 18) | ActionDownload = OperationDownload
constant ActionUpload (line 21) | ActionUpload = OperationUpload
constant ActionVerify (line 24) | ActionVerify = "verify"
constant DefaultLocksLimit (line 28) | DefaultLocksLimit = 20
type Pointer (line 32) | type Pointer struct
type PointerBlob (line 38) | type PointerBlob struct
type ErrorResponse (line 44) | type ErrorResponse struct
type BatchResponse (line 53) | type BatchResponse struct
type ObjectResponse (line 60) | type ObjectResponse struct
type Link (line 67) | type Link struct
type ObjectError (line 75) | type ObjectError struct
type BatchRequest (line 82) | type BatchRequest struct
type Reference (line 92) | type Reference struct
type AuthenticateResponse (line 97) | type AuthenticateResponse struct
type LockCreateRequest (line 107) | type LockCreateRequest struct
type Owner (line 113) | type Owner struct
type Lock (line 120) | type Lock struct
type LockDeleteRequest (line 130) | type LockDeleteRequest struct
type LockListResponse (line 138) | type LockListResponse struct
type LockVerifyRequest (line 144) | type LockVerifyRequest struct
type LockVerifyResponse (line 153) | type LockVerifyResponse struct
type LockResponse (line 160) | type LockResponse struct
FILE: pkg/lfs/endpoint.go
function NewEndpoint (line 13) | func NewEndpoint(rawurl string) (Endpoint, error) {
function endpointFromBareSSH (line 51) | func endpointFromBareSSH(rawurl string) (*url.URL, error) {
FILE: pkg/lfs/http_client.go
type httpClient (line 16) | type httpClient struct
method Download (line 37) | func (c *httpClient) Download(ctx context.Context, objects []Pointer, ...
method Upload (line 42) | func (c *httpClient) Upload(ctx context.Context, objects []Pointer, ca...
method transferNames (line 46) | func (c *httpClient) transferNames() []string {
method batch (line 57) | func (c *httpClient) batch(ctx context.Context, operation string, obje...
method performOperation (line 111) | func (c *httpClient) performOperation(ctx context.Context, objects []P...
function newHTTPClient (line 25) | func newHTTPClient(endpoint Endpoint) *httpClient {
FILE: pkg/lfs/pointer.go
constant blobSizeCutoff (line 16) | blobSizeCutoff = 1024
constant HashAlgorithmSHA256 (line 19) | HashAlgorithmSHA256 = "sha256"
constant MetaFileIdentifier (line 23) | MetaFileIdentifier = "version https://git-lfs.github.com/spec/v1"
constant MetaFileOidPrefix (line 26) | MetaFileOidPrefix = "oid " + HashAlgorithmSHA256 + ":"
function ReadPointer (line 41) | func ReadPointer(reader io.Reader) (Pointer, error) {
function ReadPointerFromBuffer (line 55) | func ReadPointerFromBuffer(buf []byte) (Pointer, error) {
method IsValid (line 85) | func (p Pointer) IsValid() bool {
method String (line 100) | func (p Pointer) String() string {
method RelativePath (line 106) | func (p Pointer) RelativePath() string {
function GeneratePointer (line 115) | func GeneratePointer(content io.Reader) (Pointer, error) {
FILE: pkg/lfs/pointer_test.go
function TestReadPointer (line 10) | func TestReadPointer(t *testing.T) {
FILE: pkg/lfs/scanner.go
function SearchPointerBlobs (line 18) | func SearchPointerBlobs(ctx context.Context, repo *git.Repository, point...
function createPointerResultsFromCatFileBatch (line 52) | func createPointerResultsFromCatFileBatch(ctx context.Context, catFileBa...
function catFileBatch (line 105) | func catFileBatch(ctx context.Context, shasToBatchReader *io.PipeReader,...
function blobsLessThan1024FromCatFileBatchCheck (line 123) | func blobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeR...
function catFileBatchCheck (line 155) | func catFileBatchCheck(ctx context.Context, shasToCheckReader *io.PipeRe...
function blobsFromRevListObjects (line 173) | func blobsFromRevListObjects(revListReader *io.PipeReader, shasToCheckWr...
function revListAllObjects (line 202) | func revListAllObjects(ctx context.Context, revListWriter *io.PipeWriter...
FILE: pkg/lfs/transfer.go
constant TransferBasic (line 9) | TransferBasic = "basic"
type TransferAdapter (line 12) | type TransferAdapter interface
FILE: pkg/log/log.go
function NewLogger (line 13) | func NewLogger(cfg *config.Config) (*log.Logger, *os.File, error) {
FILE: pkg/log/log_test.go
function TestGoodNewLogger (line 10) | func TestGoodNewLogger(t *testing.T) {
function TestBadNewLogger (line 28) | func TestBadNewLogger(t *testing.T) {
FILE: pkg/proto/access_token.go
type AccessToken (line 6) | type AccessToken struct
FILE: pkg/proto/context.go
function RepositoryFromContext (line 12) | func RepositoryFromContext(ctx context.Context) Repository {
function UserFromContext (line 20) | func UserFromContext(ctx context.Context) User {
function WithRepositoryContext (line 28) | func WithRepositoryContext(ctx context.Context, r Repository) context.Co...
function WithUserContext (line 33) | func WithUserContext(ctx context.Context, u User) context.Context {
FILE: pkg/proto/repo.go
type Repository (line 10) | type Repository interface
type RepositoryOptions (line 38) | type RepositoryOptions struct
function RepositoryDefaultBranch (line 49) | func RepositoryDefaultBranch(repo Repository) (string, error) {
FILE: pkg/proto/user.go
type User (line 6) | type User interface
type UserOptions (line 20) | type UserOptions struct
FILE: pkg/ssh/cmd/blob.go
function blobCommand (line 15) | func blobCommand() *cobra.Command {
FILE: pkg/ssh/cmd/branch.go
function branchCommand (line 15) | func branchCommand() *cobra.Command {
function branchListCommand (line 30) | func branchListCommand() *cobra.Command {
function branchDefaultCommand (line 62) | func branchDefaultCommand() *cobra.Command {
function branchDeleteCommand (line 144) | func branchDeleteCommand() *cobra.Command {
FILE: pkg/ssh/cmd/cmd.go
constant UsageTemplate (line 31) | UsageTemplate = `Usage:{{if .Runnable}}
function UsageFunc (line 65) | func UsageFunc(c *cobra.Command) error {
function trimRightSpace (line 94) | func trimRightSpace(s string) string {
function rpad (line 99) | func rpad(s string, padding int) string {
function CommandName (line 105) | func CommandName(args []string) string {
function checkIfReadable (line 112) | func checkIfReadable(cmd *cobra.Command, args []string) error {
function IsPublicKeyAdmin (line 131) | func IsPublicKeyAdmin(cfg *config.Config, pk ssh.PublicKey) bool {
function checkIfAdmin (line 140) | func checkIfAdmin(cmd *cobra.Command, args []string) error {
function checkIfCollab (line 172) | func checkIfCollab(cmd *cobra.Command, args []string) error {
function checkIfReadableAndCollab (line 189) | func checkIfReadableAndCollab(cmd *cobra.Command, args []string) error {
FILE: pkg/ssh/cmd/collab.go
function collabCommand (line 9) | func collabCommand() *cobra.Command {
function collabAddCommand (line 25) | func collabAddCommand() *cobra.Command {
function collabRemoveCommand (line 52) | func collabRemoveCommand() *cobra.Command {
function collabListCommand (line 71) | func collabListCommand() *cobra.Command {
FILE: pkg/ssh/cmd/commit.go
function commitCommand (line 18) | func commitCommand() *cobra.Command {
function renderDiff (line 111) | func renderDiff(patch string, color bool) string {
function renderStats (line 137) | func renderStats(diff *git.Diff, commonStyle *styles.Styles, color bool)...
FILE: pkg/ssh/cmd/create.go
function createCommand (line 13) | func createCommand() *cobra.Command {
FILE: pkg/ssh/cmd/delete.go
function deleteCommand (line 8) | func deleteCommand() *cobra.Command {
FILE: pkg/ssh/cmd/description.go
function descriptionCommand (line 10) | func descriptionCommand() *cobra.Command {
FILE: pkg/ssh/cmd/git.go
function GitUploadPackCommand (line 103) | func GitUploadPackCommand() *cobra.Command {
function GitUploadArchiveCommand (line 116) | func GitUploadArchiveCommand() *cobra.Command {
function GitReceivePackCommand (line 129) | func GitReceivePackCommand() *cobra.Command {
function GitLFSAuthenticateCommand (line 142) | func GitLFSAuthenticateCommand() *cobra.Command {
function GitLFSTransfer (line 155) | func GitLFSTransfer() *cobra.Command {
function gitRunE (line 167) | func gitRunE(cmd *cobra.Command, args []string) error {
FILE: pkg/ssh/cmd/hidden.go
function hiddenCommand (line 8) | func hiddenCommand() *cobra.Command {
FILE: pkg/ssh/cmd/import.go
function importCommand (line 13) | func importCommand() *cobra.Command {
FILE: pkg/ssh/cmd/info.go
function InfoCommand (line 10) | func InfoCommand() *cobra.Command {
FILE: pkg/ssh/cmd/jwt.go
function JWTCommand (line 15) | func JWTCommand() *cobra.Command {
FILE: pkg/ssh/cmd/list.go
function listCommand (line 11) | func listCommand() *cobra.Command {
FILE: pkg/ssh/cmd/mirror.go
function mirrorCommand (line 8) | func mirrorCommand() *cobra.Command {
FILE: pkg/ssh/cmd/private.go
function privateCommand (line 11) | func privateCommand() *cobra.Command {
FILE: pkg/ssh/cmd/project_name.go
function projectName (line 10) | func projectName() *cobra.Command {
FILE: pkg/ssh/cmd/pubkey.go
function PubkeyCommand (line 12) | func PubkeyCommand() *cobra.Command {
FILE: pkg/ssh/cmd/rename.go
function renameCommand (line 8) | func renameCommand() *cobra.Command {
FILE: pkg/ssh/cmd/repo.go
function RepoCommand (line 13) | func RepoCommand() *cobra.Command {
FILE: pkg/ssh/cmd/set_username.go
function SetUsernameCommand (line 10) | func SetUsernameCommand() *cobra.Command {
FILE: pkg/ssh/cmd/settings.go
function SettingsCommand (line 13) | func SettingsCommand() *cobra.Command {
FILE: pkg/ssh/cmd/tag.go
function tagCommand (line 14) | func tagCommand() *cobra.Command {
function tagListCommand (line 28) | func tagListCommand() *cobra.Command {
function tagDeleteCommand (line 61) | func tagDeleteCommand() *cobra.Command {
FILE: pkg/ssh/cmd/token.go
function TokenCommand (line 17) | func TokenCommand() *cobra.Command {
FILE: pkg/ssh/cmd/tree.go
function treeCommand (line 15) | func treeCommand() *cobra.Command {
FILE: pkg/ssh/cmd/user.go
function UserCommand (line 15) | func UserCommand() *cobra.Command {
FILE: pkg/ssh/cmd/webhooks.go
function webhookCommand (line 17) | func webhookCommand() *cobra.Command {
function init (line 37) | func init() {
function webhookListCommand (line 45) | func webhookListCommand() *cobra.Command {
function webhookCreateCommand (line 88) | func webhookCreateCommand() *cobra.Command {
function webhookDeleteCommand (line 139) | func webhookDeleteCommand() *cobra.Command {
function webhookUpdateCommand (line 165) | func webhookUpdateCommand() *cobra.Command {
function webhookDeliveriesCommand (line 256) | func webhookDeliveriesCommand() *cobra.Command {
function webhookDeliveriesListCommand (line 272) | func webhookDeliveriesListCommand() *cobra.Command {
function webhookDeliveriesRedeliverCommand (line 312) | func webhookDeliveriesRedeliverCommand() *cobra.Command {
function webhookDeliveriesGetCommand (line 343) | func webhookDeliveriesGetCommand() *cobra.Command {
FILE: pkg/ssh/middleware.go
function AuthenticationMiddleware (line 28) | func AuthenticationMiddleware(sh ssh.Handler) ssh.Handler {
function ContextMiddleware (line 77) | func ContextMiddleware(cfg *config.Config, dbx *db.DB, datastore store.S...
function CommandMiddleware (line 101) | func CommandMiddleware(sh ssh.Handler) ssh.Handler {
function LoggingMiddleware (line 166) | func LoggingMiddleware(sh ssh.Handler) ssh.Handler {
FILE: pkg/ssh/middleware_test.go
function TestAuthenticationBypass (line 35) | func TestAuthenticationBypass(t *testing.T) {
type mockSSHContext (line 152) | type mockSSHContext struct
method SetValue (line 158) | func (m *mockSSHContext) SetValue(key, value any) {
method Value (line 162) | func (m *mockSSHContext) Value(key any) any {
method Permissions (line 169) | func (m *mockSSHContext) Permissions() *ssh.Permissions {
method User (line 173) | func (m *mockSSHContext) User() string { return "" }
method RemoteAddr (line 174) | func (m *mockSSHContext) RemoteAddr() net.Addr { return &net.TCPAddr{} }
method LocalAddr (line 175) | func (m *mockSSHContext) LocalAddr() net.Addr { return &net.TCPAddr{} }
method ServerVersion (line 176) | func (m *mockSSHContext) ServerVersion() string { return "" }
method ClientVersion (line 177) | func (m *mockSSHContext) ClientVersion() string { return "" }
method SessionID (line 178) | func (m *mockSSHContext) SessionID() string { return "" }
method Lock (line 179) | func (m *mockSSHContext) Lock() {}
method Unlock (line 180) | func (m *mockSSHContext) Unlock() {}
FILE: pkg/ssh/session.go
function SessionHandler (line 35) | func SessionHandler(s ssh.Session) *tea.Program {
FILE: pkg/ssh/session_test.go
function TestSession (line 27) | func TestSession(t *testing.T) {
function setup (line 50) | func setup(tb testing.TB) (*gossh.Session, func() error) {
FILE: pkg/ssh/ssh.go
type SSHServer (line 43) | type SSHServer struct
method ListenAndServe (line 133) | func (s *SSHServer) ListenAndServe() error {
method Serve (line 138) | func (s *SSHServer) Serve(l net.Listener) error {
method Close (line 143) | func (s *SSHServer) Close() error {
method Shutdown (line 148) | func (s *SSHServer) Shutdown(ctx context.Context) error {
method PublicKeyHandler (line 163) | func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey...
method KeyboardInteractiveHandler (line 184) | func (s *SSHServer) KeyboardInteractiveHandler(ctx ssh.Context, _ goss...
function NewSSHServer (line 52) | func NewSSHServer(ctx context.Context) (*SSHServer, error) {
function initializePermissions (line 152) | func initializePermissions(ctx ssh.Context) {
FILE: pkg/ssh/ui.go
type page (line 20) | type page
constant selectionPage (line 23) | selectionPage page = iota
constant repoPage (line 24) | repoPage
type sessionState (line 27) | type sessionState
constant loadingState (line 30) | loadingState sessionState = iota
constant errorState (line 31) | errorState
constant readyState (line 32) | readyState
type UI (line 36) | type UI struct
method getMargins (line 67) | func (ui *UI) getMargins() (wm, hm int) {
method ShortHelp (line 87) | func (ui *UI) ShortHelp() []key.Binding {
method FullHelp (line 103) | func (ui *UI) FullHelp() [][]key.Binding {
method SetSize (line 122) | func (ui *UI) SetSize(width, height int) {
method Init (line 135) | func (ui *UI) Init() tea.Cmd {
method IsFiltering (line 159) | func (ui *UI) IsFiltering() bool {
method Update (line 169) | func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 259) | func (ui *UI) View() tea.View {
method openRepo (line 297) | func (ui *UI) openRepo(rn string) (proto.Repository, error) {
method setRepoCmd (line 318) | func (ui *UI) setRepoCmd(rn string) tea.Cmd {
method initialRepoCmd (line 328) | func (ui *UI) initialRepoCmd(rn string) tea.Cmd {
function NewUI (line 50) | func NewUI(c common.Common, initialRepo string) *UI {
FILE: pkg/sshutils/utils.go
function ParseAuthorizedKey (line 12) | func ParseAuthorizedKey(ak string) (gossh.PublicKey, string, error) {
function MarshalAuthorizedKey (line 22) | func MarshalAuthorizedKey(pk gossh.PublicKey) string {
function KeysEqual (line 30) | func KeysEqual(a, b gossh.PublicKey) bool {
function PublicKeyFromContext (line 35) | func PublicKeyFromContext(ctx context.Context) gossh.PublicKey {
function SessionFromContext (line 46) | func SessionFromContext(ctx context.Context) ssh.Session {
FILE: pkg/sshutils/utils_test.go
function generateKeys (line 10) | func generateKeys(tb testing.TB) (*keygen.SSHKeyPair, *keygen.SSHKeyPair) {
function TestParseAuthorizedKey (line 23) | func TestParseAuthorizedKey(t *testing.T) {
function TestMarshalAuthorizedKey (line 57) | func TestMarshalAuthorizedKey(t *testing.T) {
function TestKeysEqual (line 84) | func TestKeysEqual(t *testing.T) {
FILE: pkg/ssrf/ssrf.go
function NewSecureClient (line 30) | func NewSecureClient() *http.Client {
function isPrivateOrInternal (line 77) | func isPrivateOrInternal(ip net.IP) bool {
function ValidateURL (line 130) | func ValidateURL(rawURL string) error {
function ValidateIPBeforeDial (line 176) | func ValidateIPBeforeDial(ip net.IP) error {
function isLocalhost (line 184) | func isLocalhost(hostname string) bool {
FILE: pkg/ssrf/ssrf_test.go
function TestNewSecureClientBlocksPrivateIPs (line 13) | func TestNewSecureClientBlocksPrivateIPs(t *testing.T) {
function TestNewSecureClientBlocksPrivateHostnames (line 52) | func TestNewSecureClientBlocksPrivateHostnames(t *testing.T) {
function TestNewSecureClientNilIPNotErrPrivateIP (line 71) | func TestNewSecureClientNilIPNotErrPrivateIP(t *testing.T) {
function TestNewSecureClientBlocksRedirects (line 90) | func TestNewSecureClientBlocksRedirects(t *testing.T) {
function TestIsPrivateOrInternal (line 117) | func TestIsPrivateOrInternal(t *testing.T) {
function TestValidateURL (line 165) | func TestValidateURL(t *testing.T) {
function TestIsLocalhost (line 208) | func TestIsLocalhost(t *testing.T) {
FILE: pkg/stats/stats.go
type StatsServer (line 13) | type StatsServer struct
method ListenAndServe (line 39) | func (s *StatsServer) ListenAndServe() error {
method Shutdown (line 44) | func (s *StatsServer) Shutdown(ctx context.Context) error {
method Close (line 49) | func (s *StatsServer) Close() error {
function NewStatsServer (line 20) | func NewStatsServer(ctx context.Context) (*StatsServer, error) {
FILE: pkg/storage/local.go
type LocalStorage (line 14) | type LocalStorage struct
method Delete (line 26) | func (l *LocalStorage) Delete(name string) error {
method Open (line 32) | func (l *LocalStorage) Open(name string) (Object, error) {
method Stat (line 38) | func (l *LocalStorage) Stat(name string) (fs.FileInfo, error) {
method Put (line 44) | func (l *LocalStorage) Put(name string, r io.Reader) (int64, error) {
method Exists (line 59) | func (l *LocalStorage) Exists(name string) (bool, error) {
method Rename (line 72) | func (l *LocalStorage) Rename(oldName, newName string) error {
method fixPath (line 83) | func (l LocalStorage) fixPath(path string) string {
function NewLocalStorage (line 21) | func NewLocalStorage(root string) *LocalStorage {
FILE: pkg/storage/storage.go
type Object (line 9) | type Object interface
type Storage (line 16) | type Storage interface
FILE: pkg/store/access_token.go
type AccessTokenStore (line 12) | type AccessTokenStore interface
FILE: pkg/store/collab.go
type CollaboratorStore (line 12) | type CollaboratorStore interface
FILE: pkg/store/context.go
function FromContext (line 9) | func FromContext(ctx context.Context) Store {
function WithContext (line 18) | func WithContext(ctx context.Context, s Store) context.Context {
FILE: pkg/store/database/access_token.go
type accessTokenStore (line 12) | type accessTokenStore struct
method CreateAccessToken (line 17) | func (s *accessTokenStore) CreateAccessToken(ctx context.Context, h db...
method DeleteAccessToken (line 39) | func (*accessTokenStore) DeleteAccessToken(ctx context.Context, h db.H...
method DeleteAccessTokenForUser (line 46) | func (*accessTokenStore) DeleteAccessTokenForUser(ctx context.Context,...
method GetAccessToken (line 53) | func (*accessTokenStore) GetAccessToken(ctx context.Context, h db.Hand...
method GetAccessTokensByUserID (line 61) | func (*accessTokenStore) GetAccessTokensByUserID(ctx context.Context, ...
method GetAccessTokenByToken (line 69) | func (*accessTokenStore) GetAccessTokenByToken(ctx context.Context, h ...
FILE: pkg/store/database/collab.go
type collabStore (line 14) | type collabStore struct
method AddCollabByUsernameAndRepo (line 19) | func (*collabStore) AddCollabByUsernameAndRepo(ctx context.Context, tx...
method GetCollabByUsernameAndRepo (line 43) | func (*collabStore) GetCollabByUsernameAndRepo(ctx context.Context, tx...
method ListCollabsByRepo (line 68) | func (*collabStore) ListCollabsByRepo(ctx context.Context, tx db.Handl...
method ListCollabsByRepoAsUsers (line 87) | func (*collabStore) ListCollabsByRepoAsUsers(ctx context.Context, tx d...
method RemoveCollabByUsernameAndRepo (line 107) | func (*collabStore) RemoveCollabByUsernameAndRepo(ctx context.Context,...
FILE: pkg/store/database/database.go
type datastore (line 12) | type datastore struct
function New (line 28) | func New(ctx context.Context, db *db.DB) store.Store {
FILE: pkg/store/database/lfs.go
type lfsStore (line 12) | type lfsStore struct
method CreateLFSLockForUser (line 23) | func (*lfsStore) CreateLFSLockForUser(ctx context.Context, tx db.Handl...
method GetLFSLocks (line 39) | func (*lfsStore) GetLFSLocks(ctx context.Context, tx db.Handler, repoI...
method GetLFSLocksWithCount (line 56) | func (s *lfsStore) GetLFSLocksWithCount(ctx context.Context, tx db.Han...
method GetLFSLocksForUser (line 77) | func (*lfsStore) GetLFSLocksForUser(ctx context.Context, tx db.Handler...
method GetLFSLockForPath (line 89) | func (*lfsStore) GetLFSLockForPath(ctx context.Context, tx db.Handler,...
method GetLFSLockForUserPath (line 102) | func (*lfsStore) GetLFSLockForUserPath(ctx context.Context, tx db.Hand...
method GetLFSLockByID (line 115) | func (*lfsStore) GetLFSLockByID(ctx context.Context, tx db.Handler, id...
method GetLFSLockForUserByID (line 127) | func (*lfsStore) GetLFSLockForUserByID(ctx context.Context, tx db.Hand...
method DeleteLFSLockForUserByID (line 139) | func (*lfsStore) DeleteLFSLockForUserByID(ctx context.Context, tx db.H...
method DeleteLFSLock (line 149) | func (*lfsStore) DeleteLFSLock(ctx context.Context, tx db.Handler, rep...
method CreateLFSObject (line 159) | func (*lfsStore) CreateLFSObject(ctx context.Context, tx db.Handler, r...
method DeleteLFSObjectByOid (line 166) | func (*lfsStore) DeleteLFSObjectByOid(ctx context.Context, tx db.Handl...
method GetLFSObjectByOid (line 173) | func (*lfsStore) GetLFSObjectByOid(ctx context.Context, tx db.Handler,...
method GetLFSObjects (line 181) | func (*lfsStore) GetLFSObjects(ctx context.Context, tx db.Handler, rep...
method GetLFSObjectsByName (line 189) | func (*lfsStore) GetLFSObjectsByName(ctx context.Context, tx db.Handle...
function sanitizePath (line 16) | func sanitizePath(path string) string {
FILE: pkg/store/database/repo.go
type repoStore (line 12) | type repoStore struct
method CreateRepo (line 17) | func (*repoStore) CreateRepo(ctx context.Context, tx db.Handler, name ...
method DeleteRepoByName (line 36) | func (*repoStore) DeleteRepoByName(ctx context.Context, tx db.Handler,...
method GetAllRepos (line 44) | func (*repoStore) GetAllRepos(ctx context.Context, tx db.Handler) ([]m...
method GetUserRepos (line 52) | func (*repoStore) GetUserRepos(ctx context.Context, tx db.Handler, use...
method GetRepoByName (line 60) | func (*repoStore) GetRepoByName(ctx context.Context, tx db.Handler, na...
method GetRepoDescriptionByName (line 69) | func (*repoStore) GetRepoDescriptionByName(ctx context.Context, tx db....
method GetRepoIsHiddenByName (line 78) | func (*repoStore) GetRepoIsHiddenByName(ctx context.Context, tx db.Han...
method GetRepoIsMirrorByName (line 87) | func (*repoStore) GetRepoIsMirrorByName(ctx context.Context, tx db.Han...
method GetRepoIsPrivateByName (line 96) | func (*repoStore) GetRepoIsPrivateByName(ctx context.Context, tx db.Ha...
method GetRepoProjectNameByName (line 105) | func (*repoStore) GetRepoProjectNameByName(ctx context.Context, tx db....
method SetRepoDescriptionByName (line 114) | func (*repoStore) SetRepoDescriptionByName(ctx context.Context, tx db....
method SetRepoIsHiddenByName (line 122) | func (*repoStore) SetRepoIsHiddenByName(ctx context.Context, tx db.Han...
method SetRepoIsPrivateByName (line 130) | func (*repoStore) SetRepoIsPrivateByName(ctx context.Context, tx db.Ha...
method SetRepoNameByName (line 138) | func (*repoStore) SetRepoNameByName(ctx context.Context, tx db.Handler...
method SetRepoProjectNameByName (line 147) | func (*repoStore) SetRepoProjectNameByName(ctx context.Context, tx db....
FILE: pkg/store/database/settings.go
type settingsStore (line 11) | type settingsStore struct
method GetAllowKeylessAccess (line 16) | func (*settingsStore) GetAllowKeylessAccess(ctx context.Context, tx db...
method GetAnonAccess (line 26) | func (*settingsStore) GetAnonAccess(ctx context.Context, tx db.Handler...
method SetAllowKeylessAccess (line 36) | func (*settingsStore) SetAllowKeylessAccess(ctx context.Context, tx db...
method SetAnonAccess (line 43) | func (*settingsStore) SetAnonAccess(ctx context.Context, tx db.Handler...
FILE: pkg/store/database/user.go
type userStore (line 15) | type userStore struct
method AddPublicKeyByUsername (line 20) | func (*userStore) AddPublicKeyByUsername(ctx context.Context, tx db.Ha...
method CreateUser (line 40) | func (*userStore) CreateUser(ctx context.Context, tx db.Handler, usern...
method DeleteUserByUsername (line 68) | func (*userStore) DeleteUserByUsername(ctx context.Context, tx db.Hand...
method GetUserByID (line 80) | func (*userStore) GetUserByID(ctx context.Context, tx db.Handler, id i...
method FindUserByPublicKey (line 88) | func (*userStore) FindUserByPublicKey(ctx context.Context, tx db.Handl...
method FindUserByUsername (line 99) | func (*userStore) FindUserByUsername(ctx context.Context, tx db.Handle...
method FindUserByAccessToken (line 112) | func (*userStore) FindUserByAccessToken(ctx context.Context, tx db.Han...
method GetAllUsers (line 123) | func (*userStore) GetAllUsers(ctx context.Context, tx db.Handler) ([]m...
method ListPublicKeysByUserID (line 131) | func (*userStore) ListPublicKeysByUserID(ctx context.Context, tx db.Ha...
method ListPublicKeysByUsername (line 154) | func (*userStore) ListPublicKeysByUsername(ctx context.Context, tx db....
method RemovePublicKeyByUsername (line 183) | func (*userStore) RemovePublicKeyByUsername(ctx context.Context, tx db...
method SetAdminByUsername (line 197) | func (*userStore) SetAdminByUsername(ctx context.Context, tx db.Handle...
method SetUsernameByUsername (line 209) | func (*userStore) SetUsernameByUsername(ctx context.Context, tx db.Han...
method SetUserPassword (line 226) | func (*userStore) SetUserPassword(ctx context.Context, tx db.Handler, ...
method SetUserPasswordByUsername (line 233) | func (*userStore) SetUserPasswordByUsername(ctx context.Context, tx db...
FILE: pkg/store/database/webhooks.go
type webhookStore (line 13) | type webhookStore struct
method CreateWebhook (line 18) | func (*webhookStore) CreateWebhook(ctx context.Context, h db.Handler, ...
method CreateWebhookDelivery (line 31) | func (*webhookStore) CreateWebhookDelivery(ctx context.Context, h db.H...
method CreateWebhookEvents (line 43) | func (*webhookStore) CreateWebhookEvents(ctx context.Context, h db.Han...
method DeleteWebhookByID (line 56) | func (*webhookStore) DeleteWebhookByID(ctx context.Context, h db.Handl...
method DeleteWebhookForRepoByID (line 63) | func (*webhookStore) DeleteWebhookForRepoByID(ctx context.Context, h d...
method DeleteWebhookDeliveryByID (line 70) | func (*webhookStore) DeleteWebhookDeliveryByID(ctx context.Context, h ...
method DeleteWebhookEventsByID (line 77) | func (*webhookStore) DeleteWebhookEventsByID(ctx context.Context, h db...
method GetWebhookByID (line 89) | func (*webhookStore) GetWebhookByID(ctx context.Context, h db.Handler,...
method GetWebhookDeliveriesByWebhookID (line 97) | func (*webhookStore) GetWebhookDeliveriesByWebhookID(ctx context.Conte...
method GetWebhookDeliveryByID (line 105) | func (*webhookStore) GetWebhookDeliveryByID(ctx context.Context, h db....
method GetWebhookEventByID (line 113) | func (*webhookStore) GetWebhookEventByID(ctx context.Context, h db.Han...
method GetWebhookEventsByWebhookID (line 121) | func (*webhookStore) GetWebhookEventsByWebhookID(ctx context.Context, ...
method GetWebhooksByRepoID (line 129) | func (*webhookStore) GetWebhooksByRepoID(ctx context.Context, h db.Han...
method GetWebhooksByRepoIDWhereEvent (line 137) | func (*webhookStore) GetWebhooksByRepoIDWhereEvent(ctx context.Context...
method ListWebhookDeliveriesByWebhookID (line 153) | func (*webhookStore) ListWebhookDeliveriesByWebhookID(ctx context.Cont...
method UpdateWebhookByID (line 161) | func (*webhookStore) UpdateWebhookByID(ctx context.Context, h db.Handl...
FILE: pkg/store/lfs.go
type LFSStore (line 11) | type LFSStore interface
FILE: pkg/store/repo.go
type RepositoryStore (line 11) | type RepositoryStore interface
FILE: pkg/store/settings.go
type SettingStore (line 11) | type SettingStore interface
FILE: pkg/store/store.go
type Store (line 4) | type Store interface
FILE: pkg/store/user.go
type UserStore (line 12) | type UserStore interface
FILE: pkg/store/webhooks.go
type WebhookStore (line 12) | type WebhookStore interface
FILE: pkg/sync/workqueue.go
type WorkPool (line 11) | type WorkPool struct
method Run (line 52) | func (wq *WorkPool) Run() {
method Add (line 77) | func (wq *WorkPool) Add(id string, fn func()) {
method Status (line 85) | func (wq *WorkPool) Status(id string) bool {
method logf (line 90) | func (wq *WorkPool) logf(format string, args ...interface{}) {
type WorkPoolOption (line 20) | type WorkPoolOption
function WithWorkPoolLogger (line 23) | func WithWorkPoolLogger(logger func(string, ...interface{})) WorkPoolOpt...
function NewWorkPool (line 32) | func NewWorkPool(ctx context.Context, workers int, opts ...WorkPoolOptio...
FILE: pkg/sync/workqueue_test.go
function TestWorkPool (line 10) | func TestWorkPool(t *testing.T) {
FILE: pkg/task/manager.go
type Task (line 19) | type Task struct
type Manager (line 29) | type Manager struct
method Add (line 44) | func (m *Manager) Add(id string, fn func(context.Context) error) {
method Stop (line 59) | func (m *Manager) Stop(id string) error {
method Exists (line 73) | func (m *Manager) Exists(id string) bool {
method Run (line 80) | func (m *Manager) Run(id string, done chan<- error) {
function NewManager (line 35) | func NewManager(ctx context.Context) *Manager {
FILE: pkg/test/test.go
function RandomPort (line 15) | func RandomPort() int {
FILE: pkg/ui/common/common.go
type contextKey (line 18) | type contextKey struct
type Common (line 29) | type Common struct
method SetValue (line 56) | func (c *Common) SetValue(key, value interface{}) {
method SetSize (line 61) | func (c *Common) SetSize(width, height int) {
method Context (line 67) | func (c *Common) Context() context.Context {
method Config (line 72) | func (c *Common) Config() *config.Config {
method Backend (line 77) | func (c *Common) Backend() *backend.Backend {
method Repo (line 82) | func (c *Common) Repo() *git.Repository {
method PublicKey (line 91) | func (c *Common) PublicKey() ssh.PublicKey {
method CloneCmd (line 100) | func (c *Common) CloneCmd(publicURL, name string) string {
function NewCommon (line 40) | func NewCommon(ctx context.Context, width, height int) Common {
function IsFileMarkdown (line 109) | func IsFileMarkdown(content, ext string) bool {
function ScrollPercent (line 123) | func ScrollPercent(position int) string {
FILE: pkg/ui/common/common_test.go
function TestIsFileMarkdown (line 9) | func TestIsFileMarkdown(t *testing.T) {
FILE: pkg/ui/common/component.go
type Model (line 9) | type Model interface
type Component (line 16) | type Component interface
type TabComponent (line 24) | type TabComponent interface
FILE: pkg/ui/common/error.go
type ErrorMsg (line 13) | type ErrorMsg
function ErrorCmd (line 16) | func ErrorCmd(err error) tea.Cmd {
FILE: pkg/ui/common/format.go
function FormatLineNumber (line 14) | func FormatLineNumber(styles *styles.Styles, s string, color bool) (stri...
function FormatHighlight (line 37) | func FormatHighlight(p, c string) (string, error) {
function UnquoteFilename (line 63) | func UnquoteFilename(s string) string {
FILE: pkg/ui/common/style.go
function strptr (line 12) | func strptr(s string) *string {
function StyleConfig (line 17) | func StyleConfig() gansi.StyleConfig {
function StyleRenderer (line 27) | func StyleRenderer() gansi.RenderContext {
function StyleRendererWithStyles (line 32) | func StyleRendererWithStyles(styles gansi.StyleConfig) gansi.RenderConte...
FILE: pkg/ui/common/utils.go
function TruncateString (line 12) | func TruncateString(s string, max int) string { //nolint:revive
function RepoURL (line 20) | func RepoURL(publicURL, name string) string {
FILE: pkg/ui/components/code/code.go
constant defaultTabWidth (line 18) | defaultTabWidth = 4
constant defaultSideNotePercent (line 19) | defaultSideNotePercent = 0.3
type Code (line 23) | type Code struct
method SetSize (line 59) | func (r *Code) SetSize(width, height int) {
method SetContent (line 65) | func (r *Code) SetContent(c, ext string) tea.Cmd {
method SetSideNote (line 72) | func (r *Code) SetSideNote(s string) tea.Cmd {
method Init (line 78) | func (r *Code) Init() tea.Cmd {
method Update (line 135) | func (r *Code) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 151) | func (r *Code) View() string {
method GotoTop (line 156) | func (r *Code) GotoTop() {
method GotoBottom (line 161) | func (r *Code) GotoBottom() {
method HalfViewDown (line 166) | func (r *Code) HalfViewDown() {
method HalfViewUp (line 171) | func (r *Code) HalfViewUp() {
method ScrollPercent (line 176) | func (r *Code) ScrollPercent() float64 {
method ScrollPosition (line 181) | func (r *Code) ScrollPosition() int {
method glamourize (line 189) | func (r *Code) glamourize(w int, md string) (string, error) {
method renderFile (line 209) | func (r *Code) renderFile(path, content string) (string, error) {
function New (line 41) | func New(c common.Common, content, extension string) *Code {
FILE: pkg/ui/components/footer/footer.go
type ToggleFooterMsg (line 12) | type ToggleFooterMsg struct
type Footer (line 15) | type Footer struct
method SetSize (line 38) | func (f *Footer) SetSize(width, height int) {
method Init (line 45) | func (f *Footer) Init() tea.Cmd {
method Update (line 50) | func (f *Footer) Update(_ tea.Msg) (common.Model, tea.Cmd) {
method View (line 55) | func (f *Footer) View() string {
method ShortHelp (line 69) | func (f *Footer) ShortHelp() []key.Binding {
method FullHelp (line 74) | func (f *Footer) FullHelp() [][]key.Binding {
method ShowAll (line 79) | func (f *Footer) ShowAll() bool {
method SetShowAll (line 84) | func (f *Footer) SetShowAll(show bool) {
method Height (line 89) | func (f *Footer) Height() int {
function New (line 22) | func New(c common.Common, keymap help.KeyMap) *Footer {
function ToggleFooterCmd (line 94) | func ToggleFooterCmd() tea.Msg {
FILE: pkg/ui/components/header/header.go
type Header (line 11) | type Header struct
method SetSize (line 25) | func (h *Header) SetSize(width, height int) {
method Init (line 30) | func (h *Header) Init() tea.Cmd {
method Update (line 35) | func (h *Header) Update(_ tea.Msg) (common.Model, tea.Cmd) {
method View (line 40) | func (h *Header) View() string {
function New (line 17) | func New(c common.Common, text string) *Header {
FILE: pkg/ui/components/selector/selector.go
type Selector (line 13) | type Selector struct
method PerPage (line 61) | func (s *Selector) PerPage() int {
method SetPage (line 68) | func (s *Selector) SetPage(page int) {
method Page (line 75) | func (s *Selector) Page() int {
method TotalPages (line 82) | func (s *Selector) TotalPages() int {
method SetTotalPages (line 89) | func (s *Selector) SetTotalPages(items int) int {
method SelectedItem (line 96) | func (s *Selector) SelectedItem() IdentifiableItem {
method Select (line 108) | func (s *Selector) Select(index int) {
method SetShowTitle (line 115) | func (s *Selector) SetShowTitle(show bool) {
method SetShowHelp (line 122) | func (s *Selector) SetShowHelp(show bool) {
method SetShowStatusBar (line 129) | func (s *Selector) SetShowStatusBar(show bool) {
method DisableQuitKeybindings (line 136) | func (s *Selector) DisableQuitKeybindings() {
method SetShowFilter (line 143) | func (s *Selector) SetShowFilter(show bool) {
method SetShowPagination (line 150) | func (s *Selector) SetShowPagination(show bool) {
method SetFilteringEnabled (line 157) | func (s *Selector) SetFilteringEnabled(enabled bool) {
method SetSize (line 164) | func (s *Selector) SetSize(width, height int) {
method SetItems (line 172) | func (s *Selector) SetItems(items []IdentifiableItem) tea.Cmd {
method Index (line 183) | func (s *Selector) Index() int {
method Items (line 190) | func (s *Selector) Items() []list.Item {
method VisibleItems (line 197) | func (s *Selector) VisibleItems() []list.Item {
method FilterState (line 204) | func (s *Selector) FilterState() list.FilterState {
method CursorUp (line 211) | func (s *Selector) CursorUp() {
method CursorDown (line 218) | func (s *Selector) CursorDown() {
method Init (line 225) | func (s *Selector) Init() tea.Cmd {
method Update (line 230) | func (s *Selector) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 292) | func (s *Selector) View() string {
method SelectItemCmd (line 297) | func (s *Selector) SelectItemCmd() tea.Msg {
method activeCmd (line 301) | func (s *Selector) activeCmd() tea.Msg {
method activeFilterCmd (line 306) | func (s *Selector) activeFilterCmd() tea.Msg {
type IdentifiableItem (line 28) | type IdentifiableItem interface
type ItemDelegate (line 34) | type ItemDelegate interface
type SelectMsg (line 39) | type SelectMsg struct
type ActiveMsg (line 42) | type ActiveMsg struct
function New (line 45) | func New(common common.Common, items []IdentifiableItem, delegate ItemDe...
FILE: pkg/ui/components/statusbar/statusbar.go
type Model (line 11) | type Model struct
method SetSize (line 28) | func (s *Model) SetSize(width, height int) {
method SetStatus (line 34) | func (s *Model) SetStatus(key, value, info, extra string) {
method Init (line 50) | func (s *Model) Init() tea.Cmd {
method Update (line 55) | func (s *Model) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 64) | func (s *Model) View() string {
function New (line 20) | func New(c common.Common) *Model {
FILE: pkg/ui/components/tabs/tabs.go
type SelectTabMsg (line 12) | type SelectTabMsg
type ActiveTabMsg (line 15) | type ActiveTabMsg
type Tabs (line 18) | type Tabs struct
method SetSize (line 43) | func (t *Tabs) SetSize(width, height int) {
method Init (line 48) | func (t *Tabs) Init() tea.Cmd {
method Update (line 54) | func (t *Tabs) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 86) | func (t *Tabs) View() string {
method activeTabCmd (line 114) | func (t *Tabs) activeTabCmd() tea.Msg {
function New (line 30) | func New(c common.Common, tabs []string) *Tabs {
function SelectTabCmd (line 119) | func SelectTabCmd(tab int) tea.Cmd {
FILE: pkg/ui/components/viewport/viewport.go
type Viewport (line 11) | type Viewport struct
method SetSize (line 29) | func (v *Viewport) SetSize(width, height int) {
method Init (line 36) | func (v *Viewport) Init() tea.Cmd {
method Update (line 41) | func (v *Viewport) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 57) | func (v *Viewport) View() string {
method SetContent (line 62) | func (v *Viewport) SetContent(content string) {
method GotoTop (line 67) | func (v *Viewport) GotoTop() {
method GotoBottom (line 72) | func (v *Viewport) GotoBottom() {
method HalfViewDown (line 77) | func (v *Viewport) HalfViewDown() {
method HalfViewUp (line 82) | func (v *Viewport) HalfViewUp() {
method ScrollPercent (line 87) | func (v *Viewport) ScrollPercent() float64 {
function New (line 17) | func New(c common.Common) *Viewport {
FILE: pkg/ui/keymap/keymap.go
type KeyMap (line 6) | type KeyMap struct
function DefaultKeyMap (line 29) | func DefaultKeyMap() *KeyMap {
FILE: pkg/ui/pages/repo/empty.go
function defaultEmptyRepoMsg (line 10) | func defaultEmptyRepoMsg(cfg *config.Config, repo string) string {
FILE: pkg/ui/pages/repo/files.go
type filesView (line 20) | type filesView
constant filesViewLoading (line 23) | filesViewLoading filesView = iota
constant filesViewFiles (line 24) | filesViewFiles
constant filesViewContent (line 25) | filesViewContent
type FileItemsMsg (line 50) | type FileItemsMsg
type FileContentMsg (line 53) | type FileContentMsg struct
type FileBlameMsg (line 59) | type FileBlameMsg
type Files (line 62) | type Files struct
method Path (line 108) | func (f *Files) Path() string {
method TabName (line 117) | func (f *Files) TabName() string {
method SetSize (line 122) | func (f *Files) SetSize(width, height int) {
method ShortHelp (line 129) | func (f *Files) ShortHelp() []key.Binding {
method FullHelp (line 151) | func (f *Files) FullHelp() [][]key.Binding {
method Init (line 211) | func (f *Files) Init() tea.Cmd {
method Update (line 223) | func (f *Files) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 354) | func (f *Files) View() string {
method SpinnerID (line 368) | func (f *Files) SpinnerID() int {
method StatusBarValue (line 373) | func (f *Files) StatusBarValue() string {
method StatusBarInfo (line 382) | func (f *Files) StatusBarInfo() string {
method updateFilesCmd (line 393) | func (f *Files) updateFilesCmd() tea.Msg {
method selectTreeCmd (line 424) | func (f *Files) selectTreeCmd() tea.Msg {
method selectFileCmd (line 433) | func (f *Files) selectFileCmd() tea.Msg {
method fetchBlame (line 484) | func (f *Files) fetchBlame() tea.Msg {
method deselectItemCmd (line 529) | func (f *Files) deselectItemCmd() tea.Cmd {
method setItems (line 545) | func (f *Files) setItems(items []selector.IdentifiableItem) tea.Cmd {
function NewFiles (line 81) | func NewFiles(common common.Common) *Files {
function renderBlame (line 498) | func renderBlame(c common.Common, f *FileItem, b *gitm.Blame) string {
FILE: pkg/ui/pages/repo/filesitem.go
type FileItem (line 19) | type FileItem struct
method ID (line 24) | func (i FileItem) ID() string {
method Title (line 29) | func (i FileItem) Title() string {
method Description (line 34) | func (i FileItem) Description() string {
method Mode (line 39) | func (i FileItem) Mode() fs.FileMode {
method FilterValue (line 44) | func (i FileItem) FilterValue() string { return i.Title() }
type FileItems (line 47) | type FileItems
method Len (line 50) | func (cl FileItems) Len() int { return len(cl) }
method Swap (line 53) | func (cl FileItems) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
method Less (line 56) | func (cl FileItems) Less(i, j int) bool {
type FileItemDelegate (line 68) | type FileItemDelegate struct
method Height (line 73) | func (d FileItemDelegate) Height() int { return 1 }
method Spacing (line 76) | func (d FileItemDelegate) Spacing() int { return 0 }
method Update (line 79) | func (d FileItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
method Render (line 95) | func (d FileItemDelegate) Render(w io.Writer, m list.Model, index int,...
FILE: pkg/ui/pages/repo/log.go
type logView (line 25) | type logView
constant logViewLoading (line 28) | logViewLoading logView = iota
constant logViewCommits (line 29) | logViewCommits
constant logViewDiff (line 30) | logViewDiff
type LogCountMsg (line 34) | type LogCountMsg
type LogItemsMsg (line 37) | type LogItemsMsg
type LogCommitMsg (line 40) | type LogCommitMsg
type LogDiffMsg (line 43) | type LogDiffMsg
type Log (line 46) | type Log struct
method Path (line 87) | func (l *Log) Path() string {
method TabName (line 97) | func (l *Log) TabName() string {
method SetSize (line 102) | func (l *Log) SetSize(width, height int) {
method ShortHelp (line 109) | func (l *Log) ShortHelp() []key.Binding {
method FullHelp (line 135) | func (l *Log) FullHelp() [][]key.Binding {
method startLoading (line 185) | func (l *Log) startLoading() tea.Cmd {
method Init (line 192) | func (l *Log) Init() tea.Cmd {
method Update (line 206) | func (l *Log) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 350) | func (l *Log) View() string {
method SpinnerID (line 374) | func (l *Log) SpinnerID() int {
method StatusBarValue (line 379) | func (l *Log) StatusBarValue() string {
method StatusBarInfo (line 399) | func (l *Log) StatusBarInfo() string {
method goBack (line 417) | func (l *Log) goBack() {
method countCommitsCmd (line 424) | func (l *Log) countCommitsCmd() tea.Msg {
method updateCommitsCmd (line 440) | func (l *Log) updateCommitsCmd() tea.Msg {
method selectCommitCmd (line 475) | func (l *Log) selectCommitCmd(commit *git.Commit) tea.Cmd {
method loadDiffCmd (line 481) | func (l *Log) loadDiffCmd() tea.Msg {
method renderCommit (line 498) | func (l *Log) renderCommit(c *git.Commit) string {
method setItems (line 542) | func (l *Log) setItems(items []selector.IdentifiableItem) tea.Cmd {
function NewLog (line 63) | func NewLog(common common.Common) *Log {
function renderSummary (line 512) | func renderSummary(diff *git.Diff, styles *styles.Styles, width int) str...
function renderDiff (line 526) | func renderDiff(diff *git.Diff, width int) string {
FILE: pkg/ui/pages/repo/logitem.go
type LogItem (line 19) | type LogItem struct
method ID (line 24) | func (i LogItem) ID() string {
method Hash (line 29) | func (i LogItem) Hash() string {
method Title (line 34) | func (i LogItem) Title() string {
method Description (line 42) | func (i LogItem) Description() string { return "" }
method FilterValue (line 45) | func (i LogItem) FilterValue() string { return i.Title() }
type LogItemDelegate (line 48) | type LogItemDelegate struct
method Height (line 53) | func (d LogItemDelegate) Height() int { return 2 }
method Spacing (line 56) | func (d LogItemDelegate) Spacing() int { return 1 }
method Update (line 59) | func (d LogItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
method Render (line 75) | func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, ...
FILE: pkg/ui/pages/repo/readme.go
type ReadmeMsg (line 16) | type ReadmeMsg struct
type Readme (line 22) | type Readme struct
method Path (line 48) | func (r *Readme) Path() string {
method TabName (line 53) | func (r *Readme) TabName() string {
method SetSize (line 58) | func (r *Readme) SetSize(width, height int) {
method ShortHelp (line 64) | func (r *Readme) ShortHelp() []key.Binding {
method FullHelp (line 72) | func (r *Readme) FullHelp() [][]key.Binding {
method Init (line 92) | func (r *Readme) Init() tea.Cmd {
method Update (line 98) | func (r *Readme) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 136) | func (r *Readme) View() string {
method SpinnerID (line 144) | func (r *Readme) SpinnerID() int {
method StatusBarValue (line 149) | func (r *Readme) StatusBarValue() string {
method StatusBarInfo (line 158) | func (r *Readme) StatusBarInfo() string {
method updateReadmeCmd (line 162) | func (r *Readme) updateReadmeCmd() tea.Msg {
function NewReadme (line 33) | func NewReadme(common common.Common) *Readme {
FILE: pkg/ui/pages/repo/refs.go
type RefMsg (line 18) | type RefMsg
type RefItemsMsg (line 21) | type RefItemsMsg struct
type Refs (line 27) | type Refs struct
method Path (line 61) | func (r *Refs) Path() string {
method TabName (line 66) | func (r *Refs) TabName() string {
method SetSize (line 77) | func (r *Refs) SetSize(width, height int) {
method ShortHelp (line 83) | func (r *Refs) ShortHelp() []key.Binding {
method FullHelp (line 96) | func (r *Refs) FullHelp() [][]key.Binding {
method Init (line 117) | func (r *Refs) Init() tea.Cmd {
method Update (line 123) | func (r *Refs) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 182) | func (r *Refs) View() string {
method SpinnerID (line 190) | func (r *Refs) SpinnerID() int {
method StatusBarValue (line 195) | func (r *Refs) StatusBarValue() string {
method StatusBarInfo (line 203) | func (r *Refs) StatusBarInfo() string {
method updateItemsCmd (line 211) | func (r *Refs) updateItemsCmd() tea.Msg {
method setItems (line 250) | func (r *Refs) setItems(items []selector.IdentifiableItem) tea.Cmd {
function NewRefs (line 39) | func NewRefs(common common.Common, refPrefix string) *Refs {
function switchRefCmd (line 259) | func switchRefCmd(ref *git.Reference) tea.Cmd {
function UpdateRefCmd (line 266) | func UpdateRefCmd(repo proto.Repository) tea.Cmd {
FILE: pkg/ui/pages/repo/refsitem.go
type RefItem (line 20) | type RefItem struct
method ID (line 27) | func (i RefItem) ID() string {
method Title (line 32) | func (i RefItem) Title() string {
method Description (line 37) | func (i RefItem) Description() string {
method Short (line 42) | func (i RefItem) Short() string {
method FilterValue (line 47) | func (i RefItem) FilterValue() string { return i.Short() }
type RefItems (line 50) | type RefItems
method Len (line 53) | func (cl RefItems) Len() int { return len(cl) }
method Swap (line 56) | func (cl RefItems) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
method Less (line 59) | func (cl RefItems) Less(i, j int) bool {
type RefItemDelegate (line 69) | type RefItemDelegate struct
method Height (line 74) | func (d RefItemDelegate) Height() int { return 1 }
method Spacing (line 77) | func (d RefItemDelegate) Spacing() int { return 0 }
method Update (line 80) | func (d RefItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
method Render (line 96) | func (d RefItemDelegate) Render(w io.Writer, m list.Model, index int, ...
FILE: pkg/ui/pages/repo/repo.go
type state (line 21) | type state
constant loadingState (line 24) | loadingState state = iota
constant readyState (line 25) | readyState
type EmptyRepoMsg (line 29) | type EmptyRepoMsg struct
type CopyURLMsg (line 32) | type CopyURLMsg struct
type RepoMsg (line 35) | type RepoMsg
type GoBackMsg (line 38) | type GoBackMsg struct
type CopyMsg (line 41) | type CopyMsg struct
type SwitchTabMsg (line 47) | type SwitchTabMsg
type Repo (line 50) | type Repo struct
method getMargins (line 87) | func (r *Repo) getMargins() (int, int) {
method SetSize (line 97) | func (r *Repo) SetSize(width, height int) {
method Path (line 108) | func (r *Repo) Path() string {
method commonHelp (line 112) | func (r *Repo) commonHelp() []key.Binding {
method ShortHelp (line 124) | func (r *Repo) ShortHelp() []key.Binding {
method FullHelp (line 131) | func (r *Repo) FullHelp() [][]key.Binding {
method Init (line 139) | func (r *Repo) Init() tea.Cmd {
method Update (line 150) | func (r *Repo) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 291) | func (r *Repo) View() string {
method headerView (line 322) | func (r *Repo) headerView() string {
method setStatusBarInfo (line 360) | func (r *Repo) setStatusBarInfo() {
method updateTabComponent (line 377) | func (r *Repo) updateTabComponent(c common.TabComponent, msg tea.Msg) ...
method updateModels (line 392) | func (r *Repo) updateModels(msg tea.Msg) tea.Cmd {
function New (line 64) | func New(c common.Common, comps ...common.TabComponent) *Repo {
function copyCmd (line 404) | func copyCmd(text, msg string) tea.Cmd {
function goBackCmd (line 413) | func goBackCmd() tea.Msg {
function switchTabCmd (line 417) | func switchTabCmd(m common.TabComponent) tea.Cmd {
function renderLoading (line 423) | func renderLoading(c common.Common, s spinner.Model) string {
FILE: pkg/ui/pages/repo/stash.go
type stashState (line 18) | type stashState
constant stashStateLoading (line 21) | stashStateLoading stashState = iota
constant stashStateList (line 22) | stashStateList
constant stashStatePatch (line 23) | stashStatePatch
type StashListMsg (line 27) | type StashListMsg
type StashPatchMsg (line 30) | type StashPatchMsg struct
type Stash (line 33) | type Stash struct
method Path (line 68) | func (s *Stash) Path() string {
method TabName (line 73) | func (s *Stash) TabName() string {
method SetSize (line 78) | func (s *Stash) SetSize(width, height int) {
method ShortHelp (line 85) | func (s *Stash) ShortHelp() []key.Binding {
method FullHelp (line 94) | func (s *Stash) FullHelp() [][]key.Binding {
method StatusBarValue (line 112) | func (s *Stash) StatusBarValue() string {
method StatusBarInfo (line 122) | func (s *Stash) StatusBarInfo() string {
method SpinnerID (line 138) | func (s *Stash) SpinnerID() int {
method Init (line 143) | func (s *Stash) Init() tea.Cmd {
method Update (line 149) | func (s *Stash) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 238) | func (s *Stash) View() string {
method fetchStash (line 250) | func (s *Stash) fetchStash() tea.Msg {
method fetchStashPatch (line 268) | func (s *Stash) fetchStashPatch() tea.Msg {
function NewStash (line 45) | func NewStash(common common.Common) *Stash {
FILE: pkg/ui/pages/repo/stashitem.go
type StashItem (line 15) | type StashItem struct
method ID (line 18) | func (i StashItem) ID() string {
method Title (line 23) | func (i StashItem) Title() string {
method Description (line 28) | func (i StashItem) Description() string {
method FilterValue (line 33) | func (i StashItem) FilterValue() string { return i.Title() }
type StashItems (line 36) | type StashItems
method Len (line 39) | func (cl StashItems) Len() int { return len(cl) }
method Swap (line 42) | func (cl StashItems) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
method Less (line 45) | func (cl StashItems) Less(i, j int) bool {
type StashItemDelegate (line 50) | type StashItemDelegate struct
method Height (line 55) | func (d StashItemDelegate) Height() int { return 1 }
method Spacing (line 58) | func (d StashItemDelegate) Spacing() int { return 0 }
method Update (line 61) | func (d StashItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
method Render (line 79) | func (d StashItemDelegate) Render(w io.Writer, m list.Model, index int...
FILE: pkg/ui/pages/selection/item.go
type Items (line 22) | type Items
method Len (line 25) | func (it Items) Len() int {
method Less (line 30) | func (it Items) Less(i int, j int) bool {
method Swap (line 44) | func (it Items) Swap(i int, j int) {
type Item (line 49) | type Item struct
method ID (line 74) | func (i Item) ID() string {
method Title (line 79) | func (i Item) Title() string {
method Description (line 89) | func (i Item) Description() string { return strings.TrimSpace(i.repo.D...
method FilterValue (line 92) | func (i Item) FilterValue() string { return i.Title() }
method Command (line 95) | func (i Item) Command() string {
function NewItem (line 56) | func NewItem(c common.Common, repo proto.Repository) (Item, error) {
type ItemDelegate (line 100) | type ItemDelegate struct
method Width (line 116) | func (d ItemDelegate) Width() int {
method Height (line 122) | func (d *ItemDelegate) Height() int {
method Spacing (line 128) | func (d *ItemDelegate) Spacing() int { return 1 }
method Update (line 131) | func (d *ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
method Render (line 152) | func (d *ItemDelegate) Render(w io.Writer, m list.Model, index int, li...
function NewItemDelegate (line 107) | func NewItemDelegate(common *common.Common, activePane *pane) *ItemDeleg...
FILE: pkg/ui/pages/selection/selection.go
constant defaultNoContent (line 20) | defaultNoContent = "No readme found.\n\nCreate a `.soft-serve` repositor...
type pane (line 23) | type pane
method String (line 31) | func (p pane) String() string {
constant selectorPane (line 26) | selectorPane pane = iota
constant readmePane (line 27) | readmePane
constant lastPane (line 28) | lastPane
type Selection (line 39) | type Selection struct
method getMargins (line 80) | func (s *Selection) getMargins() (wm, hm int) {
method FilterState (line 92) | func (s *Selection) FilterState() list.FilterState {
method SetSize (line 97) | func (s *Selection) SetSize(width, height int) {
method IsFiltering (line 106) | func (s *Selection) IsFiltering() bool {
method ShortHelp (line 111) | func (s *Selection) ShortHelp() []key.Binding {
method FullHelp (line 132) | func (s *Selection) FullHelp() [][]key.Binding {
method Init (line 184) | func (s *Selection) Init() tea.Cmd {
method Update (line 239) | func (s *Selection) Update(msg tea.Msg) (common.Model, tea.Cmd) {
method View (line 287) | func (s *Selection) View() string {
function New (line 48) | func New(c common.Common) *Selection {
FILE: pkg/ui/styles/styles.go
type Styles (line 13) | type Styles struct
function DefaultStyles (line 170) | func DefaultStyles() *Styles {
FILE: pkg/utils/utils.go
function SanitizeRepo (line 13) | func SanitizeRepo(repo string) string {
function Sanitize (line 27) | func Sanitize(s string) string {
function ValidateUsername (line 32) | func ValidateUsername(username string) error {
function ValidateRepo (line 51) | func ValidateRepo(repo string) error {
FILE: pkg/utils/utils_test.go
function TestValidateRepo (line 5) | func TestValidateRepo(t *testing.T) {
function TestSanitizeRepo (line 38) | func TestSanitizeRepo(t *testing.T) {
FILE: pkg/web/auth.go
function authenticate (line 18) | func authenticate(r *http.Request) (proto.User, error) {
function parseUsernamePassword (line 34) | func parseUsernamePassword(ctx context.Context, username, password strin...
function parseAuthHdr (line 70) | func parseAuthHdr(r *http.Request) (proto.User, error) {
function parseJWT (line 136) | func parseJWT(ctx context.Context, bearer string) (*jwt.RegisteredClaims...
FILE: pkg/web/context.go
function NewContextHandler (line 16) | func NewContextHandler(ctx context.Context) func(http.Handler) http.Hand...
FILE: pkg/web/git.go
type GitRoute (line 32) | type GitRoute struct
method ServeHTTP (line 41) | func (g GitRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) {
function withParams (line 76) | func withParams(next http.Handler) http.Handler {
function GitController (line 107) | func GitController(_ context.Context, r *mux.Router) {
function askCredentials (line 194) | func askCredentials(w http.ResponseWriter, _ *http.Request) {
function withAccess (line 200) | func withAccess(next http.Handler) http.HandlerFunc {
function serviceRpc (line 370) | func serviceRpc(w http.ResponseWriter, r *http.Request) {
type flushResponseWriter (line 457) | type flushResponseWriter struct
method ReadFrom (line 461) | func (f *flushResponseWriter) ReadFrom(r io.Reader) (int64, error) {
function getInfoRefs (line 488) | func getInfoRefs(w http.ResponseWriter, r *http.Request) {
function getInfoPacks (line 551) | func getInfoPacks(w http.ResponseWriter, r *http.Request) {
function getLooseObject (line 556) | func getLooseObject(w http.ResponseWriter, r *http.Request) {
function getPackFile (line 561) | func getPackFile(w http.ResponseWriter, r *http.Request) {
function getIdxFile (line 566) | func getIdxFile(w http.ResponseWriter, r *http.Request) {
function getTextFile (line 571) | func getTextFile(w http.ResponseWriter, r *http.Request) {
function sendFile (line 576) | func sendFile(contentType string, w http.ResponseWriter, r *http.Request) {
function getServiceType (line 592) | func getServiceType(r *http.Request) git.Service {
function isSmart (line 601) | func isSmart(r *http.Request, service git.Service) bool {
function updateServerInfo (line 606) | func updateServerInfo(ctx context.Context, dir string) error {
function renderBadRequest (line 612) | func renderBadRequest(w http.ResponseWriter, r *http.Request) {
function renderMethodNotAllowed (line 616) | func renderMethodNotAllowed(w http.ResponseWriter, r *http.Request) {
function renderNotFound (line 624) | func renderNotFound(w http.ResponseWriter, r *http.Request) {
function renderUnauthorized (line 628) | func renderUnauthorized(w http.ResponseWriter, r *http.Request) {
function renderForbidden (line 632) | func renderForbidden(w http.ResponseWriter, r *http.Request) {
function renderInternalServerError (line 636) | func renderInternalServerError(w http.ResponseWriter, r *http.Request) {
function hdrNocache (line 642) | func hdrNocache(w http.ResponseWriter) {
function hdrCacheForever (line 648) | func hdrCacheForever(w http.ResponseWriter) {
FILE: pkg/web/git_lfs.go
function serviceLfsBatch (line 33) | func serviceLfsBatch(w http.ResponseWriter, r *http.Request) {
function serviceLfsBasic (line 243) | func serviceLfsBasic(w http.ResponseWriter, r *http.Request) {
function serviceLfsBasicDownload (line 253) | func serviceLfsBasicDownload(w http.ResponseWriter, r *http.Request) {
function serviceLfsBasicUpload (line 296) | func serviceLfsBasicUpload(w http.ResponseWriter, r *http.Request) {
function serviceLfsBasicVerify (line 370) | func serviceLfsBasicVerify(w http.ResponseWriter, r *http.Request) {
function serviceLfsLocks (line 446) | func serviceLfsLocks(w http.ResponseWriter, r *http.Request) {
function serviceLfsLocksCreate (line 458) | func serviceLfsLocksCreate(w http.ResponseWriter, r *http.Request) {
function serviceLfsLocksGet (line 559) | func serviceLfsLocksGet(w http.ResponseWriter, r *http.Request) {
function serviceLfsLocksVerify (line 734) | func serviceLfsLocksVerify(w http.ResponseWriter, r *http.Request) {
function serviceLfsLocksDelete (line 831) | func serviceLfsLocksDelete(w http.ResponseWriter, r *http.Request) {
function renderJSON (line 959) | func renderJSON(w http.ResponseWriter, statusCode int, v interface{}) {
function renderNotAcceptable (line 967) | func renderNotAcceptable(w http.ResponseWriter) {
function isLfs (line 971) | func isLfs(r *http.Request) bool {
function isBinary (line 977) | func isBinary(r *http.Request) bool {
function hdrLfs (line 982) | func hdrLfs(w http.ResponseWriter) {
FILE: pkg/web/goget.go
function GoGetHandler (line 39) | func GoGetHandler(w http.ResponseWriter, r *http.Request) {
FILE: pkg/web/health.go
function HealthController (line 13) | func HealthController(_ context.Context, r *mux.Router) {
function getLiveness (line 18) | func getLiveness(w http.ResponseWriter, _ *http.Request) {
function getReadiness (line 22) | func getReadiness(w http.ResponseWriter, r *http.Request) {
FILE: pkg/web/http.go
type HTTPServer (line 14) | type HTTPServer struct
method SetTLSConfig (line 42) | func (s *HTTPServer) SetTLSConfig(tlsConfig *tls.Config) {
method Close (line 47) | func (s *HTTPServer) Close() error {
method ListenAndServe (line 52) | func (s *HTTPServer) ListenAndServe() error {
method Shutdown (line 60) | func (s *HTTPServer) Shutdown(ctx context.Context) error {
function NewHTTPServer (line 22) | func NewHTTPServer(ctx context.Context) (*HTTPServer, error) {
FILE: pkg/web/logging.go
type logWriter (line 16) | type logWriter struct
method Write (line 29) | func (r *logWriter) Write(p []byte) (int, error) {
method WriteHeader (line 37) | func (r *logWriter) WriteHeader(code int) {
method Unwrap (line 43) | func (r *logWriter) Unwrap() http.ResponseWriter {
method Flush (line 48) | func (r *logWriter) Flush() {
method CloseNotify (line 55) | func (r *logWriter) CloseNotify() <-chan bool {
method Hijack (line 63) | func (r *logWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
function NewLoggingMiddleware (line 71) | func NewLoggingMiddleware(next http.Handler, logger *log.Logger) http.Ha...
FILE: pkg/web/server.go
function NewRouter (line 14) | func NewRouter(ctx context.Context) http.Handler {
FILE: pkg/web/util.go
function renderStatus (line 9) | func renderStatus(code int) http.HandlerFunc {
FILE: pkg/webhook/branch_tag.go
type BranchTagEvent (line 15) | type BranchTagEvent struct
function NewBranchTagEvent (line 31) | func NewBranchTagEvent(ctx context.Context, user proto.User, repo proto....
FILE: pkg/webhook/collaborator.go
type CollaboratorEvent (line 13) | type CollaboratorEvent struct
type CollaboratorEventAction (line 25) | type CollaboratorEventAction
constant CollaboratorEventAdded (line 29) | CollaboratorEventAdded CollaboratorEventAction = "added"
constant CollaboratorEventRemoved (line 31) | CollaboratorEventRemoved CollaboratorEventAction = "removed"
function NewCollaboratorEvent (line 35) | func NewCollaboratorEvent(ctx context.Context, user proto.User, repo pro...
FILE: pkg/webhook/common.go
type EventPayload (line 6) | type EventPayload interface
type Common (line 14) | type Common struct
method Event (line 25) | func (c Common) Event() Event {
method RepositoryID (line 31) | func (c Common) RepositoryID() int64 {
type User (line 36) | type User struct
type Repository (line 44) | type Repository struct
type Author (line 72) | type Author struct
type Commit (line 82) | type Commit struct
FILE: pkg/webhook/content_type.go
type ContentType (line 10) | type ContentType
method String (line 25) | func (c ContentType) String() string {
method UnmarshalText (line 54) | func (c *ContentType) UnmarshalText(text []byte) error {
method MarshalText (line 65) | func (c ContentType) MarshalText() (text []byte, err error) {
constant ContentTypeJSON (line 14) | ContentTypeJSON ContentType = iota
constant ContentTypeForm (line 16) | ContentTypeForm
function ParseContentType (line 38) | func ParseContentType(s string) (ContentType, error) {
FILE: pkg/webhook/content_type_test.go
function TestParseContentType (line 5) | func TestParseContentType(t *testing.T) {
function TestUnmarshalText (line 44) | func TestUnmarshalText(t *testing.T) {
function TestMarshalText (line 81) | func TestMarshalText(t *testing.T) {
FILE: pkg/webhook/event.go
type Event (line 9) | type Event
method String (line 53) | func (e Event) String() string {
method UnmarshalText (line 85) | func (e *Event) UnmarshalText(text []byte) error {
method MarshalText (line 96) | func (e Event) MarshalText() (text []byte, err error) {
constant EventBranchTagCreate (line 13) | EventBranchTagCreate Event = 1
constant EventBranchTagDelete (line 16) | EventBranchTagDelete Event = 2
constant EventCollaborator (line 19) | EventCollaborator Event = 3
constant EventPush (line 22) | EventPush Event = 4
constant EventRepository (line 25) | EventRepository Event = 5
constant EventRepositoryVisibilityChange (line 28) | EventRepositoryVisibilityChange Event = 6
function Events (line 32) | func Events() []Event {
function ParseEvent (line 70) | func ParseEvent(s string) (Event, error) {
FILE: pkg/webhook/push.go
type PushEvent (line 16) | type PushEvent struct
function NewPushEvent (line 30) | func NewPushEvent(ctx context.Context, user proto.User, repo proto.Repos...
FILE: pkg/webhook/repository.go
type RepositoryEvent (line 13) | type RepositoryEvent struct
type RepositoryEventAction (line 21) | type RepositoryEventAction
constant RepositoryEventActionDelete (line 25) | RepositoryEventActionDelete RepositoryEventAction = "delete"
constant RepositoryEventActionRename (line 27) | RepositoryEventActionRename RepositoryEventAction = "rename"
constant RepositoryEventActionVisibilityChange (line 29) | RepositoryEventActionVisibilityChange RepositoryEventAction = "visibilit...
constant RepositoryEventActionDefaultBranchChange (line 31) | RepositoryEventActionDefaultBranchChange RepositoryEventAction = "defaul...
function NewRepositoryEvent (line 35) | func NewRepositoryEvent(ctx context.Context, user proto.User, repo proto...
FILE: pkg/webhook/ssrf_test.go
function TestSSRFProtection (line 16) | func TestSSRFProtection(t *testing.T) {
FILE: pkg/webhook/validator.go
function ValidateWebhookURL (line 15) | func ValidateWebhookURL(rawURL string) error {
FILE: pkg/webhook/validator_test.go
function TestValidateWebhookURL (line 13) | func TestValidateWebhookURL(t *testing.T) {
function TestErrorAliases (line 42) | func TestErrorAliases(t *testing.T) {
FILE: pkg/webhook/webhook.go
type Hook (line 28) | type Hook struct
type Delivery (line 35) | type Delivery struct
function do (line 45) | func do(ctx context.Context, url string, method string, headers http.Hea...
function SendWebhook (line 61) | func SendWebhook(ctx context.Context, w models.Webhook, event Event, pay...
function SendEvent (line 132) | func SendEvent(ctx context.Context, payload EventPayload) error {
function repoURL (line 149) | func repoURL(publicURL string, repo string) string {
function getDefaultBranch (line 153) | func getDefaultBranch(repo proto.Repository) (string, error) {
FILE: testscript/script_test.go
function PrepareBuildCommand (line 37) | func PrepareBuildCommand(binPath string) *exec.Cmd {
function TestMain (line 46) | func TestMain(m *testing.M) {
function TestScript (line 70) | func TestScript(t *testing.T) {
function cmdSoft (line 199) | func cmdSoft(user string, keys ...ssh.Signer) func(ts *testscript.TestSc...
function cmdUI (line 224) | func cmdUI(key ssh.Signer) func(ts *testscript.TestScript, neg bool, arg...
function cmdDos2Unix (line 284) | func cmdDos2Unix(ts *testscript.TestScript, neg bool, args []string) {
function cmdGit (line 316) | func cmdGit(key string) func(ts *testscript.TestScript, neg bool, args [...
function cmdMkfile (line 341) | func cmdMkfile(ts *testscript.TestScript, neg bool, args []string) {
function check (line 352) | func check(ts *testscript.TestScript, err error, neg bool) {
function cmdReadfile (line 361) | func cmdReadfile(ts *testscript.TestScript, neg bool, args []string) {
function cmdEnvfile (line 365) | func cmdEnvfile(ts *testscript.TestScript, neg bool, args []string) {
function cmdNewWebhook (line 381) | func cmdNewWebhook(ts *testscript.TestScript, neg bool, args []string) {
function cmdCurl (line 404) | func cmdCurl(ts *testscript.TestScript, neg bool, args []string) {
function cmdEnsureServerRunning (line 491) | func cmdEnsureServerRunning(ts *testscript.TestScript, neg bool, args []...
function cmdEnsureServerNotRunning (line 517) | func cmdEnsureServerNotRunning(ts *testscript.TestScript, neg bool, args...
function cmdStopserver (line 540) | func cmdStopserver(ts *testscript.TestScript, neg bool, args []string) {
function setupPostgres (line 548) | func setupPostgres(t testscript.T, cfg *config.Config) (func(), error) {
type maliciousSigner (line 624) | type maliciousSigner struct
method PublicKey (line 631) | func (m *maliciousSigner) PublicKey() ssh.PublicKey {
method Sign (line 636) | func (m *maliciousSigner) Sign(rand io.Reader, data []byte) (*ssh.Sign...
Condensed preview — 298 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (885K chars).
[
{
"path": ".editorconfig",
"chars": 174,
"preview": "root = true\n\n[*]\ncharset=utf-8\nend_of_line=lf\ninsert_final_newline=true\ntrim_trailing_whitespace=true\nindent_size=2\ninde"
},
{
"path": ".github/CODEOWNERS",
"chars": 17,
"preview": "* @aymanbagabas\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 691,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/dependabot.yml",
"chars": 1114,
"preview": "version: 2\n\nupdates:\n - package-ecosystem: \"gomod\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n day:"
},
{
"path": ".github/workflows/build.yml",
"chars": 1080,
"preview": "name: build\n\non:\n push:\n branches:\n - \"main\"\n pull_request:\n\njobs:\n build:\n uses: charmbracelet/meta/.gith"
},
{
"path": ".github/workflows/coverage.yml",
"chars": 1896,
"preview": "name: coverage\n\non:\n push:\n branches:\n - \"main\"\n pull_request:\n\njobs:\n coverage:\n strategy:\n matrix:\n"
},
{
"path": ".github/workflows/dependabot-sync.yml",
"chars": 419,
"preview": "name: dependabot-sync\non:\n schedule:\n - cron: \"0 0 * * 0\" # every Sunday at midnight\n workflow_dispatch: # allows m"
},
{
"path": ".github/workflows/goreleaser.yml",
"chars": 956,
"preview": "# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json\nname: goreleaser\n\non:\n push:\n tags"
},
{
"path": ".github/workflows/lint-sync.yml",
"chars": 277,
"preview": "name: lint-sync\non:\n # schedule:\n # # every Sunday at midnight\n # - cron: \"0 0 * * 0\"\n workflow_dispatch: # allo"
},
{
"path": ".github/workflows/lint.yml",
"chars": 210,
"preview": "name: lint\non:\n push:\n pull_request:\n\njobs:\n lint:\n uses: charmbracelet/meta/.github/workflows/lint.yml@main\n w"
},
{
"path": ".github/workflows/nightly.yml",
"chars": 609,
"preview": "name: nightly\n\non:\n push:\n branches:\n - main\n\njobs:\n nightly:\n uses: charmbracelet/meta/.github/workflows/n"
},
{
"path": ".gitignore",
"chars": 87,
"preview": "cmd/soft/soft\n./soft\n.ssh\n.repos\ndist\ndata/\ncompletions/\nmanpages/\nsoft_serve_ed25519*\n"
},
{
"path": ".golangci.yml",
"chars": 790,
"preview": "version: \"2\"\nlinters:\n enable:\n - bodyclose\n # - exhaustive\n # - goconst\n # - godot\n # - godox\n # - g"
},
{
"path": ".goreleaser.yml",
"chars": 498,
"preview": "# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json\n\nversion: 2\n\nincludes:\n - from_url:\n "
},
{
"path": ".nfpm/postinstall.sh",
"chars": 256,
"preview": "#!/bin/sh\nset -e\n\nif ! command -V systemctl >/dev/null 2>&1; then\n\techo \"Not running SystemD, ignoring\"\n\texit 0\nfi\n\nsyst"
},
{
"path": ".nfpm/postremove.sh",
"chars": 254,
"preview": "#!/bin/sh\nset -e\n\nif ! command -V systemctl >/dev/null 2>&1; then\n\techo \"Not running SystemD, ignoring\"\n\texit 0\nfi\n\nsyst"
},
{
"path": ".nfpm/soft-serve.conf",
"chars": 443,
"preview": "# Config defined here will override the config in /var/lib/soft-serve/config.yaml\n# Keys defined in `SOFT_SERVE_INITIAL_"
},
{
"path": ".nfpm/soft-serve.service",
"chars": 1102,
"preview": "[Unit]\nDescription=Soft Serve git server 🍦\nDocumentation=https://github.com/charmbracelet/soft-serve\nRequires=network-on"
},
{
"path": ".nfpm/sysusers.conf",
"chars": 60,
"preview": "u soft-serve - \"Soft Serve daemon user\" /var/lib/soft-serve\n"
},
{
"path": ".nfpm/tmpfiles.conf",
"chars": 49,
"preview": "d /var/lib/soft-serve 0750 soft-serve soft-serve\n"
},
{
"path": "Dockerfile",
"chars": 574,
"preview": "FROM alpine:latest\n\n# Create directories\nWORKDIR /soft-serve\n# Expose data volume\nVOLUME /soft-serve\n\n# Environment vari"
},
{
"path": "LICENSE",
"chars": 1080,
"preview": "MIT License\n\nCopyright (c) 2021-2023 Charmbracelet, Inc\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "README.md",
"chars": 26117,
"preview": "# Soft Serve\n\n<p>\n <img style=\"width: 451px\" src=\"https://stuff.charm.sh/soft-serve/soft-serve-header.png?0\" alt=\"A n"
},
{
"path": "browse.tape",
"chars": 519,
"preview": "Set Width 1600\nSet Height 900\nSet FontSize 22\n\nOutput soft-serve-browse.gif\nOutput soft-serve-frames/\n\nType@300ms \"soft\""
},
{
"path": "cmd/cmd.go",
"chars": 1739,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"g"
},
{
"path": "cmd/soft/admin/admin.go",
"chars": 1909,
"preview": "package admin\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/soft-serve/cmd\"\n\t\"github.com/charmbracelet/soft-serve/pkg/bac"
},
{
"path": "cmd/soft/browse/browse.go",
"chars": 6396,
"preview": "package browse\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/key\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\""
},
{
"path": "cmd/soft/hook/hook.go",
"chars": 4439,
"preview": "package hook\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n"
},
{
"path": "cmd/soft/main.go",
"chars": 3526,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbrace"
},
{
"path": "cmd/soft/serve/certreloader.go",
"chars": 1277,
"preview": "package serve\n\nimport (\n\t\"crypto/tls\"\n\t\"sync\"\n\n\t\"charm.land/log/v2\"\n)\n\n// CertReloader is responsible for reloading TLS "
},
{
"path": "cmd/soft/serve/certreloader_test.go",
"chars": 2304,
"preview": "//go:build unix\n\npackage serve\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\""
},
{
"path": "cmd/soft/serve/serve.go",
"chars": 4984,
"preview": "package serve\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"sync\"\n\t\"syscall\"\n"
},
{
"path": "cmd/soft/serve/server.go",
"chars": 5138,
"preview": "package serve\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"charm.land/log/v2\"\n\n\t\"github.com/charmb"
},
{
"path": "codecov.yml",
"chars": 114,
"preview": "coverage:\n status:\n project:\n default:\n target: 50%\n patch:\n default:\n target: 30%\n"
},
{
"path": "demo.tape",
"chars": 414,
"preview": "Set Width 1600\nSet Height 900\nSet FontSize 22\n\nOutput soft-serve.gif\nOutput soft-serve-frames/\n\nType \"ssh git.charm.sh\"\n"
},
{
"path": "docker.md",
"chars": 1633,
"preview": "# Running Soft-Serve with Docker\n\nThe official Soft Serve Docker images are available at [charmcli/soft-serve][docker]. "
},
{
"path": "git/attr.go",
"chars": 1395,
"preview": "package git\n\nimport (\n\t\"math/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Attribute represents a Git"
},
{
"path": "git/attr_test.go",
"chars": 1503,
"preview": "package git\n\nimport (\n\t\"testing\"\n\n\t\"github.com/matryer/is\"\n)\n\nfunc TestParseAttr(t *testing.T) {\n\tcases := []struct {\n\t\t"
},
{
"path": "git/command.go",
"chars": 269,
"preview": "package git\n\nimport \"github.com/aymanbagabas/git-module\"\n\n// RunInDirOptions are options for RunInDir.\ntype RunInDirOpti"
},
{
"path": "git/commit.go",
"chars": 751,
"preview": "package git\n\nimport (\n\t\"regexp\"\n\n\t\"github.com/aymanbagabas/git-module\"\n)\n\n// ZeroID is the zero hash.\nconst ZeroID = git"
},
{
"path": "git/config.go",
"chars": 768,
"preview": "package git\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\tgcfg \"github.com/go-git/go-git/v5/plumbing/format/config\"\n)\n\n// Config re"
},
{
"path": "git/errors.go",
"chars": 687,
"preview": "package git\n\nimport (\n\t\"errors\"\n\n\t\"github.com/aymanbagabas/git-module\"\n)\n\nvar (\n\t// ErrFileNotFound is returned when a f"
},
{
"path": "git/patch.go",
"chars": 8717,
"preview": "package git\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/aymanbagabas/git-module\"\n\t\"github.com/dus"
},
{
"path": "git/reference.go",
"chars": 1209,
"preview": "package git\n\nimport (\n\t\"strings\"\n\n\t\"github.com/aymanbagabas/git-module\"\n)\n\nconst (\n\t// HEAD represents the name of the H"
},
{
"path": "git/repo.go",
"chars": 4476,
"preview": "package git\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/aymanbagabas/git-module\"\n)\n\nvar (\n\t// DiffMaxFile is the"
},
{
"path": "git/server.go",
"chars": 391,
"preview": "package git\n\nimport (\n\t\"context\"\n\n\t\"github.com/aymanbagabas/git-module\"\n)\n\n// UpdateServerInfo updates the server info f"
},
{
"path": "git/stash.go",
"chars": 447,
"preview": "package git\n\nimport \"github.com/aymanbagabas/git-module\"\n\n// StashDiff returns the diff of the given stash index.\nfunc ("
},
{
"path": "git/tag.go",
"chars": 98,
"preview": "package git\n\nimport \"github.com/aymanbagabas/git-module\"\n\n// Tag is a git tag.\ntype Tag = git.Tag\n"
},
{
"path": "git/tree.go",
"chars": 3820,
"preview": "package git\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"io\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"sort\"\n\n\t\"github.com/aymanbagabas/git-module\"\n)"
},
{
"path": "git/types.go",
"chars": 256,
"preview": "package git\n\nimport \"github.com/aymanbagabas/git-module\"\n\n// CommandOptions contain options for running a git command.\nt"
},
{
"path": "git/utils.go",
"chars": 1477,
"preview": "package git\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gobwas/glob\"\n)\n\n// LatestFile returns the contents of the fir"
},
{
"path": "go.mod",
"chars": 4510,
"preview": "module github.com/charmbracelet/soft-serve\n\ngo 1.25.8\n\nrequire (\n\tcharm.land/bubbles/v2 v2.0.0\n\tcharm.land/bubbletea/v2 "
},
{
"path": "go.sum",
"chars": 29141,
"preview": "charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=\ncharm.land/bubbles/v2 v2.0.0/go.mod h1:rCHo"
},
{
"path": "pkg/access/access.go",
"chars": 1716,
"preview": "package access\n\nimport (\n\t\"encoding\"\n\t\"errors\"\n)\n\n// AccessLevel is the level of access allowed to a repo.\ntype AccessLe"
},
{
"path": "pkg/access/access_test.go",
"chars": 484,
"preview": "package access\n\nimport \"testing\"\n\nfunc TestParseAccessLevel(t *testing.T) {\n\tcases := []struct {\n\t\tin string\n\t\tout Acce"
},
{
"path": "pkg/access/context.go",
"chars": 512,
"preview": "package access\n\nimport \"context\"\n\n// ContextKey is the context key for the access level.\nvar ContextKey = &struct{ strin"
},
{
"path": "pkg/access/context_test.go",
"chars": 418,
"preview": "package access\n\nimport (\n\t\"context\"\n\t\"testing\"\n)\n\nfunc TestGoodFromContext(t *testing.T) {\n\tctx := WithContext(context.T"
},
{
"path": "pkg/backend/access_token.go",
"chars": 1945,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\"\n\t\"github.com/charm"
},
{
"path": "pkg/backend/auth.go",
"chars": 1059,
"preview": "package backend\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\n\t\"charm.land/log/v2\"\n\t\"golang.org/x/crypto/bc"
},
{
"path": "pkg/backend/auth_test.go",
"chars": 647,
"preview": "package backend\n\nimport \"testing\"\n\nfunc TestHashPassword(t *testing.T) {\n\thash, err := HashPassword(\"password\")\n\tif err "
},
{
"path": "pkg/backend/backend.go",
"chars": 969,
"preview": "package backend\n\nimport (\n\t\"context\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbracelet/soft-serve/pkg/config\"\n\t\"github.co"
},
{
"path": "pkg/backend/cache.go",
"chars": 606,
"preview": "package backend\n\nimport lru \"github.com/hashicorp/golang-lru/v2\"\n\n// TODO: implement a caching interface.\ntype cache str"
},
{
"path": "pkg/backend/collab.go",
"chars": 3240,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n\t\"github.co"
},
{
"path": "pkg/backend/context.go",
"chars": 501,
"preview": "package backend\n\nimport \"context\"\n\n// ContextKey is the key for the backend in the context.\nvar ContextKey = &struct{ st"
},
{
"path": "pkg/backend/hooks.go",
"chars": 3687,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/charmbracelet/soft-serve/git\"\n\t\"github.com/charmb"
},
{
"path": "pkg/backend/lfs.go",
"chars": 2311,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/charmbracelet/sof"
},
{
"path": "pkg/backend/repo.go",
"chars": 19099,
"preview": "package backend\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"stri"
},
{
"path": "pkg/backend/settings.go",
"chars": 1502,
"preview": "package backend\n\nimport (\n\t\"context\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n\t\"github.com/charmbracelet/soft-"
},
{
"path": "pkg/backend/user.go",
"chars": 10836,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n\t\"g"
},
{
"path": "pkg/backend/utils.go",
"chars": 681,
"preview": "package backend\n\nimport (\n\t\"github.com/charmbracelet/soft-serve/git\"\n\t\"github.com/charmbracelet/soft-serve/pkg/proto\"\n)\n"
},
{
"path": "pkg/backend/webhooks.go",
"chars": 7930,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbracelet/soft-serve/pkg/db"
},
{
"path": "pkg/config/config.go",
"chars": 15551,
"preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/caarlos0/env/v11\"\n\t\"g"
},
{
"path": "pkg/config/config_test.go",
"chars": 3857,
"preview": "package config\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/matryer/is\"\n)\n\nfunc TestParseMultipleKeys(t *testing.T) {\n\tis :="
},
{
"path": "pkg/config/context.go",
"chars": 505,
"preview": "package config\n\nimport \"context\"\n\n// ContextKey is the context key for the config.\nvar ContextKey = struct{ string }{\"co"
},
{
"path": "pkg/config/context_test.go",
"chars": 664,
"preview": "package config\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestBadFromContext(t *testing.T) {\n\tctx := context.TOD"
},
{
"path": "pkg/config/file.go",
"chars": 4337,
"preview": "package config\n\nimport (\n\t\"bytes\"\n\t\"text/template\"\n)\n\nvar configFileTmpl = template.Must(template.New(\"config\").Parse(`#"
},
{
"path": "pkg/config/file_test.go",
"chars": 257,
"preview": "package config\n\nimport \"testing\"\n\nfunc TestNewConfigFile(t *testing.T) {\n\tfor _, cfg := range []*Config{\n\t\tnil,\n\t\tDefaul"
},
{
"path": "pkg/config/ssh.go",
"chars": 608,
"preview": "package config\n\nimport (\n\t\"errors\"\n\n\t\"github.com/charmbracelet/keygen\"\n)\n\nvar (\n\t// ErrNilConfig is returned when a nil "
},
{
"path": "pkg/config/ssh_test.go",
"chars": 475,
"preview": "package config\n\nimport \"testing\"\n\nfunc TestBadSSHKeyPair(t *testing.T) {\n\tfor _, cfg := range []*Config{\n\t\tnil,\n\t\t{},\n\t}"
},
{
"path": "pkg/config/testdata/config.yaml",
"chars": 61,
"preview": "# Soft Serve Server configurations\n\nname: \"Test server name\"\n"
},
{
"path": "pkg/config/testdata/k1.pub",
"chars": 85,
"preview": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMwLvyV3ouVrTysUYGoJdl5Vgn5BACKov+n9PlzfPwH a@b\n"
},
{
"path": "pkg/cron/cron.go",
"chars": 1439,
"preview": "package cron\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/robfig/cron/v3\"\n)\n\n// Scheduler is a cron-l"
},
{
"path": "pkg/cron/cron_test.go",
"chars": 586,
"preview": "package cron\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"charm.land/log/v2\"\n)\n\nfunc TestCronLogger(t *testing.T) "
},
{
"path": "pkg/daemon/conn.go",
"chars": 2126,
"preview": "package daemon\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n)\n\n// connections is a synchronizes access to to a "
},
{
"path": "pkg/daemon/daemon.go",
"chars": 8677,
"preview": "package daemon\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n"
},
{
"path": "pkg/daemon/daemon_test.go",
"chars": 3000,
"preview": "package daemon\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/charmbracelet"
},
{
"path": "pkg/db/context.go",
"chars": 484,
"preview": "package db\n\nimport \"context\"\n\n// ContextKey is the key used to store the database in the context.\nvar ContextKey = struc"
},
{
"path": "pkg/db/context_test.go",
"chars": 590,
"preview": "package db_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\"\n\t\"github.com/charmbracele"
},
{
"path": "pkg/db/db.go",
"chars": 1833,
"preview": "package db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbracelet/soft-s"
},
{
"path": "pkg/db/db_test.go",
"chars": 352,
"preview": "package db\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestOpenUnknownDriver(t *testing.T) {\n\t_, err := Open(cont"
},
{
"path": "pkg/db/errors.go",
"chars": 1127,
"preview": "package db\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/lib/pq\"\n\tsqlite \"modernc.org/sqlite\"\n\tsqlitelib \"modernc.or"
},
{
"path": "pkg/db/errors_test.go",
"chars": 481,
"preview": "package db\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestWrapErrorBadNoRows(t *testing.T) {\n\tfor _, "
},
{
"path": "pkg/db/handler.go",
"chars": 777,
"preview": "package db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\n// Handler is a database handler.\ntype Ha"
},
{
"path": "pkg/db/internal/test/test.go",
"chars": 634,
"preview": "package test\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\"\n)\n\n// OpenS"
},
{
"path": "pkg/db/logger.go",
"chars": 5548,
"preview": "package db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"strings\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc trac"
},
{
"path": "pkg/db/migrate/0001_create_tables.go",
"chars": 4983,
"preview": "package migrate\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n\t\"gi"
},
{
"path": "pkg/db/migrate/0001_create_tables_postgres.down.sql",
"chars": 0,
"preview": ""
},
{
"path": "pkg/db/migrate/0001_create_tables_postgres.up.sql",
"chars": 2970,
"preview": "CREATE TABLE IF NOT EXISTS settings (\n id SERIAL PRIMARY KEY,\n key TEXT NOT NULL UNIQUE,\n value TEXT NOT NULL,\n crea"
},
{
"path": "pkg/db/migrate/0001_create_tables_sqlite.down.sql",
"chars": 0,
"preview": ""
},
{
"path": "pkg/db/migrate/0001_create_tables_sqlite.up.sql",
"chars": 3073,
"preview": "CREATE TABLE IF NOT EXISTS settings (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n key TEXT NOT NULL UNIQUE,\n value TEXT N"
},
{
"path": "pkg/db/migrate/0002_webhooks.go",
"chars": 467,
"preview": "package migrate\n\nimport (\n\t\"context\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\"\n)\n\nconst (\n\twebhooksName = \"webho"
},
{
"path": "pkg/db/migrate/0002_webhooks_postgres.down.sql",
"chars": 0,
"preview": ""
},
{
"path": "pkg/db/migrate/0002_webhooks_postgres.up.sql",
"chars": 1315,
"preview": "CREATE TABLE IF NOT EXISTS webhooks (\n id SERIAL PRIMARY KEY,\n repo_id INTEGER NOT NULL,\n url TEXT NOT NULL,\n secret"
},
{
"path": "pkg/db/migrate/0002_webhooks_sqlite.down.sql",
"chars": 0,
"preview": ""
},
{
"path": "pkg/db/migrate/0002_webhooks_sqlite.up.sql",
"chars": 1341,
"preview": "CREATE TABLE IF NOT EXISTS webhooks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n repo_id INTEGER NOT NULL,\n url TEXT NOT"
},
{
"path": "pkg/db/migrate/0003_migrate_lfs_objects.go",
"chars": 1874,
"preview": "package migrate\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbracelet/"
},
{
"path": "pkg/db/migrate/migrate.go",
"chars": 3623,
"preview": "package migrate\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbracelet/s"
},
{
"path": "pkg/db/migrate/migrate_test.go",
"chars": 573,
"preview": "package migrate\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/config\"\n\t\"github.com/charmbra"
},
{
"path": "pkg/db/migrate/migrations.go",
"chars": 1455,
"preview": "package migrate\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\""
},
{
"path": "pkg/db/models/access_token.go",
"chars": 395,
"preview": "package models\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n)\n\n// AccessToken represents an access token.\ntype AccessToken struct {"
},
{
"path": "pkg/db/models/collab.go",
"chars": 446,
"preview": "package models\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n)\n\n// Collab represents a repository"
},
{
"path": "pkg/db/models/lfs.go",
"chars": 606,
"preview": "package models\n\nimport \"time\"\n\n// LFSObject is a Git LFS object.\ntype LFSObject struct {\n\tID int64 `db:\"id\"`\n"
},
{
"path": "pkg/db/models/public_key.go",
"chars": 249,
"preview": "package models\n\n// PublicKey represents a public key.\ntype PublicKey struct {\n\tID int64 `db:\"id\"`\n\tUserID int"
},
{
"path": "pkg/db/models/repo.go",
"chars": 544,
"preview": "package models\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n)\n\n// Repo is a database model for a repository.\ntype Repo struct {\n\tID"
},
{
"path": "pkg/db/models/settings.go",
"chars": 243,
"preview": "package models\n\n// Settings represents a settings record.\ntype Settings struct {\n\tID int64 `db:\"id\"`\n\tKey "
},
{
"path": "pkg/db/models/user.go",
"chars": 347,
"preview": "package models\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n)\n\n// User represents a user.\ntype User struct {\n\tID int64 "
},
{
"path": "pkg/db/models/webhook.go",
"chars": 1343,
"preview": "package models\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\n// Webhook is a repository webhook.\ntype "
},
{
"path": "pkg/git/errors.go",
"chars": 782,
"preview": "package git\n\nimport \"errors\"\n\nvar (\n\t// ErrNotAuthed represents unauthorized access.\n\tErrNotAuthed = errors.New(\"you are"
},
{
"path": "pkg/git/git.go",
"chars": 2548,
"preview": "package git\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"charm.land/log/v2\"\n\tgitm \"github."
},
{
"path": "pkg/git/git_test.go",
"chars": 1820,
"preview": "package git\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/soft-serve/git\"\n)\n\nfun"
},
{
"path": "pkg/git/lfs.go",
"chars": 11551,
"preview": "package git\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"ch"
},
{
"path": "pkg/git/lfs_auth.go",
"chars": 2336,
"preview": "package git\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbrace"
},
{
"path": "pkg/git/lfs_log.go",
"chars": 297,
"preview": "package git\n\nimport (\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbracelet/git-lfs-transfer/transfer\"\n)\n\ntype lfsLogger struc"
},
{
"path": "pkg/git/service.go",
"chars": 4757,
"preview": "package git\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"charm.land/log/v2\"\n)\n\n// "
},
{
"path": "pkg/hooks/gen.go",
"chars": 3234,
"preview": "package hooks\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"text/template\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/c"
},
{
"path": "pkg/hooks/gen_test.go",
"chars": 810,
"preview": "package hooks\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/soft-serve/git\"\n\t\"githu"
},
{
"path": "pkg/hooks/hooks.go",
"chars": 627,
"preview": "package hooks\n\nimport (\n\t\"context\"\n\t\"io\"\n)\n\n// HookArg is an argument to a git hook.\ntype HookArg struct {\n\tOldSha stri"
},
{
"path": "pkg/jobs/jobs.go",
"chars": 587,
"preview": "package jobs\n\nimport (\n\t\"context\"\n\t\"sync\"\n)\n\n// Job is a job that can be registered with the scheduler.\ntype Job struct "
},
{
"path": "pkg/jobs/mirror.go",
"chars": 3323,
"preview": "package jobs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charm"
},
{
"path": "pkg/jwk/jwk.go",
"chars": 1053,
"preview": "package jwk\n\nimport (\n\t\"crypto\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/config\"\n\t\"github.com/"
},
{
"path": "pkg/jwk/jwk_test.go",
"chars": 445,
"preview": "package jwk\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/config\"\n)\n\nfunc TestBadNewPair(t *"
},
{
"path": "pkg/lfs/basic_transfer.go",
"chars": 3000,
"preview": "package lfs\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"charm.land/log/v2\"\n)\n\n/"
},
{
"path": "pkg/lfs/client.go",
"chars": 834,
"preview": "package lfs\n\nimport (\n\t\"context\"\n\t\"io\"\n)\n\n// DownloadCallback gets called for every requested LFS object to process its "
},
{
"path": "pkg/lfs/common.go",
"chars": 5471,
"preview": "package lfs\n\nimport (\n\t\"time\"\n)\n\nconst (\n\t// MediaType contains the media type for LFS server requests.\n\tMediaType = \"ap"
},
{
"path": "pkg/lfs/endpoint.go",
"chars": 1623,
"preview": "package lfs\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n)\n\n// Endpoint is a Git LFS endpoint.\ntype Endpoint = *url.URL\n\n// Ne"
},
{
"path": "pkg/lfs/http_client.go",
"chars": 4661,
"preview": "package lfs\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"charm.land/log/v2\"\n\t\"github.c"
},
{
"path": "pkg/lfs/pointer.go",
"chars": 3250,
"preview": "package lfs\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"path\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n"
},
{
"path": "pkg/lfs/pointer_test.go",
"chars": 2162,
"preview": "package lfs\n\nimport (\n\t\"errors\"\n\t\"path\"\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc TestReadPointer(t *testing.T) {\n\tcases := []struct"
},
{
"path": "pkg/lfs/scanner.go",
"chars": 6385,
"preview": "package lfs\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\tgitm \"github.com/aymanba"
},
{
"path": "pkg/lfs/ssh_client.go",
"chars": 52,
"preview": "package lfs\n\n// TODO: implement Git LFS SSH client.\n"
},
{
"path": "pkg/lfs/transfer.go",
"chars": 468,
"preview": "package lfs\n\nimport (\n\t\"context\"\n\t\"io\"\n)\n\n// TransferBasic is the name of the Git LFS basic transfer protocol.\nconst Tra"
},
{
"path": "pkg/log/log.go",
"chars": 1060,
"preview": "package log\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbracelet/soft-serve/pkg/config\"\n)"
},
{
"path": "pkg/log/log_test.go",
"chars": 830,
"preview": "package log\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/config\"\n)\n\nfunc TestGoodNew"
},
{
"path": "pkg/proto/access_token.go",
"chars": 213,
"preview": "package proto\n\nimport \"time\"\n\n// AccessToken represents an access token.\ntype AccessToken struct {\n\tID int64\n\tNam"
},
{
"path": "pkg/proto/context.go",
"chars": 1042,
"preview": "package proto\n\nimport \"context\"\n\n// ContextKeyRepository is the context key for the repository.\nvar ContextKeyRepository"
},
{
"path": "pkg/proto/errors.go",
"chars": 1256,
"preview": "package proto\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\t// ErrUnauthorized is returned when the user is not authorized to perform ac"
},
{
"path": "pkg/proto/repo.go",
"chars": 1623,
"preview": "package proto\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/soft-serve/git\"\n)\n\n// Repository is a Git repository interfa"
},
{
"path": "pkg/proto/user.go",
"chars": 633,
"preview": "package proto\n\nimport \"golang.org/x/crypto/ssh\"\n\n// User is an interface representing a user.\ntype User interface {\n\t// "
},
{
"path": "pkg/ssh/cmd/blob.go",
"chars": 2328,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/charmbracelet/soft-serve/git\"\n\t\"github.com/charmbracelet/soft-serve/pkg"
},
{
"path": "pkg/ssh/cmd/branch.go",
"chars": 4306,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tgitm \"github.com/aymanbagabas/git-module\"\n\t\"github.com/charmbracelet/soft-serv"
},
{
"path": "pkg/ssh/cmd/cmd.go",
"chars": 5224,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"text/template\"\n\t\"unicode\"\n\n\t\"github.com/charmbracelet/soft-serve/pk"
},
{
"path": "pkg/ssh/cmd/collab.go",
"chars": 2331,
"preview": "package cmd\n\nimport (\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n\t\"github.com/charmbracelet/soft-serve/pkg/backen"
},
{
"path": "pkg/ssh/cmd/commit.go",
"chars": 3458,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\tgansi \"charm.land/glamour/v2/ansi\"\n\t\"github.com/charmbracelet/soft-ser"
},
{
"path": "pkg/ssh/cmd/create.go",
"chars": 1558,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/charmbracelet/soft-serve/p"
},
{
"path": "pkg/ssh/cmd/delete.go",
"chars": 590,
"preview": "package cmd\n\nimport (\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc deleteCommand"
},
{
"path": "pkg/ssh/cmd/description.go",
"chars": 965,
"preview": "package cmd\n\nimport (\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc d"
},
{
"path": "pkg/ssh/cmd/git.go",
"chars": 9698,
"preview": "package cmd\n\nimport (\n\t\"errors\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbracelet/sof"
},
{
"path": "pkg/ssh/cmd/hidden.go",
"chars": 911,
"preview": "package cmd\n\nimport (\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc hiddenCommand"
},
{
"path": "pkg/ssh/cmd/import.go",
"chars": 1900,
"preview": "package cmd\n\nimport (\n\t\"errors\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/charmbracelet/soft-serv"
},
{
"path": "pkg/ssh/cmd/info.go",
"chars": 850,
"preview": "package cmd\n\nimport (\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/charmbracelet/soft-serve/pkg/sshut"
},
{
"path": "pkg/ssh/cmd/jwt.go",
"chars": 1372,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/config\"\n\t\"github.com/charmbracelet/soft-"
},
{
"path": "pkg/ssh/cmd/list.go",
"chars": 991,
"preview": "package cmd\n\nimport (\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n\t\"github.com/charmbracelet/soft-serve/pkg/backen"
},
{
"path": "pkg/ssh/cmd/mirror.go",
"chars": 644,
"preview": "package cmd\n\nimport (\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc mirrorCommand"
},
{
"path": "pkg/ssh/cmd/private.go",
"chars": 1001,
"preview": "package cmd\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/spf13/cobra"
},
{
"path": "pkg/ssh/cmd/project_name.go",
"chars": 952,
"preview": "package cmd\n\nimport (\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc p"
},
{
"path": "pkg/ssh/cmd/pubkey.go",
"chars": 2130,
"preview": "package cmd\n\nimport (\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/charmbracelet/soft-ser"
},
{
"path": "pkg/ssh/cmd/rename.go",
"chars": 637,
"preview": "package cmd\n\nimport (\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc renameCommand"
},
{
"path": "pkg/ssh/cmd/repo.go",
"chars": 2425,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/charmbracelet/s"
},
{
"path": "pkg/ssh/cmd/set_username.go",
"chars": 706,
"preview": "package cmd\n\nimport (\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/charmbracelet/soft-serve/pkg/sshut"
},
{
"path": "pkg/ssh/cmd/settings.go",
"chars": 1881,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n\t\"github.com/charmbracelet/so"
},
{
"path": "pkg/ssh/cmd/tag.go",
"chars": 2591,
"preview": "package cmd\n\nimport (\n\t\"strings\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbracelet/soft-serve/git\"\n\t\"github.com/charmbrac"
},
{
"path": "pkg/ssh/cmd/token.go",
"chars": 3265,
"preview": "package cmd\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/lipgloss/v2/table\"\n\t\"github.com/caarlos0/duration\"\n\t\"g"
},
{
"path": "pkg/ssh/cmd/tree.go",
"chars": 2179,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/soft-serve/git\"\n\t\"github.com/charmbracelet/soft-serve/pkg/backe"
},
{
"path": "pkg/ssh/cmd/user.go",
"chars": 5101,
"preview": "package cmd\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/backend\"\n\t\"github.com/charmbracelet/"
},
{
"path": "pkg/ssh/cmd/webhooks.go",
"chars": 10746,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"charm.land/lipgloss/v2/table\"\n\t\"github.com/charmbracelet/soft-serv"
},
{
"path": "pkg/ssh/middleware.go",
"chars": 5445,
"preview": "package ssh\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"charm.land/log/v2\"\n\t\"charm.land/wish/v2\"\n\t\"github.com/charmbracelet/s"
},
{
"path": "pkg/ssh/middleware_test.go",
"chars": 6338,
"preview": "package ssh\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/keygen\"\n\t\"github.com/charmbracelet/soft-s"
},
{
"path": "pkg/ssh/session.go",
"chars": 2092,
"preview": "package ssh\n\nimport (\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/wish/v2\"\n\tbm \"charm.land/wish/v2/bubbletea\"\n\t"
},
{
"path": "pkg/ssh/session_test.go",
"chars": 2446,
"preview": "package ssh\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"charm.land/log/v2\"\n\tbm \"charm.land/wish/v2"
},
{
"path": "pkg/ssh/ssh.go",
"chars": 5558,
"preview": "package ssh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"charm.land/log/v2\"\n\t\"charm.land/wish/v2\"\n\tbm "
},
{
"path": "pkg/ssh/ui.go",
"chars": 8518,
"preview": "package ssh\n\nimport (\n\t\"errors\"\n\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubbles/v2/list\"\n\ttea \"charm.land/bubbletea/v"
},
{
"path": "pkg/sshutils/utils.go",
"chars": 1463,
"preview": "package sshutils\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\n\t\"github.com/charmbracelet/ssh\"\n\tgossh \"golang.org/x/crypto/ssh\"\n)\n\n// P"
},
{
"path": "pkg/sshutils/utils_test.go",
"chars": 2168,
"preview": "package sshutils\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/keygen\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc generateKey"
},
{
"path": "pkg/ssrf/ssrf.go",
"chars": 5291,
"preview": "package ssrf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n)\n\nvar (\n\t"
},
{
"path": "pkg/ssrf/ssrf_test.go",
"chars": 5800,
"preview": "package ssrf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestNewSe"
},
{
"path": "pkg/stats/stats.go",
"chars": 1258,
"preview": "package stats\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/config\"\n\t\"github.com/p"
},
{
"path": "pkg/storage/local.go",
"chars": 1934,
"preview": "package storage\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// LocalStorage is a storage imp"
},
{
"path": "pkg/storage/storage.go",
"chars": 476,
"preview": "package storage\n\nimport (\n\t\"io\"\n\t\"io/fs\"\n)\n\n// Object is an interface for objects that can be stored.\ntype Object interf"
},
{
"path": "pkg/store/access_token.go",
"chars": 844,
"preview": "package store\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\"\n\t\"github.com/charmbracelet/sof"
},
{
"path": "pkg/store/collab.go",
"chars": 822,
"preview": "package store\n\nimport (\n\t\"context\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n\t\"github.com/charmbracelet/soft-se"
},
{
"path": "pkg/store/context.go",
"chars": 472,
"preview": "package store\n\nimport \"context\"\n\n// ContextKey is the store context key.\nvar ContextKey = &struct{ string }{\"store\"}\n\n//"
},
{
"path": "pkg/store/database/access_token.go",
"chars": 2808,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\"\n\t\"github.com/charmbracelet/"
},
{
"path": "pkg/store/database/collab.go",
"chars": 3241,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n\t\"github.com/charmbr"
},
{
"path": "pkg/store/database/database.go",
"chars": 913,
"preview": "package database\n\nimport (\n\t\"context\"\n\n\t\"charm.land/log/v2\"\n\t\"github.com/charmbracelet/soft-serve/pkg/config\"\n\t\"github.c"
},
{
"path": "pkg/store/database/lfs.go",
"chars": 6273,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\"\n\t\"github.com/charmbracel"
},
{
"path": "pkg/store/database/repo.go",
"chars": 6156,
"preview": "package database\n\nimport (\n\t\"context\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\"\n\t\"github.com/charmbracelet/soft-ser"
},
{
"path": "pkg/store/database/settings.go",
"chars": 1725,
"preview": "package database\n\nimport (\n\t\"context\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/access\"\n\t\"github.com/charmbracelet/soft"
},
{
"path": "pkg/store/database/user.go",
"chars": 7637,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\"\n\t\"github.com/charmbracel"
},
{
"path": "pkg/store/database/webhooks.go",
"chars": 6969,
"preview": "package database\n\nimport (\n\t\"context\"\n\n\t\"github.com/charmbracelet/soft-serve/pkg/db\"\n\t\"github.com/charmbracelet/soft-ser"
}
]
// ... and 98 more files (download for full content)
About this extraction
This page contains the full source code of the charmbracelet/soft-serve GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 298 files (777.0 KB), approximately 235.7k tokens, and a symbol index with 1333 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.