139 lines · 4.1 KB
1 /**
2 * POST /:owner/:repo.git/git-upload-pack
3 *
4 * Handles git fetch/clone. Negotiates which objects the client needs,
5 * then generates and streams a pack file.
6 */
7
8 import {
9 pktLineParse,
10 pktLineEncode,
11 pktFlush,
12 pktConcat,
13 pktSideBandEncode,
14 pktLineDataToString,
15 SIDE_BAND,
16 } from '@gitfastr/shared';
17 import { parseCommit, type GitObjectType } from '@gitfastr/shared/git/object.js';
18 import { generatePack, type PackInput } from '@gitfastr/shared/git/pack-generate.js';
19 import { authenticate } from '../../middleware/auth.js';
20 import { RepoService } from '../../services/repo-service.js';
21 import { ObjectStore } from '../../services/object-store.js';
22 import type { HandlerContext } from '../../router.js';
23
24 const encoder = new TextEncoder();
25
26 export async function handleUploadPack(
27 request: Request,
28 hctx: HandlerContext
29 ): Promise<Response> {
30 const { owner, repo: repoName } = hctx.params;
31 const repoService = new RepoService(hctx.env.DB);
32 const { repo } = await repoService.getByOwnerAndName(owner, repoName);
33
34 // Auth check for private repos
35 const userId = await authenticate(request, hctx.env.DB);
36 const hasAccess = await repoService.hasReadAccess(repo, userId);
37 if (!hasAccess) {
38 return new Response('', {
39 status: 401,
40 headers: { 'WWW-Authenticate': 'Basic realm="gitfastr"' },
41 });
42 }
43
44 // Parse request body
45 const body = new Uint8Array(await request.arrayBuffer());
46 const { lines } = pktLineParse(body);
47
48 const wants = new Set<string>();
49 const haves = new Set<string>();
50 let done = false;
51
52 for (const line of lines) {
53 const str = pktLineDataToString(line);
54 if (!str) continue;
55
56 if (str.startsWith('want ')) {
57 // "want <sha> [capabilities]"
58 const sha = str.split(' ')[1];
59 wants.add(sha);
60 } else if (str.startsWith('have ')) {
61 haves.add(str.split(' ')[1]);
62 } else if (str === 'done') {
63 done = true;
64 }
65 }
66
67 if (wants.size === 0) {
68 return new Response(pktConcat(pktLineEncode('NAK\n'), pktFlush()), {
69 status: 200,
70 headers: { 'Content-Type': 'application/x-git-upload-pack-result' },
71 });
72 }
73
74 const objectStore = new ObjectStore(hctx.env.OBJECTS, owner, repoName);
75
76 // Walk the commit graph from wanted SHAs to collect needed objects
77 const needed = new Set<string>();
78 const visited = new Set<string>();
79 const queue = [...wants];
80
81 while (queue.length > 0) {
82 const sha = queue.pop()!;
83 if (visited.has(sha) || haves.has(sha)) continue;
84 visited.add(sha);
85 needed.add(sha);
86
87 const obj = await objectStore.get(sha);
88 if (!obj) continue;
89
90 if (obj.type === 'commit') {
91 const commit = parseCommit(obj.content);
92 // Add tree
93 queue.push(commit.tree);
94 // Add parent commits
95 for (const parent of commit.parents) {
96 queue.push(parent);
97 }
98 } else if (obj.type === 'tree') {
99 // Walk tree entries to find sub-trees and blobs
100 const { parseTree } = await import('@gitfastr/shared/git/object.js');
101 const entries = parseTree(obj.content);
102 for (const entry of entries) {
103 queue.push(entry.sha);
104 }
105 }
106 // Blobs and tags are leaf objects — no children to walk
107 }
108
109 // Fetch all needed objects for pack generation
110 const packInputs: PackInput[] = [];
111 for (const sha of needed) {
112 const obj = await objectStore.get(sha);
113 if (obj) {
114 packInputs.push({ type: obj.type, content: obj.content });
115 }
116 }
117
118 // Generate pack file
119 const packData = await generatePack(packInputs);
120
121 // Build response: NAK + side-band pack data + flush
122 const parts: Uint8Array[] = [];
123 parts.push(pktLineEncode('NAK\n'));
124
125 // Send pack data in chunks via side-band-64k (max 65519 bytes per packet)
126 const MAX_DATA_PER_PACKET = 65519 - 1; // minus 1 for channel byte
127 for (let i = 0; i < packData.length; i += MAX_DATA_PER_PACKET) {
128 const chunk = packData.slice(i, i + MAX_DATA_PER_PACKET);
129 parts.push(pktSideBandEncode(SIDE_BAND.DATA, chunk));
130 }
131
132 parts.push(pktFlush());
133
134 return new Response(pktConcat(...parts), {
135 status: 200,
136 headers: { 'Content-Type': 'application/x-git-upload-pack-result' },
137 });
138 }
139