Full Code of charmbracelet/soft-serve for AI

main 80490de86ee9 cached
298 files
777.0 KB
235.7k tokens
1333 symbols
1 requests
Download .txt
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
Download .txt
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
Download .txt
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.

Copied to clipboard!