Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
protocol.ts4.2 KB · 133 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/**
 * Git Smart HTTP Protocol implementation.
 *
 * Handles the server side of:
 *   - GET  /:owner/:repo.git/info/refs?service=git-upload-pack
 *   - GET  /:owner/:repo.git/info/refs?service=git-receive-pack
 *   - POST /:owner/:repo.git/git-upload-pack
 *   - POST /:owner/:repo.git/git-receive-pack
 *
 * Reference: https://git-scm.com/docs/http-protocol
 */

import { getRepoPath } from "./repository";

function pktLine(data: string): string {
  const len = (data.length + 4).toString(16).padStart(4, "0");
  return `${len}${data}`;
}

function pktFlush(): string {
  return "0000";
}

export function infoRefsResponse(
  service: string,
  advertise: string
): Response {
  const body = pktLine(`# service=${service}\n`) + pktFlush() + advertise;

  return new Response(body, {
    headers: {
      "Content-Type": `application/x-${service}-advertisement`,
      "Cache-Control": "no-cache",
    },
  });
}

export async function getInfoRefs(
  owner: string,
  repo: string,
  service: string
): Promise<Response> {
  const repoDir = getRepoPath(owner, repo);
  const proc = Bun.spawn([service, "--stateless-rpc", "--advertise-refs", repoDir], {
    stdout: "pipe",
    stderr: "pipe",
  });
  const stdout = await new Response(proc.stdout).text();
  await proc.exited;
  return infoRefsResponse(service, stdout);
}

export async function serviceRpc(
  owner: string,
  repo: string,
  service: string,
  body: ReadableStream<Uint8Array> | ArrayBuffer | null,
  extraEnv?: Record<string, string>
): Promise<Response> {
  const repoDir = getRepoPath(owner, repo);
  const rawBytes = body
    ? body instanceof ArrayBuffer
      ? new Uint8Array(body)
      : new Uint8Array(await new Response(body).arrayBuffer())
    : new Uint8Array();

  // git gzips the negotiation body once it gets large enough (a full
  // clone/fetch against a repo with real history routinely crosses that
  // threshold; small requests like a shallow clone don't). We were piping
  // the raw, still-compressed bytes straight into `git <service>`'s stdin,
  // which choked with "fatal: protocol error: bad line length character" —
  // gzip's magic bytes don't parse as pkt-line. Sniff and decompress
  // rather than trust Content-Encoding, since it's the actual bytes that
  // matter and gzip's 2-byte magic number is unambiguous.
  const inputBytes =
    rawBytes.length >= 2 && rawBytes[0] === 0x1f && rawBytes[1] === 0x8b
      ? Bun.gunzipSync(rawBytes)
      : rawBytes;

  const proc = Bun.spawn([service, "--stateless-rpc", repoDir], {
    stdin: "pipe",
    stdout: "pipe",
    stderr: "pipe",
    env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
  });

  proc.stdin.write(inputBytes);
  proc.stdin.end();

  // git-receive-pack (push) callers install a temp pre-receive hook and
  // delete it the instant this call resolves, so pushes must keep waiting
  // for the subprocess to fully exit before we hand back a Response.
  // git-upload-pack (clone/fetch) has no such coupling, and buffering its
  // stdout via `.arrayBuffer()` was the bug: for a repo with any real
  // history the pack can be many MB, the child starts writing it to a
  // pipe with a small OS buffer well before we ever start reading, and
  // nothing drains stdout until the process exits -- on large enough
  // packs this deadlocks and the client sees the connection die mid-clone
  // (reproduced directly against this repo: shallow clone fine, full
  // clone/fetch "remote end hung up unexpectedly"). Stream it straight
  // through instead so the OS pipe stays drained the whole time.
  if (service === "git-upload-pack") {
    proc.exited.then((code) => {
      if (code !== 0) {
        new Response(proc.stderr)
          .text()
          .then((stderr) =>
            console.error(
              `[git] ${service} exited ${code} for ${owner}/${repo}: ${stderr}`
            )
          )
          .catch(() => {});
      }
    });

    return new Response(proc.stdout, {
      headers: {
        "Content-Type": `application/x-${service}-result`,
        "Cache-Control": "no-cache",
      },
    });
  }

  const stdout = await new Response(proc.stdout).arrayBuffer();
  await proc.exited;

  return new Response(stdout, {
    headers: {
      "Content-Type": `application/x-${service}-result`,
      "Cache-Control": "no-cache",
    },
  });
}