Skip to content
File

Blob: src/worker/git/operations/validation.ts

typescript86 lines
1export type ReceiveCommand = {
2 oldOid: string;
3 newOid: string;
4 ref: string;
5};
6 
7export type ReceiveStatus = {
8 ref: string;
9 ok: boolean;
10 msg?: string;
11};
12 
13const ZERO_OID_RE = /^0{40}$/i;
14 
15export function isValidRefName(name: string): boolean {
16 if (!name || name === "HEAD") return false;
17 if (!name.startsWith("refs/")) return false;
18 if (/[\x00-\x20~^:?*\[]/.test(name)) return false;
19 if (name.includes("//") || name.startsWith("/") || name.endsWith("/")) return false;
20 if (name.split("/").some((component) => component === "." || component === "..")) return false;
21 if (name.endsWith(".") || name.endsWith(".lock")) return false;
22 if (name.includes("@{")) return false;
23 if (name.includes("\\")) return false;
24 return true;
25}
26 
27export function validateReceiveCommands(
28 currentRefs: Array<{ name: string; oid: string }>,
29 commands: ReceiveCommand[]
30): ReceiveStatus[] {
31 const refMap = new Map(currentRefs.map((ref) => [ref.name, ref.oid] as const));
32 const statuses: ReceiveStatus[] = [];
33 
34 for (const command of commands) {
35 const current = refMap.get(command.ref);
36 const isZeroOld = ZERO_OID_RE.test(command.oldOid);
37 const isZeroNew = ZERO_OID_RE.test(command.newOid);
38 
39 if (isZeroNew) {
40 if (!current) {
41 statuses.push({ ref: command.ref, ok: false, msg: "no such ref" });
42 continue;
43 }
44 if (current.toLowerCase() !== command.oldOid.toLowerCase()) {
45 statuses.push({ ref: command.ref, ok: false, msg: "stale old-oid" });
46 continue;
47 }
48 statuses.push({ ref: command.ref, ok: true });
49 continue;
50 }
51 
52 if (!current) {
53 if (!isZeroOld) {
54 statuses.push({ ref: command.ref, ok: false, msg: "expected zero old-oid" });
55 continue;
56 }
57 statuses.push({ ref: command.ref, ok: true });
58 continue;
59 }
60 
61 if (current.toLowerCase() !== command.oldOid.toLowerCase()) {
62 statuses.push({ ref: command.ref, ok: false, msg: "stale old-oid" });
63 continue;
64 }
65 
66 statuses.push({ ref: command.ref, ok: true });
67 }
68 
69 return statuses;
70}
71 
72export function applyReceiveCommands(
73 currentRefs: Array<{ name: string; oid: string }>,
74 commands: ReceiveCommand[]
75): Array<{ name: string; oid: string }> {
76 const refMap = new Map(currentRefs.map((ref) => [ref.name, ref.oid] as const));
77 for (const command of commands) {
78 if (ZERO_OID_RE.test(command.newOid)) {
79 refMap.delete(command.ref);
80 continue;
81 }
82 refMap.set(command.ref, command.newOid);
83 }
84 return Array.from(refMap, ([name, oid]) => ({ name, oid }));
85}