Skip to content

Git integration follow-up #30

@JanJakes

Description

@JanJakes

Tracking follow-up feedback for the Git integration PR series. I scanned the visible stack context around #11, #12, #13, #14, #16-#26, #28, and #29; this issue should collect findings by PR as review continues.

Some items below appear to be addressed by later stacked PRs, but they are still listed under #11 because they are observable in #11 as submitted.

PR #11: Allow Git pushes to create COW branches

Review target: #11 / 9423bb45c6e6c9a8c12ef7d2da69a9b68d4c0388.

Findings

  1. Restore Git refs and branch storage on post-receive failures.

    GitEndpoint mutates the persistent Git refs before COW materialization runs, but the general post-receive failure path only returns HTTP 500:

    $request_bytes = file_get_contents('php://input');
    $response = new StreamingResponseWriter();
    try {
    $endpoint = new GitEndpoint($repo);
    $endpoint->handle_request($endpoint_path, $request_bytes, $response);
    } catch (\Throwable $e) {
    error_log("COW git server error: " . $e->getMessage() . "\n" . $e->getTraceAsString());
    throw $e;
    }
    if ($is_post_receive) {
    try {
    cow_git_apply_all_refs_to_branches(
    $repo,
    $git_repo_dir,
    $branches_dir,
    $storage_branches_dir,
    $branch_list_path,
    $file_view,
    $debug_log,
    $pre_receive_refs
    );
    } catch (\Throwable $e) {
    if (ob_get_level() > 0) {
    ob_end_clean();
    }
    error_log("COW push processing error: " . $e->getMessage() . "\n" . $e->getTraceAsString());
    http_response_code(500);
    header('Content-Type: text/plain');
    $short = substr(str_replace(["\n", "\r"], ' ', $e->getMessage()), 0, 500);
    echo "forkpress cow: push rejected: $short\n";
    return;
    }

    The only ref restore path is the unsupported-name branch:

    if ($unsupported) {
    cow_git_restore_head_refs($git_repo_dir, $pre_receive_refs, $current_refs);

    For valid branch names, any failure in source inference, COW clone, apply, publish, config rewrite, or branches.txt write leaves the pushed ref in .forkpress/cow/git. After rename($tmp, $dest_storage), failures can also leave published storage or a public link because the catch block only removes $tmp:

    if (!rename($tmp, $dest_storage)) {
    throw new \RuntimeException("failed to publish git-created branch '$branch'");
    }
    if (cow_git_normalize_path($dest_storage) !== cow_git_normalize_path($dest_public)) {
    if (!symlink($dest_storage, $dest_public)) {
    cow_git_remove_tree($dest_storage);
    throw new \RuntimeException("failed to link git-created branch '$branch' into public branch directory");
    }
    }
    cow_git_rewrite_wp_config($dest_public, $debug_log);
    cow_git_write_branch_list($branches_dir, $branch_list_path);
    error_log("ForkPress COW git created branch '$branch' from '$source'");
    return $dest_public;
    } catch (\Throwable $e) {
    cow_git_remove_tree($tmp);
    throw $e;

    Expected follow-up: restore pre_receive_refs for any post-receive apply failure, and roll back any published storage/public branch paths after the staging directory has been renamed. Add tests for a valid branch push that fails after the Git ref is written and after branch storage is published.

  2. Resync the Git view before returning a successful push.

    The new e2e coverage edits database.sql during Git-created branch creation, but only verifies that the runtime branch did not materialize database.sql:

    git -C "$TMP/checkout" checkout -B git-created origin/main
    printf "created through git\n" > "$TMP/checkout/wordpress/wp-content/git-created.txt"
    printf "\n-- ignored git-created database.sql edit\n" >> "$TMP/checkout/database.sql"
    "$BIN" commit "$TMP/checkout" --message "create cow branch through git"
    test -f "$WORK/git-created/wp-load.php"
    test -f "$WORK/git-created/wp-content/git-created.txt"
    grep -F "created through git" "$WORK/git-created/wp-content/git-created.txt" >/dev/null
    test ! -e "$WORK/main/wp-content/git-created.txt"
    test ! -e "$WORK/git-created/database.sql"

    In [codex] Allow Git pushes to create COW branches #11, post-receive applies the WordPress files and then flushes the buffered receive-pack response without regenerating the COW Git snapshot:

    if ($is_post_receive) {
    try {
    cow_git_apply_all_refs_to_branches(
    $repo,
    $git_repo_dir,
    $branches_dir,
    $storage_branches_dir,
    $branch_list_path,
    $file_view,
    $debug_log,
    $pre_receive_refs
    );
    } catch (\Throwable $e) {
    if (ob_get_level() > 0) {
    ob_end_clean();
    }
    error_log("COW push processing error: " . $e->getMessage() . "\n" . $e->getTraceAsString());
    http_response_code(500);
    header('Content-Type: text/plain');
    $short = substr(str_replace(["\n", "\r"], ' ', $e->getMessage()), 0, 500);
    echo "forkpress cow: push rejected: $short\n";
    return;
    }
    if (ob_get_level() > 0) {
    ob_end_flush();
    }

    That means the remote ref can temporarily point at the client-pushed commit containing ignored database.sql edits, while the materialized preview ignored those edits. A later fetch may normalize the ref, but immediately after forkpress commit the client and server-visible snapshot can disagree.

    Expected follow-up: after a successful apply, resnapshot the affected branches and update refs before returning success. Add coverage that the remote database.sql is regenerated, ignored paths disappear from the Git view immediately, and the checkout is not left dirty/diverged after forkpress commit.

  3. Coordinate Git-created branch mutations with the COW operation lock.

    [codex] Allow Git pushes to create COW branches #11 protects .forkpress/cow/git with git.lock, but branch creation also mutates materialized branch storage, public branch links, wp-config.php, and branches.txt:

    cow_git_mkdir(dirname($git_repo_dir));
    $lock_path = $git_repo_dir . '.lock';
    $lock = fopen($lock_path, 'c');
    if (!$lock) {
    http_response_code(500);
    echo "Cannot open COW git lock\n";
    return;
    }
    if (!flock($lock, LOCK_EX)) {
    http_response_code(500);
    echo "Cannot lock COW git store\n";
    return;
    }
    and
    if (!rename($tmp, $dest_storage)) {
    throw new \RuntimeException("failed to publish git-created branch '$branch'");
    }
    if (cow_git_normalize_path($dest_storage) !== cow_git_normalize_path($dest_public)) {
    if (!symlink($dest_storage, $dest_public)) {
    cow_git_remove_tree($dest_storage);
    throw new \RuntimeException("failed to link git-created branch '$branch' into public branch directory");
    }
    }
    cow_git_rewrite_wp_config($dest_public, $debug_log);
    cow_git_write_branch_list($branches_dir, $branch_list_path);

    Those mutations need to be serialized with other COW branch lifecycle operations and with request-side reads of materialized branch trees. Otherwise a Git push can race forkpress branch create, agents setup, reset/delete follow-ups, or a preview request and expose half-published state.

    Expected follow-up: put Git apply/create under the same COW branch operation lock used by the CLI/runtime branch lifecycle paths, and cover a blocked request or concurrent branch operation in tests.

  4. Treat file apply failures as hard errors before publishing/listing a branch.

    cow_git_apply_wp_files() ignores the return values from file_put_contents() and unlink():

    function cow_git_apply_wp_files(GitRepository $repo, string $branch_root, array $wp_files): void {
    foreach ($wp_files as $rel => $blob_hash) {
    $target = $branch_root . '/' . $rel;
    if (is_file($target) && cow_git_file_blob_hash($target) === $blob_hash) {
    continue;
    }
    $parent = dirname($target);
    cow_git_mkdir($parent);
    file_put_contents($target, $repo->read_object($blob_hash)->consume_all());
    }
    $current = cow_git_collect_branch_files($branch_root);
    foreach ($current as $rel => $path) {
    if (!isset($wp_files[$rel]) && is_file($path)) {
    unlink($path);
    }

    PHP warnings do not make this path fail reliably. During Git-created branch creation, a failed write into the staging tree can still lead to publishing the branch and rewriting branches.txt, producing an incomplete preview branch that Git reported as pushed successfully.

    Expected follow-up: check write/delete results, throw with path context, and keep the branch unpublished on any apply error. Add tests for unwritable files or path-shape conflicts during apply.

  5. Use the same branch-name reservations as the other Git/branch creation paths.

    COW Git-created branches accept any single-label regex match:

    function cow_git_valid_branch_name(string $branch): bool {
    return (bool)preg_match('/^[A-Za-z0-9_-]{1,63}$/', $branch);
    }

    Other integration paths already reserve names such as www, admin, api, mail, localhost, and wp because they conflict with routing:

    // Reject reserved names that would conflict with HTTP routing.
    if (in_array(strtolower($branch_name), $reserved, true)) {
    throw new \RuntimeException(
    "refusing to push to reserved branch name '$branch_name' (conflicts with routing)"
    );
    }

    Expected follow-up: centralize or mirror the reservation check for COW Git-created branches, and make sure COW CLI branch creation follows the same rule if it does not already.

Open Question

  • Source inference selects an existing ForkPress branch from Git history, then clones that branch's current live storage. If a user fetched origin/main, the source branch's database changed, and then the user pushed a new branch based on the older fetched commit, [codex] Allow Git pushes to create COW branches #11 will apply the older pushed WordPress tree onto a clone of the current live database. Should Git-created branches require the pushed fork point to be the current source tip, or should the stale-base/database behavior be documented explicitly?

Verification Run

On the PR #11 head worktree:

  • php -l scripts/git_server/cow_server.php
  • php -l runtime/router_cow.php
  • bash -n tests/test_cow_strategy_e2e.sh
  • git diff --check aa9d633d335a40901f67cac0993af14461e059d1..HEAD
  • FORKPRESS_RUNTIME_BUNDLE=/dev/null cargo test -p forkpress (28 passed)

Not run for this review pass: make test-all, musl release build, or the full COW e2e script.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions