← Back
mainapp/controllers/PullController.php
<?php

class PullController
{
  private array $cfg;
  public function __construct(array $cfg){ $this->cfg=$cfg; }

  public function list(int $repoId): void {
    $db = db();
    $uid = auth_user_id();

    if (!repo_can_read($db, $repoId, $uid)) { http_response_code(403); exit("403"); }
    $repo = repo_get($db, $repoId);

    $st = $db->prepare("SELECT pr.*, u.username as author_name
                        FROM pull_requests pr
                        JOIN users u ON u.id = pr.author_id
                        WHERE pr.repo_id=?
                        ORDER BY pr.id DESC");
    $st->execute([$repoId]);
    $prs = $st->fetchAll();

    $canWrite = repo_can_write($db, $repoId, $uid);

    $title = $repo['owner_username'] . "/" . $repo['name'] . " / Pull Requests";
    require __DIR__ . '/../views/pulls/list.php';
  }

  public function showCreate(int $repoId): void {
    $db = db();
    $uid = require_auth();
    if (!repo_can_write($db, $repoId, $uid)) { http_response_code(403); exit("403"); }
    $repo = repo_get($db, $repoId);

    $st = $db->prepare("SELECT name FROM branches WHERE repo_id=? ORDER BY name ASC");
    $st->execute([$repoId]);
    $branches = $st->fetchAll();

    $title = $repo['owner_username'] . "/" . $repo['name'] . " / New PR";
    require __DIR__ . '/../views/pulls/create.php';
  }

  public function create(int $repoId): void {
    $db = db();
    $uid = require_auth();
    csrf_verify();
    if (!repo_can_write($db, $repoId, $uid)) { http_response_code(403); exit("403"); }

    $repo = repo_get($db, $repoId);

    $title = trim($_POST['title'] ?? '');
    $body = trim($_POST['body'] ?? '');
    $source = safe_branch($_POST['source_branch'] ?? '');
    $target = safe_branch($_POST['target_branch'] ?? '');

    if ($title === '' || strlen($title) > 255) {
      flash_set('err','Title required (max 255).');
      redirect("/r/$repoId/pulls/new");
    }
    if ($source === $target) {
      flash_set('err','Source and target branches must be different.');
      redirect("/r/$repoId/pulls/new");
    }

    $st = $db->prepare("SELECT COALESCE(MAX(number),0)+1 as nextn FROM pull_requests WHERE repo_id=?");
    $st->execute([$repoId]);
    $next = (int)($st->fetch()['nextn'] ?? 1);

    $st = $db->prepare("INSERT INTO pull_requests
      (repo_id, number, title, body, state, source_branch, target_branch, author_id, created_at, updated_at)
      VALUES (?,?,?,?, 'open', ?, ?, ?, ?, ?)");
    $st->execute([$repoId, $next, $title, $body, $source, $target, $uid, now(), now()]);

    flash_set('ok', "PR created (#$next).");
    redirect("/r/$repoId/pulls/$next");
  }

  public function show(int $repoId, int $number): void {
    $db = db();
    $uid = auth_user_id();
    if (!repo_can_read($db, $repoId, $uid)) { http_response_code(403); exit("403"); }

    $repo = repo_get($db, $repoId);

    $st = $db->prepare("SELECT pr.*, u.username as author_name
                        FROM pull_requests pr
                        JOIN users u ON u.id = pr.author_id
                        WHERE pr.repo_id=? AND pr.number=? LIMIT 1");
    $st->execute([$repoId, $number]);
    $pr = $st->fetch();
    if (!$pr) { http_response_code(404); exit("PR not found"); }

    $blobs = new BlobStore($this->cfg['storage_base']);
    $engine = new RepoEngine($blobs);
    $diffEngine = new DiffEngine($blobs);

    $sourceHead = $engine->getBranchHead($db, $repoId, $pr['source_branch']);
    $targetHead = $engine->getBranchHead($db, $repoId, $pr['target_branch']);

    $snapA = $targetHead ? $engine->getSnapshot($db, (int)$targetHead) : [];
    $snapB = $sourceHead ? $engine->getSnapshot($db, (int)$sourceHead) : [];

    $changes = $diffEngine->diffSnapshots($snapA, $snapB);

    $canWrite = repo_can_write($db, $repoId, $uid);

    $title = $repo['owner_username'] . "/" . $repo['name'] . " / PR #" . $number;
    require __DIR__ . '/../views/pulls/show.php';
  }

  public function merge(int $repoId, int $number): void {
    $db = db();
    $uid = require_auth();
    csrf_verify();
    if (!repo_can_write($db, $repoId, $uid)) { http_response_code(403); exit("403"); }

    $st = $db->prepare("SELECT * FROM pull_requests WHERE repo_id=? AND number=? LIMIT 1");
    $st->execute([$repoId, $number]);
    $pr = $st->fetch();
    if (!$pr) { http_response_code(404); exit("PR not found"); }
    if ($pr['state'] !== 'open') { flash_set('err','PR is not open.'); redirect("/r/$repoId/pulls/$number"); }

    $blobs = new BlobStore($this->cfg['storage_base']);
    $engine = new RepoEngine($blobs);

    $sourceHead = $engine->getBranchHead($db, $repoId, $pr['source_branch']);
    if (!$sourceHead) { flash_set('err','Source branch has no commits.'); redirect("/r/$repoId/pulls/$number"); }

    $st = $db->prepare("UPDATE branches SET head_commit_id=? WHERE repo_id=? AND name=?");
    $st->execute([(int)$sourceHead, $repoId, $pr['target_branch']]);

    $st = $db->prepare("UPDATE pull_requests SET state='merged', updated_at=? WHERE id=?");
    $st->execute([now(), (int)$pr['id']]);

    flash_set('ok', "PR merged (fast-forward).");
    redirect("/r/$repoId/pulls/$number");
  }
}