main • app/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;
}
}
}