nano-git 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +407 -0
- package/dist/backend/file-backend.d.mts +26 -0
- package/dist/backend/file-backend.mjs +83 -0
- package/dist/backend/file.d.mts +2 -0
- package/dist/backend/file.mjs +2 -0
- package/dist/backend/index.d.mts +2 -0
- package/dist/backend/index.mjs +1 -0
- package/dist/backend/memory-backend.d.mts +25 -0
- package/dist/backend/memory-backend.mjs +33 -0
- package/dist/backend/memory.d.mts +2 -0
- package/dist/backend/memory.mjs +2 -0
- package/dist/backend/types.d.mts +90 -0
- package/dist/core/errors.d.mts +124 -0
- package/dist/core/errors.mjs +168 -0
- package/dist/core/hash-digest.d.mts +35 -0
- package/dist/core/hash-digest.mjs +50 -0
- package/dist/core/hash-file.d.mts +20 -0
- package/dist/core/hash-file.mjs +27 -0
- package/dist/core/hash-path.d.mts +17 -0
- package/dist/core/hash-path.mjs +35 -0
- package/dist/core/types/odb.d.mts +63 -0
- package/dist/core/types/refs.d.mts +140 -0
- package/dist/core/types/refs.mjs +19 -0
- package/dist/core/types/shallow.d.mts +52 -0
- package/dist/core/types.d.mts +154 -0
- package/dist/core/types.mjs +43 -0
- package/dist/errors.d.mts +2 -0
- package/dist/errors.mjs +2 -0
- package/dist/hash-file.d.mts +2 -0
- package/dist/hash-file.mjs +2 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +14 -0
- package/dist/objects/author.d.mts +25 -0
- package/dist/objects/author.mjs +45 -0
- package/dist/objects/blob.d.mts +15 -0
- package/dist/objects/blob.mjs +20 -0
- package/dist/objects/codec.d.mts +46 -0
- package/dist/objects/codec.mjs +84 -0
- package/dist/objects/commit.d.mts +26 -0
- package/dist/objects/commit.mjs +160 -0
- package/dist/objects/index.d.mts +8 -0
- package/dist/objects/index.mjs +8 -0
- package/dist/objects/raw.d.mts +85 -0
- package/dist/objects/raw.mjs +111 -0
- package/dist/objects/tag.d.mts +26 -0
- package/dist/objects/tag.mjs +148 -0
- package/dist/objects/tree.d.mts +22 -0
- package/dist/objects/tree.mjs +66 -0
- package/dist/odb/file-utils.mjs +136 -0
- package/dist/odb/file.d.mts +22 -0
- package/dist/odb/file.mjs +66 -0
- package/dist/odb/memory.d.mts +22 -0
- package/dist/odb/memory.mjs +54 -0
- package/dist/pack/composite-store.d.mts +76 -0
- package/dist/pack/composite-store.mjs +133 -0
- package/dist/pack/constants.mjs +48 -0
- package/dist/pack/crc32.mjs +38 -0
- package/dist/pack/delta-apply.d.mts +21 -0
- package/dist/pack/delta-apply.mjs +71 -0
- package/dist/pack/delta-create.d.mts +28 -0
- package/dist/pack/delta-create.mjs +151 -0
- package/dist/pack/index.d.mts +16 -0
- package/dist/pack/index.mjs +13 -0
- package/dist/pack/object-header.d.mts +36 -0
- package/dist/pack/object-header.mjs +72 -0
- package/dist/pack/ofs-delta-offset.d.mts +35 -0
- package/dist/pack/ofs-delta-offset.mjs +59 -0
- package/dist/pack/pack-builder-types.d.mts +19 -0
- package/dist/pack/pack-builder.d.mts +52 -0
- package/dist/pack/pack-builder.mjs +80 -0
- package/dist/pack/pack-encoding.mjs +76 -0
- package/dist/pack/pack-index-reader.d.mts +57 -0
- package/dist/pack/pack-index-reader.mjs +90 -0
- package/dist/pack/pack-index-types.d.mts +16 -0
- package/dist/pack/pack-index-writer.d.mts +45 -0
- package/dist/pack/pack-index-writer.mjs +106 -0
- package/dist/pack/pack-reader-resolver.mjs +104 -0
- package/dist/pack/pack-reader-types.d.mts +31 -0
- package/dist/pack/pack-reader-types.mjs +22 -0
- package/dist/pack/pack-reader-utils.mjs +61 -0
- package/dist/pack/pack-reader.d.mts +81 -0
- package/dist/pack/pack-reader.mjs +171 -0
- package/dist/pack/pack-store-loader.mjs +79 -0
- package/dist/pack/pack-store-types.d.mts +16 -0
- package/dist/pack/pack-store.d.mts +55 -0
- package/dist/pack/pack-store.mjs +114 -0
- package/dist/pack/pack-writer.mjs +96 -0
- package/dist/pack/varint.d.mts +34 -0
- package/dist/pack/varint.mjs +57 -0
- package/dist/refs/file.d.mts +6 -0
- package/dist/refs/file.mjs +222 -0
- package/dist/refs/fs-utils.mjs +30 -0
- package/dist/refs/memory.d.mts +14 -0
- package/dist/refs/memory.mjs +104 -0
- package/dist/refs/names.d.mts +57 -0
- package/dist/refs/names.mjs +80 -0
- package/dist/refs/resolve.d.mts +33 -0
- package/dist/refs/resolve.mjs +55 -0
- package/dist/refs/shallow/file.d.mts +17 -0
- package/dist/refs/shallow/file.mjs +61 -0
- package/dist/refs/shallow/memory.d.mts +18 -0
- package/dist/refs/shallow/memory.mjs +33 -0
- package/dist/repository/core.d.mts +3 -0
- package/dist/repository/core.mjs +2 -0
- package/dist/repository/create.d.mts +22 -0
- package/dist/repository/create.mjs +43 -0
- package/dist/repository/file.d.mts +43 -0
- package/dist/repository/file.mjs +82 -0
- package/dist/repository/import/import-glob.mjs +44 -0
- package/dist/repository/import/import-plan-builder.mjs +625 -0
- package/dist/repository/import/import-session-types.d.mts +280 -0
- package/dist/repository/import/import-session.mjs +96 -0
- package/dist/repository/import/import-view.mjs +133 -0
- package/dist/repository/memory.d.mts +17 -0
- package/dist/repository/memory.mjs +33 -0
- package/dist/repository/ops/fetch-operations.mjs +20 -0
- package/dist/repository/ops/fetch-types.d.mts +82 -0
- package/dist/repository/ops/fetch-url.mjs +82 -0
- package/dist/repository/ops/fs-object-operations.mjs +31 -0
- package/dist/repository/ops/maintenance-operations.mjs +62 -0
- package/dist/repository/ops/maintenance-types.d.mts +42 -0
- package/dist/repository/ops/object-operations.mjs +65 -0
- package/dist/repository/ops/object-types.d.mts +109 -0
- package/dist/repository/ops/push-operations.mjs +16 -0
- package/dist/repository/ops/push-resolution.mjs +17 -0
- package/dist/repository/ops/push-types.d.mts +72 -0
- package/dist/repository/ops/push-url.mjs +45 -0
- package/dist/repository/ops/reachability.mjs +60 -0
- package/dist/repository/ops/ref-operations.mjs +86 -0
- package/dist/repository/ops/ref-types.d.mts +70 -0
- package/dist/repository/tree/tree-patch.d.mts +64 -0
- package/dist/repository/tree/tree-patch.mjs +268 -0
- package/dist/repository/tree/tree-walk.d.mts +46 -0
- package/dist/repository/tree/tree-walk.mjs +65 -0
- package/dist/repository/tree/tree-writer.mjs +68 -0
- package/dist/repository/types.d.mts +36 -0
- package/dist/sha1.d.mts +4 -0
- package/dist/sha1.mjs +4 -0
- package/dist/transport/client/receive-pack/http.d.mts +33 -0
- package/dist/transport/client/receive-pack/http.mjs +99 -0
- package/dist/transport/client/receive-pack/push-error.d.mts +23 -0
- package/dist/transport/client/receive-pack/push-error.mjs +32 -0
- package/dist/transport/client/receive-pack/push-pack-plan.d.mts +28 -0
- package/dist/transport/client/receive-pack/push-pack-plan.mjs +60 -0
- package/dist/transport/client/receive-pack/push-policy.d.mts +19 -0
- package/dist/transport/client/receive-pack/push-policy.mjs +64 -0
- package/dist/transport/client/receive-pack/push-ref-plan.d.mts +45 -0
- package/dist/transport/client/receive-pack/push-ref-plan.mjs +108 -0
- package/dist/transport/client/receive-pack/push-report.d.mts +28 -0
- package/dist/transport/client/receive-pack/push-report.mjs +84 -0
- package/dist/transport/client/receive-pack/push-request-plan.mjs +52 -0
- package/dist/transport/client/receive-pack/push.d.mts +32 -0
- package/dist/transport/client/receive-pack/push.mjs +97 -0
- package/dist/transport/client/receive-pack/request.d.mts +39 -0
- package/dist/transport/client/receive-pack/request.mjs +46 -0
- package/dist/transport/client/receive-pack/response.d.mts +26 -0
- package/dist/transport/client/receive-pack/response.mjs +52 -0
- package/dist/transport/client/receive-pack/result.d.mts +34 -0
- package/dist/transport/client/receive-pack/result.mjs +100 -0
- package/dist/transport/client/upload-pack/capability-advertisement.d.mts +56 -0
- package/dist/transport/client/upload-pack/capability-advertisement.mjs +130 -0
- package/dist/transport/client/upload-pack/fetch.d.mts +109 -0
- package/dist/transport/client/upload-pack/fetch.mjs +392 -0
- package/dist/transport/client/upload-pack/http.d.mts +29 -0
- package/dist/transport/client/upload-pack/http.mjs +79 -0
- package/dist/transport/client/upload-pack/ls-refs.d.mts +75 -0
- package/dist/transport/client/upload-pack/ls-refs.mjs +150 -0
- package/dist/transport/client/upload-pack/object-info.d.mts +65 -0
- package/dist/transport/client/upload-pack/object-info.mjs +111 -0
- package/dist/transport/client/upload-pack/types.d.mts +153 -0
- package/dist/transport/http/index.d.mts +3 -0
- package/dist/transport/http/index.mjs +2 -0
- package/dist/transport/http/smart-http.d.mts +46 -0
- package/dist/transport/http/smart-http.mjs +176 -0
- package/dist/transport/http/types.d.mts +27 -0
- package/dist/transport/index.d.mts +9 -0
- package/dist/transport/index.mjs +8 -0
- package/dist/transport/protocol/object-graph.d.mts +63 -0
- package/dist/transport/protocol/object-graph.mjs +149 -0
- package/dist/transport/protocol/pkt-line.d.mts +109 -0
- package/dist/transport/protocol/pkt-line.mjs +195 -0
- package/dist/transport/protocol/ref-advertisement.mjs +185 -0
- package/dist/transport/protocol/ref-collection.d.mts +38 -0
- package/dist/transport/protocol/ref-collection.mjs +63 -0
- package/dist/transport/protocol/ref-match.d.mts +39 -0
- package/dist/transport/protocol/ref-match.mjs +42 -0
- package/dist/transport/protocol/refspec.d.mts +44 -0
- package/dist/transport/protocol/refspec.mjs +79 -0
- package/dist/transport/protocol/side-band.d.mts +65 -0
- package/dist/transport/protocol/side-band.mjs +142 -0
- package/dist/transport/protocol/transport-capabilities.mjs +46 -0
- package/dist/transport/protocol/types.d.mts +148 -0
- package/dist/transport/protocol/update-refs.d.mts +68 -0
- package/dist/transport/protocol/update-refs.mjs +126 -0
- package/dist/transport/receive-pack.d.mts +11 -0
- package/dist/transport/receive-pack.mjs +11 -0
- package/dist/transport/server/receive-pack/advertise.d.mts +28 -0
- package/dist/transport/server/receive-pack/advertise.mjs +88 -0
- package/dist/transport/server/receive-pack/handler.d.mts +30 -0
- package/dist/transport/server/receive-pack/handler.mjs +156 -0
- package/dist/transport/server/receive-pack/index.d.mts +6 -0
- package/dist/transport/server/receive-pack/index.mjs +6 -0
- package/dist/transport/server/receive-pack/parse.d.mts +17 -0
- package/dist/transport/server/receive-pack/parse.mjs +80 -0
- package/dist/transport/server/receive-pack/report-status.mjs +64 -0
- package/dist/transport/server/receive-pack/service.d.mts +41 -0
- package/dist/transport/server/receive-pack/service.mjs +39 -0
- package/dist/transport/server/receive-pack/types.d.mts +56 -0
- package/dist/transport/server/receive-pack/types.mjs +25 -0
- package/dist/transport/server/receive-pack/unpack.mjs +119 -0
- package/dist/transport/server/upload-pack/advertise.d.mts +20 -0
- package/dist/transport/server/upload-pack/advertise.mjs +30 -0
- package/dist/transport/server/upload-pack/command.d.mts +43 -0
- package/dist/transport/server/upload-pack/command.mjs +56 -0
- package/dist/transport/server/upload-pack/fetch.d.mts +43 -0
- package/dist/transport/server/upload-pack/fetch.mjs +217 -0
- package/dist/transport/server/upload-pack/index.d.mts +7 -0
- package/dist/transport/server/upload-pack/index.mjs +7 -0
- package/dist/transport/server/upload-pack/ls-refs.d.mts +38 -0
- package/dist/transport/server/upload-pack/ls-refs.mjs +113 -0
- package/dist/transport/server/upload-pack/service.d.mts +40 -0
- package/dist/transport/server/upload-pack/service.mjs +51 -0
- package/dist/transport/server/upload-pack/types.d.mts +11 -0
- package/dist/transport/server/upload-pack/types.mjs +21 -0
- package/dist/transport/upload-pack.d.mts +7 -0
- package/dist/transport/upload-pack.mjs +6 -0
- package/package.json +98 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { GitError } from "../../core/errors.mjs";
|
|
2
|
+
//#region src/transport/protocol/pkt-line.ts
|
|
3
|
+
/**
|
|
4
|
+
* pkt-line 帧编解码
|
|
5
|
+
*
|
|
6
|
+
* Git 协议的基本传输单元。每条帧以 4 字符十六进制长度前缀开头,
|
|
7
|
+
* 后跟对应长度的负载数据。
|
|
8
|
+
*
|
|
9
|
+
* 帧类型:
|
|
10
|
+
* - 数据帧:长度前缀在 0004–FFF4 之间(含 4 字节前缀自身)
|
|
11
|
+
* - 0000:flush-pkt,表示结束
|
|
12
|
+
* - 0001:delimiter-pkt(协议 v2)
|
|
13
|
+
* - 0002:response-end-pkt(协议 v2)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const data = encodePktLine("hello");
|
|
18
|
+
* console.log(data.toString("hex")); // => "000968656c6c6f"
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @see https://github.com/git/git/blob/master/Documentation/technical/pkt-line.txt
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* pkt-line 协议错误
|
|
25
|
+
*
|
|
26
|
+
* 当 pkt-line 数据格式不符合 Git 协议规范时抛出。
|
|
27
|
+
*/
|
|
28
|
+
var PktLineError = class extends GitError {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(`pkt-line error: ${message}`);
|
|
31
|
+
this.name = "PktLineError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** pkt-line 特殊帧的十六进制编码 */
|
|
35
|
+
const FLUSH_PKT_HEX = "0000";
|
|
36
|
+
const DELIMITER_PKT_HEX = "0001";
|
|
37
|
+
const RESPONSE_END_PKT_HEX = "0002";
|
|
38
|
+
/** pkt-line 长度前缀的字节数 */
|
|
39
|
+
const LENGTH_PREFIX_BYTES = 4;
|
|
40
|
+
/** pkt-line 数据帧的最大总长度(含 4 字节前缀) */
|
|
41
|
+
const MAX_PACKET_SIZE = 65524;
|
|
42
|
+
/** pkt-line 数据帧的最大负载长度 */
|
|
43
|
+
const MAX_PAYLOAD_SIZE = MAX_PACKET_SIZE - LENGTH_PREFIX_BYTES;
|
|
44
|
+
/**
|
|
45
|
+
* 编码 pkt-line 数据帧
|
|
46
|
+
*
|
|
47
|
+
* 将 payload 编码为 pkt-line 格式:"XXXX<payload>"
|
|
48
|
+
* 其中 XXXX 是含 4 字节前缀的总长度(十六进制)。
|
|
49
|
+
*
|
|
50
|
+
* @param payload - 负载数据(字符串或 Buffer)
|
|
51
|
+
* @returns pkt-line 编码后的 Buffer
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const buf = encodePktLine("hello");
|
|
56
|
+
* console.log(buf.toString("utf-8")); // => "0009hello"
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
function encodePktLine(payload) {
|
|
60
|
+
const data = typeof payload === "string" ? Buffer.from(payload, "utf-8") : payload;
|
|
61
|
+
if (data.length > MAX_PAYLOAD_SIZE) throw new PktLineError(`Payload too large: ${data.length} bytes (max ${MAX_PAYLOAD_SIZE})`);
|
|
62
|
+
const prefix = (data.length + LENGTH_PREFIX_BYTES).toString(16).padStart(4, "0").toUpperCase();
|
|
63
|
+
return Buffer.concat([Buffer.from(prefix, "utf-8"), data]);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 生成 flush-pkt
|
|
67
|
+
*
|
|
68
|
+
* @returns Buffer("0000")
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const buf = encodeFlushPkt();
|
|
73
|
+
* console.log(buf.toString("utf-8")); // => "0000"
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
function encodeFlushPkt() {
|
|
77
|
+
return Buffer.from(FLUSH_PKT_HEX, "utf-8");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 生成 delimiter-pkt(协议 v2)
|
|
81
|
+
*
|
|
82
|
+
* @returns Buffer("0001")
|
|
83
|
+
*/
|
|
84
|
+
function encodeDelimiterPkt() {
|
|
85
|
+
return Buffer.from(DELIMITER_PKT_HEX, "utf-8");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 生成 response-end-pkt(协议 v2)
|
|
89
|
+
*
|
|
90
|
+
* @returns Buffer("0002")
|
|
91
|
+
*/
|
|
92
|
+
function encodeResponseEndPkt() {
|
|
93
|
+
return Buffer.from(RESPONSE_END_PKT_HEX, "utf-8");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 解析 pkt-line 编码的完整数据流
|
|
97
|
+
*
|
|
98
|
+
* 将包含一个或多个 pkt-line 帧的 Buffer 解析为 PktLine 数组。
|
|
99
|
+
*
|
|
100
|
+
* @param data - 完整的 pkt-line 编码数据
|
|
101
|
+
* @returns 解析后的 PktLine 列表
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const lines = parsePktLines(Buffer.from("0009hello0000", "utf-8"));
|
|
106
|
+
* // lines[0] = { type: "data", payload: Buffer("hello") }
|
|
107
|
+
* // lines[1] = { type: "flush" }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
function parsePktLines(data) {
|
|
111
|
+
const result = [];
|
|
112
|
+
let offset = 0;
|
|
113
|
+
while (offset < data.length) {
|
|
114
|
+
if (offset + LENGTH_PREFIX_BYTES > data.length) throw new PktLineError(`Truncated pkt-line: need ${LENGTH_PREFIX_BYTES} bytes for length prefix, got ${data.length - offset}`);
|
|
115
|
+
const hex = data.subarray(offset, offset + LENGTH_PREFIX_BYTES).toString("utf-8");
|
|
116
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new PktLineError(`Invalid pkt-line length: "${hex}"`);
|
|
117
|
+
const length = parseInt(hex, 16);
|
|
118
|
+
offset += LENGTH_PREFIX_BYTES;
|
|
119
|
+
if (length === 0) result.push({ type: "flush" });
|
|
120
|
+
else if (length === 1) result.push({ type: "delimiter" });
|
|
121
|
+
else if (length === 2) result.push({ type: "response-end" });
|
|
122
|
+
else {
|
|
123
|
+
if (length < LENGTH_PREFIX_BYTES) throw new PktLineError(`Invalid pkt-line length: ${length} (minimum data length is ${LENGTH_PREFIX_BYTES})`);
|
|
124
|
+
if (length > MAX_PACKET_SIZE) throw new PktLineError(`Invalid pkt-line length: ${length} (max ${MAX_PACKET_SIZE})`);
|
|
125
|
+
const payloadLength = length - LENGTH_PREFIX_BYTES;
|
|
126
|
+
if (offset + payloadLength > data.length) throw new PktLineError(`Truncated pkt-line: expected ${payloadLength} bytes of payload, got ${data.length - offset}`);
|
|
127
|
+
const payload = data.subarray(offset, offset + payloadLength);
|
|
128
|
+
offset += payloadLength;
|
|
129
|
+
result.push({
|
|
130
|
+
type: "data",
|
|
131
|
+
payload
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 解析 pkt-line 编码的数据流,将有效帧与尾部原始二进制数据分离
|
|
139
|
+
*
|
|
140
|
+
* 与 {@link parsePktLines} 不同,此函数不会因尾部存在非 pkt-line 原始数据而抛出错误,
|
|
141
|
+
* 而是将剩余数据作为 trailing 返回。适用于解析非 side-band 编码的 upload-pack 响应:
|
|
142
|
+
* pkt-line 头部(ACK/NAK)+ 尾部原始 packfile 数据。
|
|
143
|
+
*
|
|
144
|
+
* @param data - 可能包含尾部原始数据的 pkt-line 编码 buffer
|
|
145
|
+
* @returns 解析出的帧列表和尾部原始数据
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const { lines, trailing } = splitPktLinesFromBuffer(
|
|
150
|
+
* Buffer.concat([encodePktLine("NAK\n"), rawPackfile]),
|
|
151
|
+
* );
|
|
152
|
+
* // lines[0] = { type: "data", payload: Buffer("NAK\n") }
|
|
153
|
+
* // trailing = <raw packfile>
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
function splitPktLinesFromBuffer(data) {
|
|
157
|
+
const lines = [];
|
|
158
|
+
let offset = 0;
|
|
159
|
+
while (offset < data.length) {
|
|
160
|
+
if (offset + LENGTH_PREFIX_BYTES > data.length) break;
|
|
161
|
+
const hex = data.subarray(offset, offset + LENGTH_PREFIX_BYTES).toString("utf-8");
|
|
162
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) break;
|
|
163
|
+
const length = parseInt(hex, 16);
|
|
164
|
+
if (length === 0) {
|
|
165
|
+
lines.push({ type: "flush" });
|
|
166
|
+
offset += LENGTH_PREFIX_BYTES;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (length === 1) {
|
|
170
|
+
lines.push({ type: "delimiter" });
|
|
171
|
+
offset += LENGTH_PREFIX_BYTES;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (length === 2) {
|
|
175
|
+
lines.push({ type: "response-end" });
|
|
176
|
+
offset += LENGTH_PREFIX_BYTES;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (length < LENGTH_PREFIX_BYTES || length > MAX_PACKET_SIZE) break;
|
|
180
|
+
const payloadLength = length - LENGTH_PREFIX_BYTES;
|
|
181
|
+
if (offset + LENGTH_PREFIX_BYTES + payloadLength > data.length) break;
|
|
182
|
+
const payload = data.subarray(offset + LENGTH_PREFIX_BYTES, offset + LENGTH_PREFIX_BYTES + payloadLength);
|
|
183
|
+
lines.push({
|
|
184
|
+
type: "data",
|
|
185
|
+
payload
|
|
186
|
+
});
|
|
187
|
+
offset += LENGTH_PREFIX_BYTES + payloadLength;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
lines,
|
|
191
|
+
trailing: Buffer.from(data.subarray(offset))
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
export { PktLineError, encodeDelimiterPkt, encodeFlushPkt, encodePktLine, encodeResponseEndPkt, parsePktLines, splitPktLinesFromBuffer };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { sha1 } from "../../core/types.mjs";
|
|
2
|
+
import { PktLineError, parsePktLines } from "./pkt-line.mjs";
|
|
3
|
+
//#region src/transport/protocol/ref-advertisement.ts
|
|
4
|
+
/**
|
|
5
|
+
* 引用广告解析
|
|
6
|
+
*
|
|
7
|
+
* 将服务端通过 pkt-line 编码的 ref advertisement 数据解析为
|
|
8
|
+
* 结构化的 RefAdvertisement 对象。
|
|
9
|
+
*
|
|
10
|
+
* 数据流格式:
|
|
11
|
+
* # service=git-upload-pack\n
|
|
12
|
+
* 0000 (flush)
|
|
13
|
+
* <hash> <refname>\0<capabilities>\n (第一行,capabilities 紧跟在 NUL 后)
|
|
14
|
+
* <hash> <refname>\n (后续引用行)
|
|
15
|
+
* <hash> <refname>^{}\n (peeled tag)
|
|
16
|
+
* 0000 (flush 结束)
|
|
17
|
+
*
|
|
18
|
+
* @see https://git-scm.com/docs/pack-protocol#_reference_discovery
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Ref 广告解析错误
|
|
22
|
+
*
|
|
23
|
+
* 当 ref advertisement 数据格式不符合 Git 协议规范时抛出。
|
|
24
|
+
*/
|
|
25
|
+
var RefAdvertisementError = class extends PktLineError {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(`ref-advertisement: ${message}`);
|
|
28
|
+
this.name = "RefAdvertisementError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/** 服务端头信息前缀 */
|
|
32
|
+
const SERVICE_HEADER_PREFIX = "# service=";
|
|
33
|
+
/** Peeled tag 后缀 */
|
|
34
|
+
const PEELED_TAG_SUFFIX = "^{}";
|
|
35
|
+
/**
|
|
36
|
+
* 解析 ref advertisement 数据
|
|
37
|
+
*
|
|
38
|
+
* @param data - 来自服务端的完整 ref advertisement 响应体(pkt-line 编码)
|
|
39
|
+
* @param service - 服务名称(如 "git-upload-pack")
|
|
40
|
+
* @returns 结构化的 RefAdvertisement
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const data = await fetch("https://example.com/repo/info/refs?service=git-upload-pack")
|
|
45
|
+
* .then(r => r.body);
|
|
46
|
+
* const adv = parseRefAdvertisement(data, "git-upload-pack");
|
|
47
|
+
* console.log(adv.refs[0].name); // "refs/heads/main"
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
function parseRefAdvertisement(data, service) {
|
|
51
|
+
const pktLines = parsePktLines(data);
|
|
52
|
+
if (pktLines.length === 0) throw new RefAdvertisementError("Empty ref advertisement");
|
|
53
|
+
let idx = 0;
|
|
54
|
+
if (idx < pktLines.length) {
|
|
55
|
+
const firstPkt = pktLines[idx];
|
|
56
|
+
if (firstPkt.type === "data") {
|
|
57
|
+
const firstPayload = firstPkt.payload.toString("utf-8");
|
|
58
|
+
if (firstPayload.startsWith(SERVICE_HEADER_PREFIX)) {
|
|
59
|
+
const headerService = firstPayload.slice(10).trim();
|
|
60
|
+
if (headerService !== service) throw new RefAdvertisementError(`Expected service "${service}" but got "${headerService}"`);
|
|
61
|
+
idx++;
|
|
62
|
+
if (idx >= pktLines.length || pktLines[idx].type !== "flush") throw new RefAdvertisementError(`Expected flush-pkt after service header "# service=${service}"`);
|
|
63
|
+
idx++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const refs = [];
|
|
68
|
+
let capabilities = {};
|
|
69
|
+
for (; idx < pktLines.length; idx++) {
|
|
70
|
+
const line = pktLines[idx];
|
|
71
|
+
if (line.type !== "data") break;
|
|
72
|
+
const payload = line.payload;
|
|
73
|
+
const nullIndex = payload.indexOf(0);
|
|
74
|
+
const refLine = (nullIndex === -1 ? payload.toString("utf-8") : payload.subarray(0, nullIndex).toString("utf-8")).trim();
|
|
75
|
+
if (refLine.length === 0) continue;
|
|
76
|
+
if (nullIndex !== -1 && payload.length > nullIndex + 1) capabilities = parseCapabilities(payload.subarray(nullIndex + 1).toString("utf-8"));
|
|
77
|
+
if (refLine.endsWith(PEELED_TAG_SUFFIX)) {
|
|
78
|
+
const peeledHash = parseHashFromRefLine(refLine);
|
|
79
|
+
const tagName = refLine.slice(0, -3).split(" ").slice(1).join(" ").trim();
|
|
80
|
+
if (tagName === "capabilities") continue;
|
|
81
|
+
if (refs.length === 0) throw new RefAdvertisementError(`Orphaned peeled tag line "${refLine}": no preceding ref`);
|
|
82
|
+
const lastRef = refs[refs.length - 1];
|
|
83
|
+
if (lastRef.name !== tagName) throw new RefAdvertisementError(`Peeled tag name "${tagName}" does not match previous ref "${lastRef.name}"`);
|
|
84
|
+
if (!lastRef.name.startsWith("refs/tags/")) throw new RefAdvertisementError(`Peeled tag line "${refLine}" follows non-tag ref "${lastRef.name}"`);
|
|
85
|
+
lastRef.peeled = peeledHash;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const ref = parseRefLine(refLine);
|
|
89
|
+
refs.push(ref);
|
|
90
|
+
}
|
|
91
|
+
applySymrefToRefs(capabilities, refs);
|
|
92
|
+
const defaultBranch = computeDefaultBranch(capabilities, refs);
|
|
93
|
+
return {
|
|
94
|
+
capabilities,
|
|
95
|
+
refs,
|
|
96
|
+
defaultBranch
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 从 ref 行中提取 hash
|
|
101
|
+
*
|
|
102
|
+
* @param line - 类似 "95d09f2b... refs/tags/v1.0^{}" 的行
|
|
103
|
+
* @returns SHA1 哈希
|
|
104
|
+
*/
|
|
105
|
+
function parseHashFromRefLine(line) {
|
|
106
|
+
const hashStr = line.split(" ")[0];
|
|
107
|
+
if (!hashStr) throw new RefAdvertisementError(`Cannot parse hash from ref line: "${line}"`);
|
|
108
|
+
return sha1(hashStr);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 解析单条 ref 行
|
|
112
|
+
*
|
|
113
|
+
* @param line - 类似 "95d09f2b... refs/heads/main" 的行
|
|
114
|
+
* @returns RemoteRef 对象
|
|
115
|
+
*/
|
|
116
|
+
function parseRefLine(line) {
|
|
117
|
+
const spaceIndex = line.indexOf(" ");
|
|
118
|
+
if (spaceIndex === -1) throw new RefAdvertisementError(`Invalid ref line: "${line}"`);
|
|
119
|
+
const hashStr = line.substring(0, spaceIndex);
|
|
120
|
+
const name = line.substring(spaceIndex + 1).trim();
|
|
121
|
+
if (hashStr.length !== 40) throw new RefAdvertisementError(`Invalid hash in ref line: "${line}"`);
|
|
122
|
+
return {
|
|
123
|
+
hash: sha1(hashStr),
|
|
124
|
+
name
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 解析 capabilities 字符串
|
|
129
|
+
*
|
|
130
|
+
* capabilities 以空格分隔,可能带参数(key=value)或不带参数(key)
|
|
131
|
+
*
|
|
132
|
+
* @param capsStr - 如 "multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/main agent=git/2.45.1"
|
|
133
|
+
* @returns 解析后的 capabilities 对象
|
|
134
|
+
*/
|
|
135
|
+
function parseCapabilities(capsStr) {
|
|
136
|
+
const caps = {};
|
|
137
|
+
const tokens = capsStr.split(" ");
|
|
138
|
+
for (const token of tokens) {
|
|
139
|
+
if (token.length === 0) continue;
|
|
140
|
+
const eqIndex = token.indexOf("=");
|
|
141
|
+
if (eqIndex === -1) caps[token] = true;
|
|
142
|
+
else {
|
|
143
|
+
const key = token.substring(0, eqIndex);
|
|
144
|
+
caps[key] = token.substring(eqIndex + 1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return caps;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 从 capabilities 中提取 symref=HEAD:<target>,填充到 refs 中 HEAD ref 的 symrefTarget
|
|
151
|
+
*
|
|
152
|
+
* Git 服务端通过 `symref=HEAD:refs/heads/main` 能力声明默认分支。
|
|
153
|
+
* 解析后将目标写入对应 HEAD ref 的 `symrefTarget` 字段,供 fetch 层据此设置本地 HEAD。
|
|
154
|
+
*
|
|
155
|
+
* @param capabilities - 解析后的 capabilities 对象
|
|
156
|
+
* @param refs - refs 数组(原地修改 HEAD ref 的 symrefTarget)
|
|
157
|
+
*/
|
|
158
|
+
function applySymrefToRefs(capabilities, refs) {
|
|
159
|
+
const symref = capabilities["symref"];
|
|
160
|
+
if (typeof symref !== "string") return;
|
|
161
|
+
const colonIndex = symref.indexOf(":");
|
|
162
|
+
if (colonIndex === -1) return;
|
|
163
|
+
const headName = symref.substring(0, colonIndex);
|
|
164
|
+
const target = symref.substring(colonIndex + 1);
|
|
165
|
+
for (const ref of refs) if (ref.name === headName) {
|
|
166
|
+
ref.symrefTarget = target;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 从广告数据推断默认分支(唯一解析规则)
|
|
172
|
+
*/
|
|
173
|
+
function computeDefaultBranch(capabilities, refs) {
|
|
174
|
+
const symref = capabilities["symref"];
|
|
175
|
+
if (typeof symref === "string") {
|
|
176
|
+
const colonIndex = symref.indexOf(":");
|
|
177
|
+
if (colonIndex !== -1) {
|
|
178
|
+
if (symref.substring(0, colonIndex) === "HEAD") return symref.substring(colonIndex + 1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const heads = refs.filter((r) => r.name.startsWith("refs/heads/"));
|
|
182
|
+
if (heads.length === 1) return heads[0].name;
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
export { RefAdvertisementError, parseRefAdvertisement };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { SHA1 } from "../../core/types.mjs";
|
|
2
|
+
import { RefStore } from "../../core/types/refs.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/transport/protocol/ref-collection.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 获取本地 refs 的哈希映射
|
|
7
|
+
*
|
|
8
|
+
* 扫描 refs/ 下所有命名空间的引用,确保任意自定义目标命名空间
|
|
9
|
+
*(如 refs/mirrors/、refs/remotes/ 等)都能被正确检测到。
|
|
10
|
+
*
|
|
11
|
+
* @param refs - 本地引用存储
|
|
12
|
+
* @returns ref 名称 → SHA1 哈希的映射
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const refs = getLocalRefs(refStore);
|
|
17
|
+
* console.log(refs.get("refs/heads/main")); // SHA1 hash
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function getLocalRefs(refs: RefStore): Map<string, SHA1>;
|
|
21
|
+
/**
|
|
22
|
+
* 将远程 ref 广告转换为哈希映射
|
|
23
|
+
*
|
|
24
|
+
* @param refs - 远程引用列表(如来自 advertisement)
|
|
25
|
+
* @returns ref 名称 → SHA1 哈希的映射
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const remoteMap = remoteRefsToMap(advertisement.refs);
|
|
30
|
+
* console.log(remoteMap.get("refs/heads/main"));
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function remoteRefsToMap(refs: Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
hash: SHA1;
|
|
36
|
+
}>): Map<string, SHA1>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { getLocalRefs, remoteRefsToMap };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { HEAD_REF } from "../../core/types/refs.mjs";
|
|
2
|
+
import { resolveRefHash } from "../../refs/resolve.mjs";
|
|
3
|
+
//#region src/transport/protocol/ref-collection.ts
|
|
4
|
+
/**
|
|
5
|
+
* 本地/远程引用收集
|
|
6
|
+
*
|
|
7
|
+
* 提供从 RefStore 收集本地 ref hash map 以及将远程 advertisement 转 map 的中立基础设施。
|
|
8
|
+
* 不带 fetch/push 语义。
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { getLocalRefs, remoteRefsToMap } from "./ref-collection.ts";
|
|
13
|
+
*
|
|
14
|
+
* const localRefs = getLocalRefs(refStore);
|
|
15
|
+
* const remoteMap = remoteRefsToMap(advertisement.refs);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* 获取本地 refs 的哈希映射
|
|
20
|
+
*
|
|
21
|
+
* 扫描 refs/ 下所有命名空间的引用,确保任意自定义目标命名空间
|
|
22
|
+
*(如 refs/mirrors/、refs/remotes/ 等)都能被正确检测到。
|
|
23
|
+
*
|
|
24
|
+
* @param refs - 本地引用存储
|
|
25
|
+
* @returns ref 名称 → SHA1 哈希的映射
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const refs = getLocalRefs(refStore);
|
|
30
|
+
* console.log(refs.get("refs/heads/main")); // SHA1 hash
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
function getLocalRefs(refs) {
|
|
34
|
+
const map = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const refName of refs.listAll()) try {
|
|
36
|
+
const hash = resolveRefHash(refs, refName);
|
|
37
|
+
if (hash) map.set(refName, hash);
|
|
38
|
+
} catch {}
|
|
39
|
+
try {
|
|
40
|
+
const hash = resolveRefHash(refs, HEAD_REF);
|
|
41
|
+
if (hash) map.set(HEAD_REF, hash);
|
|
42
|
+
} catch {}
|
|
43
|
+
return map;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 将远程 ref 广告转换为哈希映射
|
|
47
|
+
*
|
|
48
|
+
* @param refs - 远程引用列表(如来自 advertisement)
|
|
49
|
+
* @returns ref 名称 → SHA1 哈希的映射
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const remoteMap = remoteRefsToMap(advertisement.refs);
|
|
54
|
+
* console.log(remoteMap.get("refs/heads/main"));
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
function remoteRefsToMap(refs) {
|
|
58
|
+
const map = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const ref of refs) map.set(ref.name, ref.hash);
|
|
60
|
+
return map;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { getLocalRefs, remoteRefsToMap };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { RemoteRef } from "./types.mjs";
|
|
2
|
+
import { ParsedRefSpec } from "./refspec.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/transport/protocol/ref-match.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 判断远程引用是否匹配 refspec 源模式
|
|
7
|
+
*
|
|
8
|
+
* 通配符 refspec 使用 startsWith 匹配前缀,
|
|
9
|
+
* 精确 refspec 需要完全相等。
|
|
10
|
+
*
|
|
11
|
+
* @param ref - 远程引用
|
|
12
|
+
* @param spec - 解析后的 refspec
|
|
13
|
+
* @returns 是否匹配
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* matchesRefSpec(
|
|
18
|
+
* { name: "refs/heads/main", hash: sha1("...") },
|
|
19
|
+
* parseRefSpec("refs/heads/*:refs/remotes/origin/*"),
|
|
20
|
+
* ); // => true
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare function matchesRefSpec(ref: RemoteRef, spec: ParsedRefSpec): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 将远程引用名转换为本地引用名
|
|
26
|
+
*
|
|
27
|
+
* @param refName - 远程引用名称
|
|
28
|
+
* @param spec - 解析后的 refspec
|
|
29
|
+
* @returns 映射后的本地引用名称
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* mapRefName("refs/heads/main", parseRefSpec("refs/heads/*:refs/remotes/origin/*"))
|
|
34
|
+
* // => "refs/remotes/origin/main"
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare function mapRefName(refName: string, spec: ParsedRefSpec): string;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { mapRefName, matchesRefSpec };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/transport/protocol/ref-match.ts
|
|
2
|
+
/**
|
|
3
|
+
* 判断远程引用是否匹配 refspec 源模式
|
|
4
|
+
*
|
|
5
|
+
* 通配符 refspec 使用 startsWith 匹配前缀,
|
|
6
|
+
* 精确 refspec 需要完全相等。
|
|
7
|
+
*
|
|
8
|
+
* @param ref - 远程引用
|
|
9
|
+
* @param spec - 解析后的 refspec
|
|
10
|
+
* @returns 是否匹配
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* matchesRefSpec(
|
|
15
|
+
* { name: "refs/heads/main", hash: sha1("...") },
|
|
16
|
+
* parseRefSpec("refs/heads/*:refs/remotes/origin/*"),
|
|
17
|
+
* ); // => true
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function matchesRefSpec(ref, spec) {
|
|
21
|
+
if (spec.isWildcard) return ref.name.startsWith(spec.srcPattern);
|
|
22
|
+
return ref.name === spec.srcPattern;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 将远程引用名转换为本地引用名
|
|
26
|
+
*
|
|
27
|
+
* @param refName - 远程引用名称
|
|
28
|
+
* @param spec - 解析后的 refspec
|
|
29
|
+
* @returns 映射后的本地引用名称
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* mapRefName("refs/heads/main", parseRefSpec("refs/heads/*:refs/remotes/origin/*"))
|
|
34
|
+
* // => "refs/remotes/origin/main"
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function mapRefName(refName, spec) {
|
|
38
|
+
const suffix = refName.slice(spec.srcPattern.length);
|
|
39
|
+
return `${spec.dstPattern}${suffix}`;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { mapRefName, matchesRefSpec };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { GitError } from "../../core/errors.mjs";
|
|
2
|
+
import { RefMappingRule } from "./types.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/transport/protocol/refspec.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* RefSpec 解析错误
|
|
7
|
+
*/
|
|
8
|
+
declare class RefSpecError extends GitError {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 解析后的 refspec
|
|
13
|
+
*/
|
|
14
|
+
interface ParsedRefSpec {
|
|
15
|
+
force: boolean;
|
|
16
|
+
srcPattern: string;
|
|
17
|
+
dstPattern: string;
|
|
18
|
+
/** 原始 refspec 是否包含通配符 * */
|
|
19
|
+
isWildcard: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 解析 refspec 字符串
|
|
23
|
+
*
|
|
24
|
+
* 格式: [+]<src>:<dst>
|
|
25
|
+
* 其中 + 表示 force push/fetch
|
|
26
|
+
* * 是通配符
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* parseRefSpec("+refs/heads/*:refs/remotes/origin/*")
|
|
31
|
+
* // => { force: true, srcPattern: "refs/heads/", dstPattern: "refs/remotes/origin/", isWildcard: true }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare function parseRefSpec(refSpec: string): ParsedRefSpec;
|
|
35
|
+
/**
|
|
36
|
+
* 将 RefMappingRule 转换为 ParsedRefSpec
|
|
37
|
+
*/
|
|
38
|
+
declare function mappingRuleToParsedSpec(rule: RefMappingRule): ParsedRefSpec;
|
|
39
|
+
/**
|
|
40
|
+
* 将 ParsedRefSpec 转换为 RefMappingRule
|
|
41
|
+
*/
|
|
42
|
+
declare function parsedSpecToMappingRule(spec: ParsedRefSpec): RefMappingRule;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { ParsedRefSpec, RefSpecError, mappingRuleToParsedSpec, parseRefSpec, parsedSpecToMappingRule };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GitError } from "../../core/errors.mjs";
|
|
2
|
+
//#region src/transport/protocol/refspec.ts
|
|
3
|
+
/**
|
|
4
|
+
* RefSpec 解析与转换
|
|
5
|
+
*
|
|
6
|
+
* 只处理 refspec/mapping rule 的语法与转换,不带 fetch/push 语义。
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { parseRefSpec, mappingRuleToParsedSpec } from "./refspec.ts";
|
|
11
|
+
*
|
|
12
|
+
* const spec = parseRefSpec("+refs/heads/*:refs/remotes/origin/*");
|
|
13
|
+
* console.log(spec.force, spec.srcPattern, spec.dstPattern);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* RefSpec 解析错误
|
|
18
|
+
*/
|
|
19
|
+
var RefSpecError = class extends GitError {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(`Refspec error: ${message}`);
|
|
22
|
+
this.name = "RefSpecError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* 解析 refspec 字符串
|
|
27
|
+
*
|
|
28
|
+
* 格式: [+]<src>:<dst>
|
|
29
|
+
* 其中 + 表示 force push/fetch
|
|
30
|
+
* * 是通配符
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* parseRefSpec("+refs/heads/*:refs/remotes/origin/*")
|
|
35
|
+
* // => { force: true, srcPattern: "refs/heads/", dstPattern: "refs/remotes/origin/", isWildcard: true }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
function parseRefSpec(refSpec) {
|
|
39
|
+
const force = refSpec.startsWith("+");
|
|
40
|
+
const spec = force ? refSpec.slice(1) : refSpec;
|
|
41
|
+
const colonIndex = spec.indexOf(":");
|
|
42
|
+
if (colonIndex === -1) throw new RefSpecError(`Invalid refspec: "${refSpec}" (missing ":")`);
|
|
43
|
+
const src = spec.substring(0, colonIndex);
|
|
44
|
+
const dst = spec.substring(colonIndex + 1);
|
|
45
|
+
for (const [sideName, side] of [["src", src], ["dst", dst]]) {
|
|
46
|
+
const starCount = (side.match(/\*/g) ?? []).length;
|
|
47
|
+
if (starCount > 1) throw new RefSpecError(`Invalid refspec: "${refSpec}" (multiple wildcards in ${sideName})`);
|
|
48
|
+
if (starCount === 1 && !side.endsWith("*")) throw new RefSpecError(`Invalid refspec: "${refSpec}" (wildcard must be at the end of ${sideName})`);
|
|
49
|
+
}
|
|
50
|
+
const srcHasStar = src.endsWith("*");
|
|
51
|
+
if (srcHasStar !== dst.endsWith("*")) throw new RefSpecError(`Invalid refspec: "${refSpec}" (wildcard must appear on both sides)`);
|
|
52
|
+
const isWildcard = srcHasStar;
|
|
53
|
+
return {
|
|
54
|
+
force,
|
|
55
|
+
srcPattern: src.replace(/\*$/, ""),
|
|
56
|
+
dstPattern: dst.replace(/\*$/, ""),
|
|
57
|
+
isWildcard
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 将 RefMappingRule 转换为 ParsedRefSpec
|
|
62
|
+
*/
|
|
63
|
+
function mappingRuleToParsedSpec(rule) {
|
|
64
|
+
const forceFromSource = rule.source.startsWith("+");
|
|
65
|
+
const cleanSource = forceFromSource ? rule.source.slice(1) : rule.source;
|
|
66
|
+
return parseRefSpec(`${rule.force ?? forceFromSource ? "+" : ""}${cleanSource}:${rule.target}`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 将 ParsedRefSpec 转换为 RefMappingRule
|
|
70
|
+
*/
|
|
71
|
+
function parsedSpecToMappingRule(spec) {
|
|
72
|
+
return {
|
|
73
|
+
source: spec.isWildcard ? `${spec.srcPattern}*` : spec.srcPattern,
|
|
74
|
+
target: spec.isWildcard ? `${spec.dstPattern}*` : spec.dstPattern,
|
|
75
|
+
force: spec.force
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { RefSpecError, mappingRuleToParsedSpec, parseRefSpec, parsedSpecToMappingRule };
|