///| Upload-pack (server-side) implementation
///|
#warnings("-deprecated")
fn upload_pack_parse_int(value : StringView) -> Int raise {
@strconv.parse_int(value)
}
///|
#warnings("-deprecated")
fn upload_pack_parse_int64(value : StringView) -> Int64 raise {
@strconv.parse_int64(value)
}
///|
pub struct UploadPackRequest {
wants : Array[@bit.ObjectId]
haves : Array[@bit.ObjectId]
capabilities : Array[String]
done : Bool
shallow : Array[@bit.ObjectId]
deepen : Int?
}
///|
/// Protocol v2 upload-pack request
pub struct UploadPackRequestV2 {
command : String // "fetch", "ls-refs", etc.
capabilities : Array[String]
wants : Array[@bit.ObjectId]
want_refs : Array[String]
haves : Array[@bit.ObjectId]
done : Bool
// v2-specific capabilities
filter : String? // e.g., "blob:none"
packfile_uris : String? // e.g., "https"
}
///|
/// Detect protocol version from request bytes
/// Returns 1 for v1 (want lines), 2 for v2 (command=...)
pub fn detect_protocol_version(data : Bytes) -> Int {
if data.length() < 8 {
return 1
}
// Parse first pktline length
let len = upload_parse_pkt_len_safe(data, 0)
if len < 4 {
return 1
}
// Check if first line starts with "command="
let end = if len > data.length() { data.length() } else { len }
let line = upload_bytes_to_string_range(data, 4, end)
if line.has_prefix("command=") {
2
} else {
1
}
}
///|
fn upload_parse_pkt_len_safe(data : Bytes, start : Int) -> Int {
if start + 4 > data.length() {
return 0
}
let mut v = 0
for i in 0..<4 {
let c = data[start + i].to_int()
let digit = if c >= 0x30 && c <= 0x39 {
// '0'-'9'
c - 0x30
} else if c >= 0x61 && c <= 0x66 {
// 'a'-'f'
10 + (c - 0x61)
} else if c >= 0x41 && c <= 0x46 {
// 'A'-'F'
10 + (c - 0x41)
} else {
return 0
}
v = v * 16 + digit
}
v
}
///|
/// Parse protocol v2 upload-pack request
pub fn parse_upload_pack_request_v2(
data : Bytes,
) -> UploadPackRequestV2 raise @bit.GitError {
let mut command = ""
let capabilities : Array[String] = []
let wants : Array[@bit.ObjectId] = []
let want_refs : Array[String] = []
let haves : Array[@bit.ObjectId] = []
let mut done = false
let mut filter : String? = None
let mut packfile_uris : String? = None
let mut i = 0
let mut in_args = false // After delimiter (0001)
let mut saw_flush_after_args = false
while i + 4 <= data.length() {
let len = upload_parse_pkt_len_safe(data, i)
// 0000 = flush
if len == 0 {
i += 4
if in_args {
saw_flush_after_args = true
}
continue
}
// 0001 = delimiter (switch from command section to arguments)
if len == 1 {
if in_args {
let cmd = if command.length() > 0 { command } else { "unknown" }
raise @bit.GitError::ProtocolError(
"expected flush after " + cmd + " arguments",
)
}
in_args = true
i += 4
continue
}
if len < 4 || i + len > data.length() {
raise @bit.GitError::ProtocolError("Invalid pkt-line length in v2")
}
if saw_flush_after_args {
raise @bit.GitError::ProtocolError("Unexpected data after flush")
}
let line = upload_bytes_to_string_range(data, i + 4, i + len)
let line = upload_trim_line(line)
i += len
if !in_args {
// Command section
if line.has_prefix("command=") {
command = String::unsafe_substring(line, start=8, end=line.length())
} else if line.has_prefix("object-format=") {
// Ignore for now, assume sha1
} else if line.length() > 0 {
capabilities.push(line)
}
// Arguments section
} else if line.has_prefix("want-ref ") {
let refname = String::unsafe_substring(line, start=9, end=line.length())
let refname = upload_trim_line(refname)
if refname.length() > 0 {
want_refs.push(refname)
}
} else if line.has_prefix("want ") {
let hex = String::unsafe_substring(line, start=5, end=line.length())
let hex = upload_trim_line(hex)
if hex.length() >= 40 {
let hex40 = String::unsafe_substring(hex, start=0, end=40)
wants.push(@bit.ObjectId::from_hex(hex40))
}
} else if line.has_prefix("have ") {
let hex = String::unsafe_substring(line, start=5, end=line.length())
let hex = upload_trim_line(hex)
if hex.length() >= 40 {
let hex40 = String::unsafe_substring(hex, start=0, end=40)
haves.push(@bit.ObjectId::from_hex(hex40))
}
} else if line == "done" {
done = true
} else if line.has_prefix("filter ") {
filter = Some(String::unsafe_substring(line, start=7, end=line.length()))
} else if line.has_prefix("packfile-uris ") {
packfile_uris = Some(
String::unsafe_substring(line, start=14, end=line.length()),
)
}
}
{
command,
capabilities,
wants,
want_refs,
haves,
done,
filter,
packfile_uris,
}
}
///|
/// Build upload-pack advertisement (info/refs payload).
pub fn build_upload_pack_advertisement(
fs : &@bit.RepoFileSystem,
root : String,
agent? : String = "git/moonbit",
protocol_version? : Int = 0,
) -> Bytes raise @bit.GitError {
let git_dir = join_path(root, ".git")
let out : Array[Byte] = []
// Protocol v2: output capabilities instead of refs
if protocol_version == 2 {
upload_push_bytes(out, @protocol.pktline_encode("version 2\n"))
upload_push_bytes(out, @protocol.pktline_encode("agent=\{agent}\n"))
upload_push_bytes(out, @protocol.pktline_encode("ls-refs=unborn\n"))
upload_push_bytes(
out,
@protocol.pktline_encode("fetch=shallow wait-for-done\n"),
)
upload_push_bytes(out, @protocol.pktline_encode("server-option\n"))
upload_push_bytes(out, @protocol.pktline_encode("object-format=sha1\n"))
upload_push_bytes(out, @protocol.pktline_flush())
return Bytes::from_array(FixedArray::makei(out.length(), i => out[i]))
}
let refs = show_ref(fs, git_dir)
refs.sort_by((a, b) => String::compare(a.0, b.0))
// Resolve HEAD and prepend it to refs list
let head_id = resolve_ref(fs, git_dir, "HEAD")
let all_refs : Array[(String, @bit.ObjectId)] = []
match head_id {
Some(id) => all_refs.push(("HEAD", id))
None => ()
}
for item in refs {
all_refs.push(item)
}
// Build symref capability when HEAD is a symbolic ref
let head_symref : String? = try {
let head_ref = read_head_ref(fs, git_dir)
match head_ref {
HeadRef::Branch(name) =>
if name.has_prefix("refs/") {
Some(name)
} else {
Some("refs/heads/" + name)
}
HeadRef::Detached(_) => None
}
} catch {
_ => None
}
let caps = match head_symref {
Some(target) => upload_default_caps(agent) + " symref=HEAD:" + target
None => upload_default_caps(agent)
}
// Protocol v1: output version line before refs
if protocol_version == 1 {
upload_push_bytes(out, @protocol.pktline_encode("version 1\n"))
}
if all_refs.length() == 0 {
let zero = @bit.ObjectId::zero().to_hex()
let line = "\{zero} capabilities^{}\u0000\{caps}\n"
upload_push_bytes(out, @protocol.pktline_encode(line))
upload_push_bytes(out, @protocol.pktline_flush())
return Bytes::from_array(FixedArray::makei(out.length(), i => out[i]))
}
let (first_name, first_id) = all_refs[0]
let first_line = "\{first_id.to_hex()} \{first_name}\u0000\{caps}\n"
upload_push_bytes(out, @protocol.pktline_encode(first_line))
for i in 1.. out[i]))
}
///|
pub fn parse_upload_pack_request(
data : Bytes,
) -> UploadPackRequest raise @bit.GitError {
let wants : Array[@bit.ObjectId] = []
let haves : Array[@bit.ObjectId] = []
let shallow : Array[@bit.ObjectId] = []
let capabilities : Array[String] = []
let mut done = false
let mut deepen : Int? = None
let mut i = 0
let mut first_want = true
while i + 4 <= data.length() {
let len = upload_parse_pkt_len(data, i)
if len == 0 {
// flush packet
i += 4
continue
}
if len < 4 || i + len > data.length() {
raise @bit.GitError::ProtocolError("Invalid pkt-line length")
}
let line = upload_bytes_to_string_range(data, i + 4, i + len)
let mut line = upload_trim_line(line)
// Parse capabilities from first want line
if first_want && line.has_prefix("want ") {
first_want = false
// Try NUL separator first (git protocol)
match line.find("\u0000") {
Some(idx) => {
let caps_str = String::unsafe_substring(
line,
start=idx + 1,
end=line.length(),
)
line = String::unsafe_substring(line, start=0, end=idx)
let caps = upload_split_by_space(caps_str)
for c in caps {
if c.length() > 0 {
capabilities.push(c)
}
}
}
None => {
// HTTP stateless-rpc: caps are space-separated after SHA
// Format: "want ..."
let rest = String::unsafe_substring(line, start=5, end=line.length())
let rest = upload_trim_line(rest)
if rest.length() > 40 {
let caps_str = String::unsafe_substring(
rest,
start=41,
end=rest.length(),
)
let caps = upload_split_by_space(caps_str)
for c in caps {
if c.length() > 0 {
capabilities.push(c)
}
}
}
}
}
}
if line.has_prefix("want ") {
let hex = String::unsafe_substring(line, start=5, end=line.length())
let hex = upload_trim_line(hex)
// Extract just the SHA (first 40 chars or until space)
let hex = if hex.length() > 40 {
String::unsafe_substring(hex, start=0, end=40)
} else {
match hex.find(" ") {
None => hex
Some(idx) => String::unsafe_substring(hex, start=0, end=idx)
}
}
if hex.length() >= 40 {
let hex40 = String::unsafe_substring(hex, start=0, end=40)
wants.push(@bit.ObjectId::from_hex(hex40))
}
} else if line.has_prefix("have ") {
let hex = String::unsafe_substring(line, start=5, end=line.length())
let hex = upload_trim_line(hex)
if hex.length() >= 40 {
let hex40 = String::unsafe_substring(hex, start=0, end=40)
haves.push(@bit.ObjectId::from_hex(hex40))
}
} else if line.has_prefix("shallow ") {
let hex = String::unsafe_substring(line, start=8, end=line.length())
let hex = upload_trim_line(hex)
if hex.length() >= 40 {
let hex40 = String::unsafe_substring(hex, start=0, end=40)
shallow.push(@bit.ObjectId::from_hex(hex40))
}
} else if line.has_prefix("deepen ") {
let depth_str = String::unsafe_substring(line, start=7, end=line.length())
let depth_str = upload_trim_line(depth_str)
let depth = upload_pack_parse_int(depth_str) catch { _ => -1 }
if depth >= 0 {
deepen = Some(depth)
}
} else if line == "done" {
done = true
}
i += len
}
{ wants, haves, capabilities, done, shallow, deepen }
}
///|
pub fn upload_pack(
fs : &@bit.RepoFileSystem,
root : String,
req : UploadPackRequest,
) -> Bytes raise @bit.GitError {
let git_dir = join_path(root, ".git")
let db = ObjectDb::load(fs, git_dir)
// Validate wants (must be commit objects)
for want in req.wants {
let obj = db.get(fs, want)
match obj {
None => raise @bit.GitError::ProtocolError("not our ref " + want.to_hex())
Some(o) =>
if o.obj_type != @bit.ObjectType::Commit {
raise @bit.GitError::ProtocolError("not our ref " + want.to_hex())
}
}
}
// Stateless clients may terminate after wants; respond with flush only
if !req.done && req.haves.length() == 0 {
return @protocol.pktline_flush()
}
// Check if client wants side-band
let use_side_band = req.capabilities.contains("side-band-64k") ||
req.capabilities.contains("side-band")
// Collect have ACKs (v0 allows duplicates)
let ack_ids = collect_have_acks_v0(db, fs, req.haves)
if !req.done {
return build_upload_pack_ack_response(ack_ids)
}
// Collect objects reachable from wants
let include_pack_objects_died = req.shallow.length() == 0 &&
req.deepen is None
let objects = collect_upload_pack_objects(db, fs, req.wants, req.haves) catch {
err =>
raise @bit.GitError::ProtocolError(
upload_pack_error_message(err, include_pack_objects_died),
)
}
build_upload_pack_response(objects, use_side_band, ack_ids)
}
///|
fn build_upload_pack_ack_response(ack_ids : Array[@bit.ObjectId]) -> Bytes {
let out : Array[Byte] = []
if ack_ids.length() > 0 {
for id in ack_ids {
upload_push_bytes(
out,
@protocol.pktline_encode("ACK " + id.to_hex() + "\n"),
)
}
} else {
upload_push_bytes(out, @protocol.pktline_encode("NAK\n"))
}
upload_push_bytes(out, @protocol.pktline_flush())
Bytes::from_array(FixedArray::makei(out.length(), i => out[i]))
}
///|
fn build_upload_pack_response(
objects : Array[@bit.PackObject],
use_side_band : Bool,
ack_ids : Array[@bit.ObjectId],
) -> Bytes {
let out : Array[Byte] = []
if ack_ids.length() > 0 {
for id in ack_ids {
upload_push_bytes(
out,
@protocol.pktline_encode("ACK " + id.to_hex() + "\n"),
)
}
}
// NAK (no common commits)
upload_push_bytes(out, @protocol.pktline_encode("NAK\n"))
let pack = build_upload_packfile(objects)
if use_side_band {
// Send packfile via side-band-64k (band 1)
let chunk_size = 65515 // 65520 - 4 (pkt-line header) - 1 (band byte)
let mut offset = 0
while offset < pack.length() {
let end = if offset + chunk_size > pack.length() {
pack.length()
} else {
offset + chunk_size
}
let data_len = end - offset
let pkt_len = data_len + 5 // 4 (header) + 1 (band byte)
let len_hex = @bithash.int_to_hex4(pkt_len)
for c in len_hex {
out.push(c.to_int().to_byte())
}
out.push(b'\x01') // band 1 = packfile data
for i in offset.. out[i]))
}
///|
/// Build transfer packs with the same bounded OFS-delta search used by
/// pack-objects. It reduces bytes-on-wire and the receiver's index-pack work
/// without allowing unbounded delta chains.
fn build_upload_packfile(objects : Array[@bit.PackObject]) -> Bytes {
let (pack, _) = @pack.create_packfile_with_delta_stats_compression_reuse(
objects,
@bit.PackDeltaMode::OfsDelta,
@bit.PackCompression::Default,
false,
50,
10,
)
pack
}
///|
fn collect_have_acks_v0(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
haves : Array[@bit.ObjectId],
) -> Array[@bit.ObjectId] {
let out : Array[@bit.ObjectId] = []
for id in haves {
if (db.get(fs, id) catch { _ => None }) is Some(_) {
out.push(id)
}
}
out
}
///|
fn collect_have_acks_v2(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
haves : Array[@bit.ObjectId],
) -> Array[@bit.ObjectId] {
let out : Array[@bit.ObjectId] = []
let seen : Map[String, Bool] = Map([])
for id in haves {
let hex = id.to_hex()
if seen.contains(hex) {
continue
}
if (db.get(fs, id) catch { _ => None }) is Some(_) {
out.push(id)
seen[hex] = true
}
}
out
}
///|
/// Walk wants once, using the client's have commits as traversal boundaries.
/// This avoids materializing and hashing the complete have-side object set.
fn collect_upload_pack_objects(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
wants : Array[@bit.ObjectId],
haves : Array[@bit.ObjectId],
) -> Array[@bit.PackObject] raise @bit.GitError {
if haves.length() == 0 {
collect_reachable_objects_from_commits(db, fs, wants)
} else {
collect_reachable_objects_excluding_commits(db, fs, wants, haves)
}
}
///|
fn upload_pack_error_message(
err : @bit.GitError,
include_pack_objects_died : Bool,
) -> String {
let base = match err {
InvalidObject(msg) => upload_pack_map_invalid_object(msg)
HashMismatch(_, _) => "hash mismatch"
PackfileError(msg) => msg
ProtocolError(msg) => msg
IoError(msg) => msg
}
if include_pack_objects_died && !base.contains("pack-objects died") {
base + "\npack-objects died"
} else {
base
}
}
///|
fn upload_pack_map_invalid_object(msg : String) -> String {
let prefix = "Missing blob object: "
if msg.has_prefix(prefix) {
let hex = String::unsafe_substring(
msg,
start=prefix.length(),
end=msg.length(),
)
"unable to read " + hex
} else if msg.has_prefix("Missing tree object") ||
msg.has_prefix("Object is not a tree") {
"bad tree object"
} else if msg.has_prefix("Missing commit object") ||
msg.has_prefix("Object is not a commit") {
"not our ref"
} else {
msg
}
}
///|
fn upload_default_caps(agent : String) -> String {
let caps : Array[String] = [
"multi_ack",
"multi_ack_detailed",
"thin-pack",
"side-band",
"side-band-64k",
"ofs-delta",
"shallow",
"no-progress",
"include-tag",
"no-done",
"object-format=sha1",
"agent=\{agent}",
]
caps.join(" ")
}
///|
fn upload_parse_pkt_len(data : Bytes, start : Int) -> Int raise @bit.GitError {
if start + 4 > data.length() {
raise @bit.GitError::ProtocolError("Invalid pkt-line length")
}
let mut v = 0
for i in 0..<4 {
let c = data[start + i].to_int().unsafe_to_char()
let digit = @bithash.hex_char_to_int(c) catch {
_ => raise @bit.GitError::ProtocolError("Invalid pkt-line length")
}
v = v * 16 + digit
}
v
}
///|
fn upload_bytes_to_string_range(data : Bytes, start : Int, end : Int) -> String {
let buf = StringBuilder::new()
let mut i = start
while i < end {
buf.write_char(data[i].to_int().unsafe_to_char())
i += 1
}
buf.to_string()
}
///|
fn upload_trim_line(line : String) -> String {
if line.has_suffix("\n") {
String::unsafe_substring(line, start=0, end=line.length() - 1)
} else {
line
}
}
///|
fn upload_split_by_space(s : String) -> Array[String] {
let out : Array[String] = []
let mut buf : Array[Char] = []
for c in s {
if c == ' ' {
out.push(upload_chars_to_string(buf))
buf = []
} else {
buf.push(c)
}
}
out.push(upload_chars_to_string(buf))
out
}
///|
fn upload_chars_to_string(chars : Array[Char]) -> String {
let buf = StringBuilder::new()
for c in chars {
buf.write_char(c)
}
buf.to_string()
}
///|
fn upload_push_bytes(out : Array[Byte], data : Bytes) -> Unit {
for b in data {
out.push(b)
}
}
///|
/// Upload-pack configuration for v2 protocol
pub(all) struct UploadPackConfig {
allow_filter : Bool
blob_packfile_uri : String? // If set, packfile-uris is allowed
allow_ref_in_want : Bool
}
///|
pub fn UploadPackConfig::new(
allow_filter : Bool,
blob_packfile_uri : String?,
allow_ref_in_want : Bool,
) -> UploadPackConfig {
{ allow_filter, blob_packfile_uri, allow_ref_in_want }
}
///|
pub fn UploadPackConfig::default() -> UploadPackConfig {
{ allow_filter: true, blob_packfile_uri: None, allow_ref_in_want: false }
}
///|
/// Handle protocol v2 upload-pack request
pub fn upload_pack_v2(
fs : &@bit.RepoFileSystem,
root : String,
req : UploadPackRequestV2,
config : UploadPackConfig,
) -> Bytes raise @bit.GitError {
// Validate request against config
if req.filter is Some(_) && !config.allow_filter {
raise @bit.GitError::ProtocolError(
"unexpected line: 'filter " + req.filter.unwrap() + "'",
)
}
if req.packfile_uris is Some(_) && config.blob_packfile_uri is None {
raise @bit.GitError::ProtocolError(
"unexpected line: 'packfile-uris " + req.packfile_uris.unwrap() + "'",
)
}
// Process the fetch command
if req.command != "fetch" {
raise @bit.GitError::ProtocolError(
"unsupported command: '" + req.command + "'",
)
}
let git_dir = join_path(root, ".git")
let db = ObjectDb::load(fs, git_dir)
let refs = show_ref(fs, git_dir)
let ref_map : Map[String, @bit.ObjectId] = Map([])
for item in refs {
let (name, id) = item
ref_map[name] = id
}
let all_wants : Array[@bit.ObjectId] = []
for id in req.wants {
all_wants.push(id)
}
let wanted_refs : Array[(String, @bit.ObjectId)] = []
if req.want_refs.length() > 0 {
if !config.allow_ref_in_want {
raise @bit.GitError::ProtocolError("unknown command 'want-ref'")
}
for name in req.want_refs {
match ref_map.get(name) {
Some(id) => {
wanted_refs.push((name, id))
all_wants.push(id)
}
None => raise @bit.GitError::ProtocolError("unknown ref " + name)
}
}
}
let seen_wants : Map[String, Bool] = Map([])
let wants : Array[@bit.ObjectId] = []
for id in all_wants {
let hex = id.to_hex()
if !seen_wants.contains(hex) {
seen_wants[hex] = true
wants.push(id)
}
}
let ack_ids = collect_have_acks_v2(db, fs, req.haves)
if !req.done {
return build_upload_pack_response_v2_ack(ack_ids, wanted_refs)
}
// Collect objects reachable from wants
let objects = collect_upload_pack_objects(db, fs, wants, req.haves) catch {
err =>
raise @bit.GitError::ProtocolError(upload_pack_error_message(err, true))
}
// Apply partial clone filter (blob:none, blob:limit, tree:depth)
let filter_spec = match req.filter {
Some(f) => parse_filter_spec(f)
None => @protocol.FilterSpec::NoFilter
}
let filtered_objects = apply_filter_spec(objects, filter_spec)
// Build v2 response
build_upload_pack_response_v2(filtered_objects, ack_ids, wanted_refs)
}
///|
/// Parse filter specification string (e.g., "blob:none", "blob:limit=1000")
fn parse_filter_spec(spec : String) -> @protocol.FilterSpec {
if spec == "blob:none" {
@protocol.FilterSpec::BlobNone
} else if spec.has_prefix("blob:limit=") {
let limit_str = String::unsafe_substring(spec, start=11, end=spec.length())
let limit = upload_pack_parse_int64(limit_str) catch { _ => 0L }
@protocol.FilterSpec::BlobLimit(limit)
} else if spec.has_prefix("tree:") {
let depth_str = String::unsafe_substring(spec, start=5, end=spec.length())
let depth = upload_pack_parse_int(depth_str) catch { _ => 0 }
@protocol.FilterSpec::TreeDepth(depth)
} else {
@protocol.FilterSpec::NoFilter
}
}
///|
/// Apply filter specification to objects
fn apply_filter_spec(
objects : Array[@bit.PackObject],
filter : @protocol.FilterSpec,
) -> Array[@bit.PackObject] {
match filter {
NoFilter => objects
BlobNone => {
// Exclude all blobs
let result : Array[@bit.PackObject] = []
for obj in objects {
if obj.obj_type != @bit.ObjectType::Blob {
result.push(obj)
}
}
result
}
BlobLimit(max_size) => {
// Exclude blobs larger than max_size
let result : Array[@bit.PackObject] = []
for obj in objects {
if obj.obj_type != @bit.ObjectType::Blob ||
obj.data.length().to_int64() <= max_size {
result.push(obj)
}
}
result
}
TreeDepth(depth) =>
if depth == 0 {
let result : Array[@bit.PackObject] = []
for obj in objects {
if obj.obj_type != @bit.ObjectType::Tree &&
obj.obj_type != @bit.ObjectType::Blob {
result.push(obj)
}
}
result
} else {
objects
}
SparseOid(_) | SparsePathUnsupported => objects
}
}
///|
fn build_upload_pack_response_v2_ack(
ack_ids : Array[@bit.ObjectId],
wanted_refs : Array[(String, @bit.ObjectId)],
) -> Bytes {
let out : Array[Byte] = []
if ack_ids.length() > 0 {
upload_push_bytes(out, @protocol.pktline_encode("acknowledgments\n"))
for id in ack_ids {
upload_push_bytes(
out,
@protocol.pktline_encode("ACK " + id.to_hex() + "\n"),
)
}
upload_push_bytes(out, @protocol.pktline_delim())
}
if wanted_refs.length() > 0 {
upload_push_bytes(out, @protocol.pktline_encode("wanted-refs\n"))
for item in wanted_refs {
let (name, id) = item
upload_push_bytes(
out,
@protocol.pktline_encode("\{id.to_hex()} \{name}\n"),
)
}
upload_push_bytes(out, @protocol.pktline_delim())
}
upload_push_bytes(out, @protocol.pktline_flush())
Bytes::from_array(FixedArray::makei(out.length(), i => out[i]))
}
///|
fn build_upload_pack_response_v2(
objects : Array[@bit.PackObject],
ack_ids : Array[@bit.ObjectId],
wanted_refs : Array[(String, @bit.ObjectId)],
) -> Bytes {
let out : Array[Byte] = []
if ack_ids.length() > 0 {
upload_push_bytes(out, @protocol.pktline_encode("acknowledgments\n"))
for id in ack_ids {
upload_push_bytes(
out,
@protocol.pktline_encode("ACK " + id.to_hex() + "\n"),
)
}
upload_push_bytes(out, @protocol.pktline_delim())
}
if wanted_refs.length() > 0 {
upload_push_bytes(out, @protocol.pktline_encode("wanted-refs\n"))
for item in wanted_refs {
let (name, id) = item
upload_push_bytes(
out,
@protocol.pktline_encode("\{id.to_hex()} \{name}\n"),
)
}
upload_push_bytes(out, @protocol.pktline_delim())
}
// Send packfile-section header
upload_push_bytes(out, @protocol.pktline_encode("packfile\n"))
let pack = build_upload_packfile(objects)
// Send packfile via side-band-64k (band 1)
let chunk_size = 65515 // 65520 - 4 (pkt-line header) - 1 (band byte)
let mut offset = 0
while offset < pack.length() {
let end = if offset + chunk_size > pack.length() {
pack.length()
} else {
offset + chunk_size
}
let data_len = end - offset
let pkt_len = data_len + 5 // 4 (header) + 1 (band byte)
let len_hex = @bithash.int_to_hex4(pkt_len)
for c in len_hex {
out.push(c.to_int().to_byte())
}
out.push(b'\x01') // band 1 = packfile data
for i in offset.. out[i]))
}