~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
1107 lines · 36.3 KB Raw History
/**
 * Lexicons for the dev.pdsjs.git XRPC methods: the views a repository's
 * server answers with, the queries that read them, and the procedures that
 * write. The record lexicons are in lexicon.js; these describe what the
 * server derives from those records and the bundle chain.
 *
 * A query names the repository by record key. The server holds one account,
 * so no parameter names the account. A repository in a space is reachable
 * through the account's own session only.
 */

import { GIT_REPO_COLLECTION } from './lexicon.js';

export const GIT_DEFS = 'dev.pdsjs.git.defs';

/** Parameters every read shares, with the ones a method adds on top. */
const REPO_PARAMS = {
  repo: {
    type: 'string',
    maxLength: 512,
    description: 'Repository name, the record key.',
  },
  space: {
    type: 'string',
    format: 'at-uri',
    description:
      'The space holding the repository. Reachable through the account session only.',
  },
  cid: {
    type: 'string',
    format: 'cid',
    description:
      'The repository record version the caller read. Given and current, the answer is immutable and cacheable for a year. Given and stale, the answer is RepoNotFound, and the caller lists again.',
  },
  from: {
    type: 'string',
    format: 'did',
    description:
      "The account whose copy of the repository to read, for a branch in a collaborator's or a fork's copy. The server reads it from that account's server. The answer carries `copy`, the account it came from, and no record version, since the version is theirs.",
  },
};

const REF_PARAM = {
  type: 'string',
  maxLength: 512,
  description:
    'A full ref name, a branch or tag name, a commit id, or absent for the default branch.',
};

export const gitDefsLexicon = {
  lexicon: 1,
  id: GIT_DEFS,
  defs: {
    package: {
      type: 'object',
      required: ['registry', 'name'],
      properties: {
        registry: { type: 'string', knownValues: ['npm', 'oci'] },
        name: { type: 'string', maxLength: 512 },
      },
    },
    repoView: {
      type: 'object',
      description:
        'A hosted repository as a listing shows it: the record summarized, the settings beside it, and the URLs that clone it.',
      required: [
        'name',
        'uri',
        'cid',
        'refs',
        'branches',
        'tags',
        'bundles',
        'parts',
        'size',
        'protectedBranches',
        'packages',
        'cloneUrl',
      ],
      nullable: [
        'space',
        'description',
        'defaultBranch',
        'createdAt',
        'updatedAt',
        'runner',
        'httpCloneUrl',
        'openPulls',
        'openIssues',
        'activity',
      ],
      properties: {
        name: { type: 'string', maxLength: 512 },
        uri: {
          cid: {
            type: 'string',
            format: 'cid',
            description:
              'The record version, which a read of the repository names to be answered immutably.',
          },
          type: 'string',
          description:
            'Where the record is. An AT-URI for the public repo; for a space, the space URI followed by the writer DID, the collection and the key.',
        },
        space: { type: 'string', format: 'at-uri' },
        spaceOwned: {
          type: 'boolean',
          description: 'Whether this account is the authority of that space.',
        },
        description: { type: 'string', maxLength: 3000 },
        defaultBranch: { type: 'string', maxLength: 512 },
        refs: {
          type: 'array',
          items: { type: 'ref', ref: `${GIT_REPO_COLLECTION}#ref` },
        },
        branches: { type: 'integer' },
        tags: { type: 'integer' },
        bundles: { type: 'integer' },
        parts: {
          type: 'integer',
          description: 'Blob chunks across every bundle.',
        },
        size: { type: 'integer', description: 'Bundle bytes in total.' },
        openPulls: {
          type: 'integer',
          description:
            'Open pull requests, present only when the listing asked for a summary.',
        },
        openIssues: {
          type: 'integer',
          description:
            'Open issues, present only when the listing asked for a summary.',
        },
        activity: {
          type: 'array',
          description:
            'The commit dates in the recent window, for the activity gauge. Present only when the listing asked for a summary.',
          items: { type: 'integer' },
        },
        createdAt: { type: 'string', format: 'datetime' },
        updatedAt: { type: 'string', format: 'datetime' },
        protectedBranches: {
          type: 'array',
          items: { type: 'string', maxLength: 512 },
        },
        runner: { type: 'string', format: 'did' },
        packages: { type: 'array', items: { type: 'ref', ref: '#package' } },
        cloneUrl: {
          type: 'string',
          description: 'The atproto:// URL git-remote-atproto clones.',
        },
        httpCloneUrl: {
          type: 'string',
          description:
            'The URL stock git clones, where the server serves smart HTTP.',
        },
      },
    },
    treeEntry: {
      type: 'object',
      required: ['name', 'type', 'mode', 'sha'],
      nullable: ['size'],
      properties: {
        name: { type: 'string' },
        type: {
          type: 'string',
          knownValues: ['dir', 'file', 'symlink', 'submodule'],
        },
        mode: { type: 'string', description: 'Octal, as git stores it.' },
        sha: { type: 'string', minLength: 40, maxLength: 64 },
        size: {
          type: 'integer',
          description: 'Bytes, for a file the pack index sizes.',
        },
      },
    },
    fileEntry: {
      type: 'object',
      required: ['path', 'size'],
      properties: {
        path: { type: 'string' },
        size: { type: 'integer' },
      },
    },
    treeView: {
      type: 'object',
      description:
        'One directory of a repository. Directories come first, then files, both by name.',
      required: ['ref', 'sha', 'path', 'entries'],
      properties: {
        ref: { type: 'string', description: 'The full ref name resolved.' },
        sha: { type: 'string', description: 'The object the ref points at.' },
        path: { type: 'string', description: 'Empty for the root.' },
        entries: {
          type: 'array',
          items: { type: 'ref', ref: '#treeEntry' },
        },
      },
    },
    fileView: {
      type: 'object',
      description:
        'One file. Text comes inline up to 512 KiB; a binary or larger file reports its metadata only, and getRaw serves the bytes.',
      required: ['ref', 'path', 'size', 'mode', 'binary', 'truncated'],
      nullable: ['mediaType', 'text'],
      properties: {
        ref: { type: 'string' },
        path: { type: 'string' },
        size: { type: 'integer' },
        mode: { type: 'string' },
        mediaType: {
          type: 'string',
          description: 'Set for an image the page can show inline.',
        },
        binary: { type: 'boolean' },
        truncated: {
          type: 'boolean',
          description: 'True when the file is over the inline size.',
        },
        text: { type: 'string' },
      },
    },
    commitView: {
      type: 'object',
      required: [
        'sha',
        'parents',
        'author',
        'authoredAt',
        'committedAt',
        'message',
      ],
      properties: {
        sha: { type: 'string', minLength: 40, maxLength: 64 },
        parents: { type: 'array', items: { type: 'string' } },
        author: {
          type: 'string',
          description: 'The author ident as git records it: Name <email>.',
        },
        authoredAt: { type: 'integer', description: 'Epoch milliseconds.' },
        committedAt: { type: 'integer', description: 'Epoch milliseconds.' },
        message: { type: 'string' },
      },
    },
    diffLine: {
      type: 'object',
      required: ['type', 'text'],
      nullable: ['oldLine', 'newLine'],
      properties: {
        type: { type: 'string', knownValues: ['context', 'add', 'delete'] },
        text: { type: 'string' },
        oldLine: {
          type: 'integer',
          description: '1-based, before the change.',
        },
        newLine: { type: 'integer', description: '1-based, after the change.' },
      },
    },
    diffHunk: {
      type: 'object',
      required: ['oldStart', 'oldLines', 'newStart', 'newLines', 'lines'],
      properties: {
        oldStart: { type: 'integer' },
        oldLines: { type: 'integer' },
        newStart: { type: 'integer' },
        newLines: { type: 'integer' },
        lines: { type: 'array', items: { type: 'ref', ref: '#diffLine' } },
      },
    },
    fileDiff: {
      type: 'object',
      description:
        'One file a commit changed. Hunks are absent for a binary file or one over the inline size; the page then shows the sizes. An image on either side is readable through getRaw at the commit or its parent.',
      required: ['path', 'status', 'binary', 'additions', 'deletions'],
      nullable: [
        'oldMode',
        'newMode',
        'oldSize',
        'newSize',
        'mediaType',
        'hunks',
        'oldText',
        'newText',
      ],
      properties: {
        path: { type: 'string' },
        status: {
          type: 'string',
          knownValues: ['added', 'deleted', 'modified'],
        },
        oldMode: { type: 'string' },
        newMode: { type: 'string' },
        oldSize: { type: 'integer' },
        newSize: { type: 'integer' },
        binary: { type: 'boolean' },
        mediaType: { type: 'string' },
        hunks: { type: 'array', items: { type: 'ref', ref: '#diffHunk' } },
        oldText: {
          type: 'string',
          description:
            'The whole file before the change, present with hunks, so a page colours each side whole.',
        },
        newText: { type: 'string' },
        additions: { type: 'integer' },
        deletions: { type: 'integer' },
      },
    },
    commitDetail: {
      type: 'object',
      required: [
        'sha',
        'parents',
        'author',
        'authoredAt',
        'committedAt',
        'message',
        'files',
      ],
      properties: {
        sha: { type: 'string', minLength: 40, maxLength: 64 },
        parents: { type: 'array', items: { type: 'string' } },
        author: { type: 'string' },
        authoredAt: { type: 'integer' },
        committedAt: { type: 'integer' },
        message: { type: 'string' },
        files: { type: 'array', items: { type: 'ref', ref: '#fileDiff' } },
      },
    },
    profileView: {
      type: 'object',
      description: 'An atproto account as its profile shows it.',
      required: ['did', 'handle', 'displayName', 'avatar'],
      properties: {
        did: { type: 'string', format: 'did' },
        handle: { type: 'string', format: 'handle' },
        displayName: { type: 'string', description: 'Empty where unset.' },
        avatar: { type: 'string', description: 'A URL, or empty.' },
      },
    },
    claimView: {
      type: 'object',
      description:
        'One identity the account claims in dev.pdsjs.git.identity, with the profile behind it resolved. An empty did is the account itself; a label names a tool or a person with no account.',
      required: ['name', 'email', 'label', 'did', 'avatar'],
      nullable: ['profile'],
      properties: {
        name: { type: 'string' },
        email: { type: 'string' },
        label: { type: 'string' },
        did: { type: 'string' },
        avatar: {
          type: 'string',
          description: 'The URL of the face the claim carries, or empty.',
        },
        profile: { type: 'ref', ref: '#profileView' },
      },
    },
    checkView: {
      type: 'object',
      description:
        'The latest run of one workflow on one branch, as the runner records it in dev.pdsjs.git.latestCheck, with the branch it belongs to.',
      required: ['ref', 'workflow', 'sha', 'status', 'startedAt'],
      properties: {
        ref: { type: 'string', description: 'Full branch ref name.' },
        workflow: { type: 'string', maxLength: 512 },
        subject: { type: 'ref', ref: 'dev.pdsjs.git.check#subject' },
        check: { type: 'ref', ref: 'dev.pdsjs.git.check#subject' },
        sha: { type: 'string', minLength: 40, maxLength: 64 },
        status: {
          type: 'string',
          knownValues: ['running', 'success', 'failure', 'error'],
        },
        startedAt: { type: 'string', format: 'datetime' },
        finishedAt: { type: 'string', format: 'datetime' },
      },
    },
    contributorView: {
      type: 'object',
      description:
        'An account whose copy or statements are read, and where from.',
      required: ['did', 'handle', 'pds'],
      properties: {
        did: { type: 'string', format: 'did' },
        handle: {
          type: 'string',
          description: 'The handle its DID document publishes, or the DID.',
        },
        pds: { type: 'string', format: 'uri' },
      },
    },
    pullRequestView: {
      type: 'object',
      description:
        "A branch in one account's copy, placed against canonical main. Every number is read from the object graph on this read.",
      required: [
        'id',
        'author',
        'branch',
        'title',
        'sha',
        'when',
        'ahead',
        'behind',
        'merged',
        'rooted',
        'files',
        'fixes',
      ],
      properties: {
        id: {
          type: 'string',
          description: "The author's DID and the branch, space separated.",
        },
        author: { type: 'ref', ref: '#contributorView' },
        branch: { type: 'string', description: 'Short ref name.' },
        title: {
          type: 'string',
          description: "The author's title, or the tip commit's subject.",
        },
        sha: { type: 'string', minLength: 40, maxLength: 64 },
        when: {
          type: 'integer',
          description: "The tip commit's commit date, epoch milliseconds.",
        },
        ahead: { type: 'integer', description: 'Commits main does not have.' },
        behind: {
          type: 'integer',
          description: 'Commits main has that this does not.',
        },
        base: { type: 'string', description: 'The merge base with main.' },
        merged: {
          type: 'boolean',
          description: 'Main already reaches this tip.',
        },
        rooted: {
          type: 'boolean',
          description: 'A merge base was found inside the walk.',
        },
        files: { type: 'array', items: { type: 'string' } },
        fixes: {
          type: 'array',
          items: { type: 'string', format: 'at-uri' },
          description: 'Issue records its commits claim to fix.',
        },
        drift: {
          type: 'array',
          items: { type: 'string' },
          description: 'Paths main has changed since it branched.',
        },
        conflicts: {
          type: 'array',
          items: { type: 'string' },
          description:
            'Paths where the branch and main changed the same thing differently, so a merge cannot put them together. Empty where a merge is clean.',
        },
        conflictsUnknown: {
          type: 'boolean',
          description:
            'The branch drifts in more files than the list read for conflicts, and none it read conflict, so whether the rest do is unread. The merge reads them all.',
        },
        fork: {
          type: 'boolean',
          description: 'From an account the repository has not named.',
        },
        status: {
          type: 'string',
          description: "The author's own word, draft or withdrawn.",
        },
        note: { type: 'string' },
        noteAt: { type: 'string', format: 'datetime' },
      },
    },
  },
};

/** @param {string} name @param {string} description */
const notFound = (name, description) => ({ name, description });

const REPO_NOT_FOUND = notFound(
  'RepoNotFound',
  'No repository of that name, or the cid given is not its current version.',
);
const NOT_FOUND = notFound(
  'NotFound',
  'The repository exists but the ref, path or commit named does not.',
);
const TOO_LARGE = notFound(
  'RepositoryTooLarge',
  'The repository carries no object indexes and its bundles are past what this server downloads whole. A client with a pack reader of its own reads the bundles instead.',
);

/**
 * @param {string} id
 * @param {Record<string, unknown>} main
 */
const doc = (id, main) => ({ lexicon: 1, id, defs: { main } });

/**
 * @param {Object} shape
 * @param {string} shape.description
 * @param {Record<string, unknown>} shape.properties
 * @param {string[]} [shape.required]
 * @param {Record<string, unknown>} shape.output
 * @param {Array<{name: string, description: string}>} [shape.errors]
 */
const query = ({ description, properties, required = [], output, errors }) => ({
  type: 'query',
  description,
  parameters: { type: 'params', required, properties },
  output: { encoding: 'application/json', schema: output },
  errors: errors ?? [REPO_NOT_FOUND, NOT_FOUND, TOO_LARGE],
});

const view = (/** @type {string} */ name) => ({
  type: 'ref',
  ref: `${GIT_DEFS}#${name}`,
});

export const gitMethodLexicons = [
  doc(
    'dev.pdsjs.git.listRepos',
    query({
      description:
        'The repositories this account hosts, newest push first. Anonymous, the public repo alone; with the account session, repositories held in spaces as well.',
      properties: {
        summary: {
          type: 'boolean',
          description:
            "Each public repository's open pull request and issue counts and its recent commit dates. Every one is a read per repository, so a reader asks for them only where they show.",
        },
      },
      output: {
        type: 'object',
        required: ['httpEnabled', 'repos'],
        properties: {
          httpEnabled: {
            type: 'boolean',
            description: 'Whether the server serves git smart HTTP.',
          },
          repos: { type: 'array', items: view('repoView') },
        },
      },
      errors: [],
    }),
  ),
  doc(
    'dev.pdsjs.git.getRepo',
    query({
      description: 'One hosted repository as the listing shows it.',
      required: ['repo'],
      properties: { repo: REPO_PARAMS.repo, space: REPO_PARAMS.space },
      output: view('repoView'),
      errors: [REPO_NOT_FOUND],
    }),
  ),
  doc(
    'dev.pdsjs.git.getTree',
    query({
      description: 'One directory of a hosted repository at a ref.',
      required: ['repo'],
      properties: {
        ...REPO_PARAMS,
        ref: REF_PARAM,
        path: {
          type: 'string',
          description: 'Slash-separated, empty for the root.',
        },
      },
      output: view('treeView'),
    }),
  ),
  doc(
    'dev.pdsjs.git.listFiles',
    query({
      description:
        'Every file at a ref with its size, read from the trees and the pack index alone, so no file body is fetched.',
      required: ['repo'],
      properties: { ...REPO_PARAMS, ref: REF_PARAM },
      output: {
        type: 'object',
        required: ['ref', 'files', 'truncated'],
        properties: {
          ref: { type: 'string' },
          files: { type: 'array', items: view('fileEntry') },
          truncated: {
            type: 'boolean',
            description: 'True when the walk stopped before the last file.',
          },
        },
      },
    }),
  ),
  doc(
    'dev.pdsjs.git.getFile',
    query({
      description: 'One file of a hosted repository at a ref, for display.',
      required: ['repo', 'path'],
      properties: { ...REPO_PARAMS, ref: REF_PARAM, path: { type: 'string' } },
      output: view('fileView'),
    }),
  ),
  doc('dev.pdsjs.git.getRaw', {
    type: 'query',
    description:
      'The bytes of one file at a ref. An image is served under its media type. Text is served as text/plain whatever the extension, so a page held in a repository cannot run as script on this origin; anything else as application/octet-stream.',
    parameters: {
      type: 'params',
      required: ['repo', 'path'],
      properties: {
        ...REPO_PARAMS,
        ref: REF_PARAM,
        path: { type: 'string' },
      },
    },
    output: { encoding: '*/*' },
    errors: [REPO_NOT_FOUND, NOT_FOUND, TOO_LARGE],
  }),
  doc(
    'dev.pdsjs.git.listCommits',
    query({
      description:
        'The history a ref reaches, newest first the way git log orders it.',
      required: ['repo'],
      properties: {
        ...REPO_PARAMS,
        ref: REF_PARAM,
        path: {
          type: 'string',
          maxLength: 4096,
          description:
            'A path in the tree. Given, the history is the commits that changed it, the way git log follows a path.',
        },
        limit: { type: 'integer', minimum: 1, maximum: 500, default: 50 },
        range: {
          type: 'string',
          maxLength: 129,
          description:
            'base..tip: the commits the tip reaches that the base does not, newest first. Takes the place of ref.',
        },
      },
      output: {
        type: 'object',
        required: ['ref', 'commits', 'truncated'],
        properties: {
          ref: { type: 'string' },
          commits: { type: 'array', items: view('commitView') },
          truncated: {
            type: 'boolean',
            description: 'True when the history continues past the limit.',
          },
        },
      },
    }),
  ),
  doc(
    'dev.pdsjs.git.getCommit',
    query({
      description:
        'One commit with the diff of every file it changed, or as base..tip the changes across a range as one diff.',
      required: ['repo', 'sha'],
      properties: {
        ...REPO_PARAMS,
        sha: { type: 'string', minLength: 40, maxLength: 129 },
      },
      output: view('commitDetail'),
    }),
  ),
  doc(
    'dev.pdsjs.git.getLastCommit',
    query({
      description:
        "The commit that last changed a path, the answer git log -1 -- path gives. The walk reads the bundle chain from the server's own storage.",
      required: ['repo'],
      properties: {
        ...REPO_PARAMS,
        ref: REF_PARAM,
        path: { type: 'string', description: 'Empty for the whole tree.' },
      },
      output: {
        type: 'object',
        required: ['commit'],
        nullable: ['commit'],
        properties: {
          commit: {
            ...view('commitView'),
            description: 'Null where nothing in the walk touched the path.',
          },
        },
      },
    }),
  ),
  doc(
    'dev.pdsjs.git.getAuthors',
    query({
      description:
        "The git identities this account claims as its own, with the profile behind each, and the account's own profile. A page shows a commit author under these. Profiles come from the Bluesky AppView and are kept five minutes.",
      properties: {},
      output: {
        type: 'object',
        required: ['owner', 'claims'],
        nullable: ['owner'],
        properties: {
          owner: view('profileView'),
          claims: { type: 'array', items: view('claimView') },
        },
      },
      errors: [],
    }),
  ),
  doc(
    'dev.pdsjs.git.listChecks',
    query({
      description:
        'The latest check on each branch of a repository, read from the runner the repository names. A runner whose PDS does not answer within five seconds costs the badge, not the answer.',
      required: ['repo'],
      properties: { repo: REPO_PARAMS.repo, space: REPO_PARAMS.space },
      output: {
        type: 'object',
        required: ['runner', 'checks'],
        nullable: ['runner'],
        properties: {
          runner: { type: 'string', format: 'did' },
          service: {
            type: 'string',
            format: 'uri',
            description: "The runner's PDS, where the check records are.",
          },
          unreachable: {
            type: 'boolean',
            description: "True when the runner's DID did not resolve.",
          },
          checks: { type: 'array', items: view('checkView') },
        },
      },
      errors: [REPO_NOT_FOUND],
    }),
  ),
  doc(
    'dev.pdsjs.git.listPulls',
    query({
      description:
        "Every pull request on a repository: a branch in one account's copy that canonical main does not reach is open, one it reaches is merged. The listed collaborators' copies are read whole from their own servers; anybody else is found through the server's discovery index and only the branch their record names is read. Open and merged are computed from the object graph on every read and never stored. A repository in a space answers empty until its copies are read from the space.",
      required: ['repo'],
      properties: { repo: REPO_PARAMS.repo, space: REPO_PARAMS.space },
      output: {
        type: 'object',
        required: [
          'open',
          'merged',
          'collaborators',
          'reviews',
          'checks',
          'configured',
        ],
        properties: {
          open: { type: 'array', items: view('pullRequestView') },
          merged: { type: 'array', items: view('pullRequestView') },
          collaborators: {
            type: 'array',
            items: view('contributorView'),
            description: 'The owner and every account the config names.',
          },
          reviews: {
            type: 'array',
            description:
              'By pull request id and by sha, the review records written against it: entries of [key, [record]].',
            items: { type: 'unknown' },
          },
          checks: {
            type: 'array',
            description:
              "By pull request id and by sha, the runner's runs against it: entries of [key, [run]].",
            items: { type: 'unknown' },
          },
          configured: {
            type: 'boolean',
            description: 'Whether the config names any collaborator.',
          },
        },
      },
      errors: [REPO_NOT_FOUND],
    }),
  ),
  doc(
    'dev.pdsjs.git.listIssues',
    query({
      description:
        "Everything said in a repository's issue threads: each statement a dev.pdsjs.git.issue record in its author's own repo naming this repository. The listed voices are read from their own servers; anybody else is found through the server's discovery index and read from theirs. Threading and state are the reader's to compute. A repository in a space answers empty until its statements are read from the space.",
      required: ['repo'],
      properties: { repo: REPO_PARAMS.repo, space: REPO_PARAMS.space },
      output: {
        type: 'object',
        required: ['statements'],
        properties: {
          statements: {
            type: 'array',
            description:
              "Each the record's own fields with uri, cid and by, the account that wrote it. indexed marks one found through the discovery index.",
            items: { type: 'unknown' },
          },
        },
      },
      errors: [REPO_NOT_FOUND],
    }),
  ),
  doc(
    'dev.pdsjs.git.listCheckRuns',
    query({
      description:
        "Every run the repository's runner has published about it, newest first, read from the runner's own server. listChecks answers the latest per branch; this is the history a checks page lists. A repository in a space has no runs.",
      required: ['repo'],
      properties: { repo: REPO_PARAMS.repo, space: REPO_PARAMS.space },
      output: {
        type: 'object',
        required: ['runner', 'runs'],
        nullable: ['runner'],
        properties: {
          runner: { type: 'string', format: 'did' },
          service: {
            type: 'string',
            format: 'uri',
            description:
              "The runner's PDS, where the run records and their logs are.",
          },
          unreachable: {
            type: 'boolean',
            description: "True when the runner's DID did not resolve.",
          },
          runs: {
            type: 'array',
            description:
              'Each a dev.pdsjs.git.check record with rkey, the record key that addresses its page.',
            items: { type: 'unknown' },
          },
        },
      },
      errors: [REPO_NOT_FOUND],
    }),
  ),
  doc('dev.pdsjs.git.createRepo', {
    type: 'procedure',
    description:
      'Create an empty repository: the record exists at once with no refs and no bundles, and the first push fills it. A private one lives in a space of the git type keyed by its own name, which the server makes.',
    input: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['name'],
        properties: {
          name: {
            type: 'string',
            maxLength: 512,
            description: 'Letters, digits, and . _ ~ -',
          },
          description: {
            type: 'string',
            maxLength: 3000,
            description: 'What the repository is for.',
          },
          private: {
            type: 'boolean',
            description:
              'Kept to the people the owner adds. The address is the same as a public repository.',
          },
        },
      },
    },
    output: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['name'],
        nullable: ['space'],
        properties: {
          name: { type: 'string' },
          space: { type: 'string', format: 'at-uri' },
        },
      },
    },
    errors: [
      { name: 'RepoExists', description: 'A repository of that name exists.' },
    ],
  }),
  doc('dev.pdsjs.git.renameRepo', {
    type: 'procedure',
    description:
      'Rename a repository: one field on its record. The record key is the identity, so every statement about the repository, every fork naming it upstream and every check still find it. What moves is the address, so clone remotes are re-pointed by hand. A repository in a space keeps its name: the space is what an address resolves to and is keyed by that name.',
    input: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['name', 'newName'],
        properties: {
          name: { type: 'string', maxLength: 512 },
          newName: {
            type: 'string',
            maxLength: 512,
            description: 'Letters, digits, and . _ ~ -',
          },
        },
      },
    },
    output: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['name', 'rkey'],
        properties: {
          name: { type: 'string' },
          rkey: {
            type: 'string',
            description: 'The record key, which the rename left alone.',
          },
        },
      },
    },
    errors: [
      REPO_NOT_FOUND,
      { name: 'RepoExists', description: 'A repository of that name exists.' },
      {
        name: 'SpaceRepository',
        description: 'A repository in a space is addressed by that space.',
      },
    ],
  }),
  doc('dev.pdsjs.git.deleteRepo', {
    type: 'procedure',
    description:
      'Delete a repository: the record, the config record beside it, and the bundle blobs neither of them holds any more. A repository holding a protected branch refuses; drop the protection first. A private one keeps its space, which is keyed by the name, and loses its member list with the repository.',
    input: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['name'],
        properties: {
          name: { type: 'string', maxLength: 512 },
          space: { type: 'string', format: 'at-uri' },
        },
      },
    },
    output: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['name'],
        nullable: ['space'],
        properties: {
          name: { type: 'string' },
          space: { type: 'string', format: 'at-uri' },
        },
      },
    },
    errors: [REPO_NOT_FOUND],
  }),
  doc('dev.pdsjs.git.updateSettings', {
    type: 'procedure',
    description:
      'Repository settings. Description and default branch live on the repo record; protected branches, the check runner and the linked packages live in the config record beside it, which pushes never touch. A field left out keeps its value.',
    input: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['name'],
        nullable: ['runner'],
        properties: {
          name: { type: 'string', maxLength: 512 },
          space: { type: 'string', format: 'at-uri' },
          description: {
            type: 'string',
            maxLength: 3000,
            description: 'Empty clears it.',
          },
          defaultBranch: {
            type: 'string',
            maxLength: 512,
            description: 'Empty clears it.',
          },
          protectedBranches: {
            type: 'array',
            items: { type: 'string', maxLength: 512 },
          },
          runner: {
            type: 'string',
            format: 'did',
            description: 'Null clears it.',
          },
          packages: {
            type: 'array',
            items: view('package'),
            description: 'An empty list clears them.',
          },
        },
      },
    },
    output: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['name'],
        nullable: ['space', 'runner'],
        properties: {
          name: { type: 'string' },
          space: { type: 'string', format: 'at-uri' },
          protectedBranches: { type: 'array', items: { type: 'string' } },
          runner: { type: 'string', format: 'did' },
          packages: { type: 'array', items: view('package') },
        },
      },
    },
    errors: [REPO_NOT_FOUND],
  }),
  doc('dev.pdsjs.git.requestCheck', {
    type: 'procedure',
    description:
      'Ask the runner the repository names to check a branch again. The runner reads the firehose and listens on no port, so the request is a dev.pdsjs.git.checkRequest record in this repo; the check appears in listChecks when the run publishes.',
    input: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['name', 'ref'],
        properties: {
          name: { type: 'string', maxLength: 512 },
          ref: {
            type: 'string',
            maxLength: 512,
            description: 'A branch, as refs/heads/<name>.',
          },
        },
      },
    },
    output: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['runner', 'ref'],
        properties: {
          runner: { type: 'string', format: 'did' },
          ref: { type: 'string' },
        },
      },
    },
    errors: [
      {
        name: 'NoRunner',
        description: 'The repository names no check runner.',
      },
    ],
  }),
  doc('dev.pdsjs.git.mergePull', {
    type: 'procedure',
    description:
      "Put a pull request's work on this repository's canonical branch. The work is a branch in another account's copy, on their own server; its objects are read from there and the ones this copy lacks ride in a new bundle. Nothing is written to their repository.",
    input: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['name', 'did', 'ref'],
        properties: {
          name: { type: 'string', maxLength: 512 },
          did: {
            type: 'string',
            format: 'did',
            description: 'The account whose copy holds the branch.',
          },
          ref: {
            type: 'string',
            maxLength: 512,
            description: 'Their branch, as refs/heads/<name>.',
          },
          mode: {
            type: 'string',
            knownValues: ['merge', 'squash'],
            description:
              'A merge writes a commit with both histories under it. A squash writes one commit carrying the change.',
          },
          handle: {
            type: 'string',
            maxLength: 512,
            description:
              "The branch owner's handle, which a merge commit's subject names.",
          },
          subject: { type: 'string', maxLength: 512 },
          body: { type: 'string', maxLength: 20000 },
        },
      },
    },
    output: {
      encoding: 'application/json',
      schema: {
        type: 'object',
        required: ['sha', 'ref', 'mode', 'objects'],
        properties: {
          sha: { type: 'string', description: 'The commit now at the tip.' },
          ref: { type: 'string', description: 'The canonical branch.' },
          mode: { type: 'string' },
          objects: {
            type: 'integer',
            description: 'How many objects the new bundle carries.',
          },
        },
      },
    },
  }),
];

/** The method NSIDs, in the order the lexicons list them. */
export const GIT_METHODS = gitMethodLexicons.map((lexicon) => lexicon.id);