Skip to content

DocsRepoService

reference
3 min readUpdated

Kind: Service

Source: atloria-monorepo/apps/api/src/docs-repo/docs-repo.service.ts

DocsRepoService — the low-level git primitive for the per-project docs substrate.

One NON-BARE git repo per project lives at ${DOCS_REPO_ROOT}/<projectId> (default /work/docs-repos, overridable for tests). Every mutation goes through git so the repo IS the source of record for content history; Postgres remains the authority the repo is rebuildable from (see ReconcilerService).

ALL git invocation is via promisified execFile('git', [args], { cwd }) — an argv array, never a shell string — so a slug/path/message can never inject a shell command.

DocsRepoService manages the non-bare Git repository that backs each project's documentation content under ${DOCS_REPO_ROOT}/<projectId>. It safely initializes repositories, normalizes and writes document trees, and exposes Git-derived state such as tree hashes and normalizer versions; higher-level reconciliation can rebuild repository state from Postgres when needed.

Methods

MethodSignatureReturnsDescription
normalizerVersionTagnormalizerVersionTag()stringThe normalizer version this service stamps on commits (see driftCheck).
repoRootrepoRoot()stringRoot under which every project's repo lives.
repoPathrepoPath(projectId: string)stringAbsolute path to a project's working tree.
normalizeContentnormalizeContent(markdown: string)stringExpose the canonicaliser so callers (drift compare) normalise DB content identically.
repoRelPathrepoRelPath(p: string)stringThe EXACT git-normalised repo path a write would use (safe-relative + forward slashes).
existsexists(projectId: string)Promise<boolean>True once git init has run for this project.
ensureRepoensureRepo(projectId: string)Promise<void>Idempotent: create the repo if absent (git init on main, typed generator identity, one empty root commit so the tree is never in an unborn-HEAD state).
writeFileswriteFiles(projectId: string, files: RepoFile[], author: CommitAuthor, message: string, branch: string, opts: CommitOptions)Promise<string>Normalise + write each file, stage it, and commit with the TYPED author.
writeTreewriteTree(projectId: string, files: RepoFile[], author: CommitAuthor, message: string, branch: string, sidecar: RepoFile[], opts: CommitOptions, snippetFiles: RepoFile[])Promise<string>FULL-TREE projection under docs/ (the reconciler's rebuild primitive).
headNormalizerVersionheadNormalizerVersion(projectId: string)`Promise<stringnull>`
readFilereadFile(projectId: string, filePath: string, ref: unknown)Promise<string>Read a file's content at ref (default HEAD) via git show.
resolveSlugAliasresolveSlugAlias(projectId: string, oldSlug: string, scope: string)`Promise<stringnull>`
listFileslistFiles(projectId: string, ref: unknown)Promise<string[]>All tracked file paths at ref (default HEAD).
headCommitheadCommit(projectId: string)`Promise<stringnull>`
checkoutcheckout(projectId: string, ref: string)Promise<void>Check out ref; if it's an unknown branch name, create it from current HEAD.
checkoutForcecheckoutForce(projectId: string, ref: string)Promise<void>FORCE check out an EXISTING ref, discarding any uncommitted working-tree changes.
createBranchcreateBranch(projectId: string, name: string, fromRef: string)Promise<void>Create a branch (from fromRef or current HEAD) without switching to it.
mergemerge(projectId: string, branch: string, message: string)Promise<void>Merge branch into the current branch (fast-forward or a merge commit).
tagtag(projectId: string, name: string, ref: string, message: string)Promise<void>Create a tag (annotated when a message is given) at ref or current HEAD.
commitDetachedTreecommitDetachedTree(projectId: string, files: RepoFile[], tagName: string, message: string)Promise<string>Create a git TAG whose commit captures files as a full docs/ tree, WITHOUT touching HEAD, the working tree, or the repo index — via a throwaway GIT_INDEX…

Dependencies

  • ConfigService
  • NormalizeFn

Where it refuses work

  • DocsRepoService stops the work with Error when !gitPath.startsWith('snippets/').
  • DocsRepoService stops the work with Error when !gitPath.startsWith('.atloria/').
  • DocsRepoService stops the work with Error when !seg || seg.includes('/') || seg.includes('\\') || seg === '.' || seg === '..'.
  • DocsRepoService stops the work with Error when !p — “Empty file path”.
  • DocsRepoService stops the work with Error when path.isAbsolute(rel) || rel.split(path.sep).some((s) => s === '..').
  • DocsRepoService stops the work with an early return when !(await this.hasStagedChanges(cwd)), in 2 places.

When something fails

  • DocsRepoService handles failure in 8 places: it turns it into a return value in 6, logs it and continues in 1, and lets it reach the caller in 1.

Diagram

mermaid
sequenceDiagram
  participant Caller as Reconciler / Docs Service
  participant Repo as DocsRepoService
  participant FS as Project Docs Repository
  participant Git as git (execFile)

  Caller->>Repo: ensureRepo(projectId)
  Repo->>FS: Check repository path
  alt Repository does not exist
    Repo->>Git: git init (argv array)
    Git-->>Repo: Repository initialized
  end
  Repo-->>Caller: Ready

  Caller->>Repo: writeTree(projectId, files)
  Repo->>Repo: normalizeContent(content)
  Repo->>Repo: repoRelPath(path)
  Repo->>FS: Write normalized files
  Repo->>Git: git add / write-tree
  Git-->>Repo: tree SHA
  Repo-->>Caller: tree SHA

Usage

ts
import { DocsRepoService } from './docs-repo.service';

// Typically injected by NestJS into a reconciler or document mutation service.
export class ProjectDocsWriter {
  constructor(private readonly docsRepoService: DocsRepoService) {}

  async writeProjectDocs(projectId: string) {
    await this.docsRepoService.ensureRepo(projectId);

    const treeSha = await this.docsRepoService.writeTree(projectId, [
      {
        path: 'README.md',
        content: '# Project Documentation\n\nWelcome to the project docs.\n',
      },
      {
        path: 'guides/getting-started.md',
        content: '# Getting Started\n\nInstall the application and configure it.\n',
      },
    ]);

    return { projectId, treeSha };
  }
}

AI Coding Instructions

  • Route all repository mutations through DocsRepoService; do not write directly to project documentation repositories from callers.
  • Preserve the execFile('git', args, { cwd }) pattern for every Git command. Never construct shell command strings from project IDs, paths, or commit messages.
  • Validate and convert document paths with repoRelPath() before file operations to prevent writes outside the project repository.
  • Normalize content with normalizeContent() before writing so generated trees are deterministic and compatible with normalizer-version checks.
  • Treat Git as the content-history record, while using reconciliation integrations to rebuild repository content from Postgres when repository state is missing or stale.

Relationships

  • DEPENDS_ON → configservice
  • DEPENDS_ON → NormalizeFn

Referenced By

  • DiffService (DEPENDS_ON)
  • DocsRepoModule (MODULE_PROVIDES)
  • DocsRepoModule (MODULE_EXPORTS)
  • ReconcilerService (DEPENDS_ON)
  • GitSyncMirrorService (DEPENDS_ON)
  • PublicProjectController (DEPENDS_ON)

Was this page helpful?

Download as PDF