{
  "openapi": "3.1.0",
  "info": {
    "title": "gitoskop",
    "description": "Read-only JSON API for browsing bare git repositories under configured roots.\n\nA repository is addressed by `<label>/<relpath-to-git-dir>` where `<label>` is a configured root and `<relpath>` is the path to the bare repo under it (e.g. `pkg/gears/g/git.git`). The repo path, ref names and in-repo file paths may all contain slashes; they are NOT percent-encoded. Because the repo path and the verb are both variable-length, the server resolves the repo by walking the path to the git-dir boundary and treating the remainder as the verb.",
    "version": "0.1.0",
    "license": { "name": "MIT" }
  },
  "servers": [
    { "url": "http://localhost:1449", "description": "Local instance" }
  ],
  "paths": {
    "/": {
      "get": {
        "summary": "List configured root labels",
        "operationId": "listRoots",
        "responses": {
          "200": {
            "description": "Root labels",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RootsList" } } }
          }
        }
      }
    },
    "/_/health": {
      "get": {
        "summary": "Health check",
        "operationId": "health",
        "responses": {
          "200": { "description": "Service is up", "content": { "text/plain": { "schema": { "type": "string", "example": "ok" } } } }
        }
      }
    },
    "/_/version": {
      "get": {
        "summary": "Build identity of the running binary",
        "operationId": "version",
        "responses": {
          "200": {
            "description": "Crate version, git commit at build time (`unknown` when built without .git, `-dirty` suffix for an unclean tree) and unix build timestamp. A stale `built` after a redeploy means a cached image or a non-recreated container.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["version", "commit", "built"], "properties": { "version": { "type": "string" }, "commit": { "type": "string" }, "built": { "type": "integer", "format": "int64" } } } } }
          }
        }
      }
    },
    "/_/metrics": {
      "get": {
        "summary": "Prometheus metrics",
        "operationId": "metrics",
        "responses": {
          "200": { "description": "Metrics in Prometheus text exposition format", "content": { "text/plain": { "schema": { "type": "string" } } } }
        }
      }
    },
    "/_/openapi.json": {
      "get": {
        "summary": "This OpenAPI document",
        "operationId": "openapi",
        "responses": {
          "200": { "description": "OpenAPI specification", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/_/search": {
      "get": {
        "summary": "Search indexed repository paths",
        "operationId": "search",
        "parameters": [
          { "name": "q", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Case-insensitive substring matched against `<label>/<relpath>`." },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200, "minimum": 0 }, "description": "Page size. Defaults to the server's `search_default_limit` (50); clamped to `search_max_limit` (200)." },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0, "minimum": 0 } }
        ],
        "responses": {
          "200": {
            "description": "Matching repository paths",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResults" } } }
          }
        }
      }
    },
    "/{repo}": {
      "get": {
        "summary": "Repository summary or directory listing",
        "description": "If `{repo}` resolves to a git repository, returns its summary. If it resolves to a directory, returns a browse listing of its children.",
        "operationId": "summaryOrBrowse",
        "parameters": [
          { "$ref": "#/components/parameters/Repo" },
          { "name": "meta", "in": "query", "schema": { "type": "string", "enum": ["0"] }, "description": "On a directory listing, `meta=0` skips per-repo summary metadata for an instant structural listing." }
        ],
        "responses": {
          "200": {
            "description": "Repository summary or directory listing",
            "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/RepoSummary" }, { "$ref": "#/components/schemas/BrowseListing" } ] } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{repo}/refs": {
      "get": {
        "summary": "Branches and tags combined",
        "operationId": "refs",
        "parameters": [ { "$ref": "#/components/parameters/Repo" } ],
        "responses": {
          "200": { "description": "Refs", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RefEntry" } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{repo}/branches": {
      "get": {
        "summary": "Branch list",
        "operationId": "branches",
        "parameters": [ { "$ref": "#/components/parameters/Repo" } ],
        "responses": {
          "200": { "description": "Branches", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RefEntry" } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{repo}/tags": {
      "get": {
        "summary": "Tag list",
        "operationId": "tags",
        "parameters": [ { "$ref": "#/components/parameters/Repo" } ],
        "responses": {
          "200": { "description": "Tags", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RefEntry" } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{repo}/log/{ref}": {
      "get": {
        "summary": "Commit log for a ref, newest first",
        "operationId": "log",
        "parameters": [
          { "$ref": "#/components/parameters/Repo" },
          { "$ref": "#/components/parameters/Ref" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 500, "minimum": 0 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0, "maximum": 50000, "minimum": 0 } }
        ],
        "responses": {
          "200": { "description": "Commits", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/CommitInfo" } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{repo}/commit/{oid}": {
      "get": {
        "summary": "Single commit detail",
        "operationId": "commit",
        "parameters": [
          { "$ref": "#/components/parameters/Repo" },
          { "name": "oid", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Commit OID (hex)." }
        ],
        "responses": {
          "200": { "description": "Commit detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CommitDetail" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{repo}/show/{rev}": {
      "get": {
        "summary": "Full git-show analog: commit detail plus patch",
        "operationId": "show",
        "parameters": [
          { "$ref": "#/components/parameters/Repo" },
          { "name": "rev", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Single committish (branch, tag or OID). Diffed against its first parent (root commit → against the empty tree). Branch/tag names and OIDs only; revspec operators (`~`, `^`, `@{…}`) are rejected." },
          { "name": "format", "in": "query", "required": false, "schema": { "type": "string", "enum": ["patch"] }, "description": "format=patch returns a single-message git-format-patch mbox (text/plain) that applies with `git am`; omit for JSON." }
        ],
        "responses": {
          "200": {
            "description": "Commit detail plus unified diff (JSON, default: CommitDetail fields flattened alongside message/patch), or a format-patch mbox when format=patch. Output over max_patch_bytes (default 2 MiB, 0 = unlimited) is truncated with a trailing `# gitoskop: patch truncated` line in the JSON form; the mbox form responds 413 instead of emitting a patch that would fail `git am`.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ShowResponse" } },
              "text/plain": { "schema": { "type": "string" } }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "413": { "$ref": "#/components/responses/PatchTooLarge" }
        }
      }
    },
    "/{repo}/diff/{range}": {
      "get": {
        "summary": "Diff between two refs/OIDs",
        "operationId": "diff",
        "parameters": [
          { "$ref": "#/components/parameters/Repo" },
          { "name": "range", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Two ref-ish endpoints joined by `..`, e.g. `v1.0..master`. Branch/tag names and OIDs only; revspec operators (`~`, `^`, `@{…}`) are rejected." }
        ],
        "responses": {
          "200": { "description": "File changes", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DiffResult" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{repo}/patch/{range}": {
      "get": {
        "summary": "Unified diff as plain text",
        "operationId": "patch",
        "parameters": [
          { "$ref": "#/components/parameters/Repo" },
          { "name": "range", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Either two ref-ish endpoints joined by `..` (e.g. `v1.0..master`), or a single committish — diffed against its first parent (root commit → against the empty tree). Branch/tag names and OIDs only; revspec operators (`~`, `^`, `@{…}`) are rejected." }
        ],
        "responses": {
          "200": { "description": "git-style unified diff. Binary or >4 MiB blobs appear as `Binary files … differ` markers; output over `max_patch_bytes` (default 2 MiB, 0 = unlimited) is cut with a trailing `# gitoskop: patch truncated` line.", "content": { "text/plain": { "schema": { "type": "string" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{repo}/tree/{ref}/{path}": {
      "get": {
        "summary": "Directory listing within a tree",
        "operationId": "tree",
        "parameters": [
          { "$ref": "#/components/parameters/Repo" },
          { "$ref": "#/components/parameters/Ref" },
          { "$ref": "#/components/parameters/InRepoPath" }
        ],
        "responses": {
          "200": { "description": "Tree listing", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TreeListing" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{repo}/blob/{ref}/{path}": {
      "get": {
        "summary": "File content as JSON",
        "operationId": "blob",
        "parameters": [
          { "$ref": "#/components/parameters/Repo" },
          { "$ref": "#/components/parameters/Ref" },
          { "$ref": "#/components/parameters/InRepoPath" }
        ],
        "responses": {
          "200": { "description": "Blob", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BlobInfo" } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "413": { "$ref": "#/components/responses/TooLarge" }
        }
      }
    },
    "/{repo}/raw/{ref}/{path}": {
      "get": {
        "summary": "Raw file bytes (download)",
        "description": "Served with `X-Content-Type-Options: nosniff` and `Content-Disposition: attachment`; never `text/html`.",
        "operationId": "raw",
        "parameters": [
          { "$ref": "#/components/parameters/Repo" },
          { "$ref": "#/components/parameters/Ref" },
          { "$ref": "#/components/parameters/InRepoPath" }
        ],
        "responses": {
          "200": { "description": "Raw bytes", "content": { "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "413": { "$ref": "#/components/responses/TooLarge" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Repo": {
        "name": "repo", "in": "path", "required": true,
        "schema": { "type": "string" },
        "description": "Repository path `<label>/<relpath>` (may contain slashes, not percent-encoded), e.g. `pkg/gears/g/git.git`."
      },
      "Ref": {
        "name": "ref", "in": "path", "required": true,
        "schema": { "type": "string" },
        "description": "Branch, tag or OID. May contain slashes (e.g. `old/sisyphus-task79111`). Restricted to ref-name/hex characters."
      },
      "InRepoPath": {
        "name": "path", "in": "path", "required": false,
        "schema": { "type": "string" },
        "description": "Path within the repository tree (may contain slashes). Empty means the tree root."
      }
    },
    "responses": {
      "NotFound": { "description": "Unknown repository, ref or path", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "BadRequest": { "description": "Invalid parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "TooLarge": { "description": "Blob exceeds the configured size cap", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "PatchTooLarge": { "description": "format=patch and the diff exceeds max_patch_bytes", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "string", "examples": ["not_found", "bad_request", "too_large", "internal"] },
              "message": { "type": "string" }
            }
          }
        }
      },
      "RootsList": {
        "type": "object",
        "required": ["roots"],
        "properties": { "roots": { "type": "array", "items": { "type": "string" } } }
      },
      "SearchResults": {
        "type": "object",
        "required": ["results", "total"],
        "properties": {
          "results": { "type": "array", "items": { "type": "string" }, "description": "Repository paths `<label>/<relpath>`." },
          "total": { "type": "integer", "description": "Total number of matches; `results.length < total` means the page was truncated — paginate with `offset`." }
        }
      },
      "CommitInfo": {
        "type": "object",
        "required": ["oid", "author", "committer", "time", "subject"],
        "properties": {
          "oid": { "type": "string", "description": "40-char hex OID" },
          "author": { "type": "string", "description": "`Name <email>`" },
          "committer": { "type": "string", "description": "`Name <email>`" },
          "time": { "type": "integer", "format": "int64", "description": "Committer time, Unix seconds" },
          "subject": { "type": "string" }
        }
      },
      "RefEntry": {
        "type": "object",
        "required": ["name", "target"],
        "properties": {
          "name": { "type": "string" },
          "target": { "type": "string", "description": "40-char hex OID the ref resolves to" }
        }
      },
      "RepoSummary": {
        "type": "object",
        "required": ["default_branch", "last_commit", "branches", "tags", "size_bytes"],
        "properties": {
          "default_branch": { "type": "string", "description": "Branch whose tip has the newest committer time (HEAD is ignored — gitery repos often keep HEAD at a never-pushed master)" },
          "last_commit": { "$ref": "#/components/schemas/CommitInfo", "description": "Tip commit of `default_branch`" },
          "branches": { "type": "integer", "description": "Branch count" },
          "tags": { "type": "integer", "description": "Tag count" },
          "size_bytes": { "type": "integer", "format": "int64", "description": "Approximate on-disk size" },
          "merge_status": {
            "type": "string",
            "enum": ["merged", "unmerged", "no_upstream", "empty"],
            "description": "Only for repos under a root with `upstream` configured. merged = every ref (branches and tags, tags peeled to commits) is reachable from the upstream pair's refs — deleting this repo loses nothing; unmerged = at least one ref target is missing or unreachable upstream; no_upstream = no same-basename repo under the upstream root (or its configured subdir); empty = the repo has no refs. Omitted when the root has no upstream or the pair repo is unreadable."
          }
        }
      },
      "BrowseEntry": {
        "oneOf": [
          {
            "type": "object",
            "required": ["kind", "name"],
            "properties": {
              "kind": { "type": "string", "const": "dir" },
              "name": { "type": "string" }
            }
          },
          {
            "type": "object",
            "required": ["kind", "name"],
            "properties": {
              "kind": { "type": "string", "const": "repo" },
              "name": { "type": "string" },
              "repo": { "$ref": "#/components/schemas/RepoSummary", "description": "Omitted when listed with meta=0 or when the repo's meta is unavailable." }
            }
          }
        ]
      },
      "BrowseListing": {
        "type": "object",
        "required": ["path", "entries"],
        "properties": {
          "path": { "type": "string" },
          "entries": { "type": "array", "items": { "$ref": "#/components/schemas/BrowseEntry" } }
        }
      },
      "FileChange": {
        "type": "object",
        "required": ["path", "status", "insertions", "deletions"],
        "properties": {
          "path": { "type": "string" },
          "status": { "type": "string", "enum": ["A", "M", "D"] },
          "insertions": { "type": "integer", "description": "Lines added; 0 for binary or oversized (>4 MiB) blobs" },
          "deletions": { "type": "integer", "description": "Lines removed; 0 for binary or oversized (>4 MiB) blobs" }
        }
      },
      "CommitDetail": {
        "type": "object",
        "description": "CommitInfo fields are flattened in alongside parents/files/truncated.",
        "required": ["oid", "author", "committer", "time", "subject", "parents", "files", "truncated"],
        "properties": {
          "oid": { "type": "string" },
          "author": { "type": "string" },
          "committer": { "type": "string" },
          "time": { "type": "integer", "format": "int64" },
          "subject": { "type": "string" },
          "parents": { "type": "array", "items": { "type": "string" } },
          "files": { "type": "array", "items": { "$ref": "#/components/schemas/FileChange" } },
          "truncated": { "type": "boolean", "description": "true if the file list was capped at 50000" }
        }
      },
      "ShowResponse": {
        "type": "object",
        "description": "CommitDetail fields are flattened in alongside message and patch.",
        "required": ["oid", "author", "committer", "time", "subject", "parents", "files", "truncated", "message", "patch"],
        "properties": {
          "oid": { "type": "string" },
          "author": { "type": "string" },
          "committer": { "type": "string" },
          "time": { "type": "integer", "format": "int64" },
          "subject": { "type": "string" },
          "parents": { "type": "array", "items": { "type": "string" } },
          "files": { "type": "array", "items": { "$ref": "#/components/schemas/FileChange" } },
          "truncated": { "type": "boolean", "description": "true if the file list was capped at 50000" },
          "message": { "type": "string", "description": "Full commit message (subject + body)." },
          "patch": { "type": "string", "description": "Unified diff vs the first parent; lossy UTF-8 (JSON is UTF-8 only — byte-exact output lives at ?format=patch)." }
        }
      },
      "DiffResult": {
        "type": "object",
        "required": ["files", "truncated"],
        "properties": {
          "files": { "type": "array", "items": { "$ref": "#/components/schemas/FileChange" } },
          "truncated": { "type": "boolean" }
        }
      },
      "TreeEntry": {
        "type": "object",
        "required": ["name", "kind", "mode", "oid"],
        "properties": {
          "name": { "type": "string" },
          "kind": { "type": "string", "enum": ["blob", "tree"] },
          "mode": { "type": "string", "description": "Octal mode, e.g. `100644`" },
          "oid": { "type": "string" },
          "size": { "type": ["integer", "null"], "format": "int64" }
        }
      },
      "TreeListing": {
        "type": "object",
        "required": ["entries", "truncated"],
        "properties": {
          "entries": { "type": "array", "items": { "$ref": "#/components/schemas/TreeEntry" } },
          "truncated": { "type": "boolean", "description": "true if the listing was capped at 50000" }
        }
      },
      "BlobInfo": {
        "type": "object",
        "required": ["oid", "size", "binary"],
        "properties": {
          "oid": { "type": "string" },
          "size": { "type": "integer", "format": "int64" },
          "binary": { "type": "boolean" },
          "text": { "type": ["string", "null"], "description": "File content; null when binary" }
        }
      }
    }
  }
}
