///| Upload-pack protocol helpers (v0/v2)
///|
pub(all) enum ProtocolVersion {
V0
V2
} derive(Eq)
///|
pub fn detect_protocol_version(data : Bytes) -> ProtocolVersion raise 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 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 = trim_newline(bytes_to_string(line_bytes))
if line.has_prefix("#") || line.has_prefix("version ") {
continue
}
result.push(line)
}
result
}
///|
pub fn parse_v0_advertised_caps(data : Bytes) -> Array[String] raise 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 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[(ObjectId, String)]
symrefs : Map[String, String]
}
///|
pub fn parse_ls_refs_response(data : Bytes) -> LsRefsResult raise GitError {
let refs : Array[(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 = trim_newline(bytes_to_string(line_bytes))
let parts = split_by_space(line)
if parts.length() < 2 {
continue
}
let id = ObjectId::from_hex(parts[0])
let name = parts[1]
refs.push((id, name))
for i in 2.. Array[ObjectId] {
let wants : Array[ObjectId] = []
match symrefs.get("HEAD") {
Some(target) =>
for item in refs {
let (id, name) = item
if name == target {
wants.push(id)
return wants
}
}
None => ()
}
for item in refs {
let (id, name) = item
if name == "HEAD" {
wants.push(id)
return wants
}
}
if refs.length() > 0 {
let (id, _) = refs[0]
wants.push(id)
}
wants
}
///|
pub fn select_default_ref(
refs : Array[(ObjectId, String)],
symrefs : Map[String, String],
) -> (String, ObjectId)? {
match symrefs.get("HEAD") {
Some(target) =>
for item in refs {
let (id, name) = item
if name == target {
return Some((name, id))
}
}
None => ()
}
for item in refs {
let (id, name) = item
if name == "HEAD" {
return Some((name, id))
}
}
if refs.length() > 0 {
let (id, name) = refs[0]
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
}
///|
pub fn FilterSpec::to_string(self : FilterSpec) -> String {
match self {
NoFilter => ""
BlobNone => "blob:none"
BlobLimit(n) => "blob:limit=\{n}"
TreeDepth(d) => "tree:\{d}"
}
}
///|
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[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[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 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 not(in_pack) {
let line = trim_newline(bytes_to_string(line_bytes))
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.. Bytes raise 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.. 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(), fn(i) { result[i] }))
}