///| Upload-pack protocol helpers (v0/v2)
///|
pub(all) enum ProtocolVersion {
V0
V2
} derive(Eq)
///|
pub fn detect_protocol_version(
data : Bytes,
) -> ProtocolVersion raise @bit.GitError {
let lines = pktline_decode(data)
for item in lines {
let (line_bytes, is_flush) = item
if is_flush || line_bytes.length() == 0 {
continue
}
let line = bytes_to_string(line_bytes)
if line.has_prefix("#") {
continue
}
if line.has_prefix("version 2") {
return ProtocolVersion::V2
}
}
ProtocolVersion::V0
}
///|
pub fn parse_v2_advertised_caps(
data : Bytes,
) -> Array[String] raise @bit.GitError {
let result : Array[String] = []
let lines = pktline_decode(data)
for item in lines {
let (line_bytes, is_flush) = item
if is_flush || line_bytes.length() == 0 {
continue
}
let line = line_bytes |> bytes_to_string |> trim_newline
if line.has_prefix("#") || line.has_prefix("version ") {
continue
}
result.push(line)
}
result
}
///|
pub fn parse_v0_advertised_caps(
data : Bytes,
) -> Array[String] raise @bit.GitError {
let result : Array[String] = []
let decoded = pktline_decode(data)
for item in decoded {
let (line_bytes, is_flush) = item
if is_flush || line_bytes.length() == 0 {
continue
}
let line = bytes_to_string(line_bytes)
match line.find("\u0000") {
None => continue
Some(idx) => {
let caps_str = trim_newline(
String::unsafe_substring(line, start=idx + 1, end=line.length()),
)
let parts = split_by_space(caps_str)
for p in parts {
result.push(p)
}
break
}
}
}
result
}
///|
pub fn parse_symrefs_from_capabilities(
caps : Array[String],
) -> Map[String, String] {
let symrefs : Map[String, String] = {}
for cap in caps {
if cap.has_prefix("symref=") {
let raw = String::unsafe_substring(cap, start=7, end=cap.length())
match raw.find(":") {
None => ()
Some(idx) => {
let source = String::unsafe_substring(raw, start=0, end=idx)
let target = String::unsafe_substring(
raw,
start=idx + 1,
end=raw.length(),
)
symrefs[source] = target
}
}
continue
}
if cap.has_prefix("symref-target:") {
let target = String::unsafe_substring(cap, start=14, end=cap.length())
symrefs["HEAD"] = target
continue
}
}
symrefs
}
///|
pub fn build_ls_refs_request(agent : String, prefixes : Array[String]) -> Bytes {
let chunks : Array[Bytes] = []
chunks.push(pktline_encode("command=ls-refs\n"))
chunks.push(pktline_encode("agent=\{agent}\n"))
chunks.push(pktline_encode("object-format=sha1\n"))
chunks.push(pktline_delim())
chunks.push(pktline_encode("peel\n"))
chunks.push(pktline_encode("symrefs\n"))
chunks.push(pktline_encode("unborn\n"))
for prefix in prefixes {
chunks.push(pktline_encode("ref-prefix \{prefix}\n"))
}
chunks.push(pktline_flush())
concat_bytes(chunks)
}
///|
pub struct LsRefsResult {
refs : Array[(@bit.ObjectId, String)]
symrefs : Map[String, String]
}
///|
pub fn parse_ls_refs_response(data : Bytes) -> LsRefsResult raise @bit.GitError {
let refs : Array[(@bit.ObjectId, String)] = []
let symrefs : Map[String, String] = {}
let lines = pktline_decode(data)
for item in lines {
let (line_bytes, is_flush) = item
if is_flush || line_bytes.length() == 0 {
continue
}
let line = line_bytes |> bytes_to_string |> trim_newline
let parts = split_by_space(line)
if parts.length() < 2 {
continue
}
let id = @bit.ObjectId::from_hex(parts[0])
let name = parts[1]
refs.push((id, name))
for i in 2.. Array[@bit.ObjectId] {
let wants : Array[@bit.ObjectId] = []
let zero = @bit.ObjectId::zero()
match symrefs.get("HEAD") {
Some(target) =>
for item in refs {
let (id, name) = item
if name == target && id != zero {
wants.push(id)
return wants
}
}
None => ()
}
for item in refs {
let (id, name) = item
if name == "HEAD" && id != zero {
wants.push(id)
return wants
}
}
if refs.length() > 0 {
for item in refs {
let (id, _) = item
if id != zero {
wants.push(id)
break
}
}
}
wants
}
///|
pub fn select_default_ref(
refs : Array[(@bit.ObjectId, String)],
symrefs : Map[String, String],
) -> (String, @bit.ObjectId)? {
let zero = @bit.ObjectId::zero()
match symrefs.get("HEAD") {
Some(target) =>
for item in refs {
let (id, name) = item
if name == target && id != zero {
return Some((name, id))
}
}
None => ()
}
for item in refs {
let (id, name) = item
if name == "HEAD" && id != zero {
return Some((name, id))
}
}
for item in refs {
let (id, name) = item
if id != zero {
return Some((name, id))
}
}
None
}
///|
/// Filter specification for partial clone
pub(all) enum FilterSpec {
NoFilter // No filter (full clone)
BlobNone // blob:none - exclude all blobs
BlobLimit(Int64) // blob:limit= - exclude blobs > n bytes
TreeDepth(Int) // tree: - limit tree depth
SparseOid(String) // sparse:oid= - sparse checkout filter
SparsePathUnsupported // sparse:path= - dropped for security
}
///|
pub fn FilterSpec::to_string(self : FilterSpec) -> String {
match self {
NoFilter => ""
BlobNone => "blob:none"
BlobLimit(n) => "blob:limit=\{n}"
TreeDepth(d) => "tree:\{d}"
SparseOid(oid) => "sparse:oid=\{oid}"
SparsePathUnsupported => "sparse:path"
}
}
///|
pub fn FilterSpec::is_partial(self : FilterSpec) -> Bool {
match self {
NoFilter => false
_ => true
}
}
///|
/// Build a fetch request for protocol v2.
/// - depth > 0: shallow clone with that depth
/// - filter: partial clone filter specification
pub fn build_fetch_request_v2(
agent : String,
wants : Array[@bit.ObjectId],
depth : Int,
filter? : FilterSpec = FilterSpec::NoFilter,
) -> Bytes {
let chunks : Array[Bytes] = []
chunks.push(pktline_encode("command=fetch\n"))
chunks.push(pktline_encode("agent=\{agent}\n"))
chunks.push(pktline_encode("object-format=sha1\n"))
chunks.push(pktline_delim())
chunks.push(pktline_encode("thin-pack\n"))
chunks.push(pktline_encode("no-progress\n"))
chunks.push(pktline_encode("ofs-delta\n"))
if depth > 0 {
chunks.push(pktline_encode("deepen \{depth}\n"))
}
// Partial clone filter
if filter.is_partial() {
chunks.push(pktline_encode("filter \{filter.to_string()}\n"))
}
for want in wants {
chunks.push(pktline_encode("want \{want.to_hex()}\n"))
}
chunks.push(pktline_encode("done\n"))
chunks.push(pktline_flush())
concat_bytes(chunks)
}
///|
pub fn build_fetch_request_v0(
agent : String,
wants : Array[@bit.ObjectId],
advertised : Array[String],
) -> Bytes {
if wants.length() == 0 {
return pktline_flush()
}
let caps = select_v0_caps(agent, advertised)
let chunks : Array[Bytes] = []
let first = "want \{wants[0].to_hex()} \{caps}\n"
chunks.push(pktline_encode(first))
for i in 1.. Bytes raise @bit.GitError {
let lines = pktline_decode(data)
let mut in_pack = false
let chunks : Array[Byte] = []
for item in lines {
let (line_bytes, is_flush) = item
if is_flush {
if in_pack {
break
}
continue
}
if line_bytes.length() == 0 {
continue
}
if !in_pack {
let line = line_bytes |> bytes_to_string |> trim_newline
if line == "packfile" {
in_pack = true
}
continue
}
// side-band: channel byte + payload
let channel = line_bytes[0].to_int()
if channel == 1 {
for i in 1.. payload[i]),
),
)
let msg = bytes_to_string_from(safe, 0)
raise @bit.GitError::ProtocolError("upload-pack error: \{msg}")
}
}
Bytes::from_array(FixedArray::makei(chunks.length(), i => chunks[i]))
}
///|
pub fn extract_pack_from_v0_response(data : Bytes) -> Bytes raise @bit.GitError {
let lines = pktline_decode(data)
let chunks : Array[Byte] = []
for item in lines {
let (line_bytes, is_flush) = item
if is_flush || line_bytes.length() == 0 {
continue
}
let channel = line_bytes[0].to_int()
if channel == 1 {
for i in 1.. payload[i]),
),
)
let msg = bytes_to_string_from(safe, 0)
raise @bit.GitError::ProtocolError("upload-pack error: \{msg}")
}
}
Bytes::from_array(FixedArray::makei(chunks.length(), i => chunks[i]))
}
///|
fn select_v0_caps(agent : String, advertised : Array[String]) -> String {
let wanted = ["side-band-64k", "thin-pack", "ofs-delta"]
let caps : Array[String] = []
for cap in wanted {
if advertised.contains(cap) {
caps.push(cap)
}
}
caps.push("agent=\{agent}")
caps.join(" ")
}
///|
fn trim_newline(s : String) -> String {
if s.has_suffix("\n") {
String::unsafe_substring(s, start=0, end=s.length() - 1)
} else {
s
}
}
///|
fn bytes_to_string(data : Bytes) -> String {
let result = StringBuilder::new()
for b in data {
result.write_char(b.to_int().unsafe_to_char())
}
result.to_string()
}
///|
fn bytes_to_string_from(data : Bytes, start : Int) -> String {
if start >= data.length() {
return ""
}
let result = StringBuilder::new()
for i in start.. Array[String] {
let parts : Array[String] = []
let mut current = StringBuilder::new()
for c in s {
if c == ' ' {
if current.to_string().length() > 0 {
parts.push(current.to_string())
current = StringBuilder::new()
}
} else {
current.write_char(c)
}
}
if current.to_string().length() > 0 {
parts.push(current.to_string())
}
parts
}
///|
fn concat_bytes(chunks : Array[Bytes]) -> Bytes {
let result : Array[Byte] = []
for chunk in chunks {
for b in chunk {
result.push(b)
}
}
Bytes::from_array(FixedArray::makei(result.length(), i => result[i]))
}