← Back
mainapp/services/RepoEngine.php
<?php

class RepoEngine
{
  private BlobStore $blobs;

  public function __construct(BlobStore $blobs)
  {
    $this->blobs = $blobs;
  }

  public function createRepo(PDO $db, int $ownerId, string $name, string $visibility='public', ?string $desc=null): int
  {
    $slug = safe_slug($name);
    $db->beginTransaction();
    try {
      $stmt = $db->prepare("INSERT INTO repositories (owner_id,name,slug,visibility,description,default_branch,created_at,updated_at)
                            VALUES (?,?,?,?,?,'main',?,?)");
      $stmt->execute([$ownerId, $name, $slug, $visibility, $desc, now(), now()]);
      $repoId = (int)$db->lastInsertId();

      $stmt = $db->prepare("INSERT INTO branches (repo_id,name,head_commit_id,created_at) VALUES (?,?,NULL,?)");
      $stmt->execute([$repoId, 'main', now()]);

      $this->blobs->ensureRepoDirs($repoId);

      $db->commit();
      return $repoId;
    } catch (Throwable $e) {
      $db->rollBack();
      throw $e;
    }
  }

  public function createBranch(PDO $db, int $repoId, string $fromBranch, string $newBranch): void
  {
    $fromBranch = safe_branch($fromBranch);
    $newBranch  = safe_branch($newBranch);

    $stmt = $db->prepare("SELECT head_commit_id FROM branches WHERE repo_id=? AND name=? LIMIT 1");
    $stmt->execute([$repoId, $fromBranch]);
    $row = $stmt->fetch();
    if (!$row) throw new RuntimeException("Source branch not found");

    $head = $row['head_commit_id'] ? (int)$row['head_commit_id'] : null;

    $stmt = $db->prepare("INSERT INTO branches (repo_id,name,head_commit_id,created_at) VALUES (?,?,?,?)");
    $stmt->execute([$repoId, $newBranch, $head, now()]);
  }

  public function getSnapshot(PDO $db, int $commitId): array
  {
    $stmt = $db->prepare("SELECT file_path, blob_id, is_deleted FROM commit_files WHERE commit_id=?");
    $stmt->execute([$commitId]);
    $map = [];
    while ($r = $stmt->fetch()) {
      if ((int)$r['is_deleted'] === 1) continue;
      $map[$r['file_path']] = (int)$r['blob_id'];
    }
    ksort($map);
    return $map;
  }

  public function getBranchHead(PDO $db, int $repoId, string $branch): ?int
  {
    $branch = safe_branch($branch);
    $stmt = $db->prepare("SELECT head_commit_id FROM branches WHERE repo_id=? AND name=? LIMIT 1");
    $stmt->execute([$repoId, $branch]);
    $row = $stmt->fetch();
    if (!$row) throw new RuntimeException("Branch not found");
    return $row['head_commit_id'] ? (int)$row['head_commit_id'] : null;
  }

  public function commit(PDO $db, int $repoId, string $branch, ?int $parentCommitId, int $authorId, string $message, array $changes): int
  {
    $branch = safe_branch($branch);
    $message = trim($message);
    if ($message === '' || strlen($message) > 255) throw new RuntimeException("Invalid message");

    $base = [];
    if ($parentCommitId) {
      $base = $this->getSnapshot($db, $parentCommitId);
    }

    foreach ($changes as $ch) {
      $p = safe_path($ch['path'] ?? '');
      if ($p === '') throw new RuntimeException("Missing file path");

      $del = !empty($ch['delete']);
      if ($del) {
        unset($base[$p]);
        continue;
      }

      $content = (string)($ch['content'] ?? '');
      $blob = $this->blobs->putBlob($db, $repoId, $content);
      $base[$p] = (int)$blob['blob_id'];
    }

    $db->beginTransaction();
    try {
      $stmt = $db->prepare("INSERT INTO commits (repo_id,branch_name,parent_commit_id,author_id,message,created_at)
                            VALUES (?,?,?,?,?,?)");
      $stmt->execute([$repoId, $branch, $parentCommitId, $authorId, $message, now()]);
      $commitId = (int)$db->lastInsertId();

      $stmtCF = $db->prepare("INSERT INTO commit_files (commit_id,file_path,blob_id,is_deleted) VALUES (?,?,?,0)");
      foreach ($base as $path => $blobId) {
        $stmtCF->execute([$commitId, $path, $blobId]);
      }

      $stmt = $db->prepare("UPDATE branches SET head_commit_id=? WHERE repo_id=? AND name=?");
      $stmt->execute([$commitId, $repoId, $branch]);

      $db->commit();
      return $commitId;
    } catch (Throwable $e) {
      $db->rollBack();
      throw $e;
    }
  }
}