///| Git HTTP push protocol - common logic (all targets)
///| Uses HttpClient functions passed as arguments
///|
/// Helper to convert Array[Byte] to Bytes
fn remote_array_to_bytes(arr : Array[Byte]) -> Bytes {
Bytes::from_array(FixedArray::makei(arr.length(), i => arr[i]))
}
///|
/// Remote repository URL
pub struct Remote {
url : String // e.g., "https://github.com/user/repo.git"
}
///|
pub fn Remote::new(url : String) -> Remote {
{ url, }
}
///|
/// Normalize URL (remove trailing .git if present)
fn normalize_url(url : String) -> String {
if url.has_suffix(".git") {
let chars = url.to_array()
let result = StringBuilder::new()
for i in 0..<(chars.length() - 4) {
result.write_char(chars[i])
}
result.to_string()
} else {
url
}
}
///|
/// Build URL for git-receive-pack info/refs
pub fn Remote::info_refs_url(self : Remote) -> String {
let base = normalize_url(self.url)
"\{base}/info/refs?service=git-receive-pack"
}
///|
/// Build URL for git-receive-pack
pub fn Remote::receive_pack_url(self : Remote) -> String {
let base = normalize_url(self.url)
"\{base}/git-receive-pack"
}
///|
/// Git push request data
pub struct PushRequest {
old_id : @bit.ObjectId
new_id : @bit.ObjectId
refname : String
packfile : Bytes
}
///|
pub fn PushRequest::new(
old_id : @bit.ObjectId,
new_id : @bit.ObjectId,
refname : String,
packfile : Bytes,
) -> PushRequest {
{ old_id, new_id, refname, packfile }
}
///|
/// Build the request body for git-receive-pack
pub fn build_receive_pack_body(req : PushRequest) -> Bytes {
let result : Array[Byte] = []
let cmd = build_ref_update(
req.old_id,
req.new_id,
req.refname,
"report-status side-band-64k",
)
let cmd_pkt = pktline_encode(cmd)
for b in cmd_pkt {
result.push(b)
}
let flush = pktline_flush()
for b in flush {
result.push(b)
}
for b in req.packfile {
result.push(b)
}
remote_array_to_bytes(result)
}
///|
/// Parse receive-pack response
pub fn parse_receive_pack_response(
data : Bytes,
) -> Result[String, @bit.GitError] {
let decoded = pktline_decode(data) catch { e => return Err(e) }
let mut unpack_ok = false
let mut ref_ok = false
let mut error_msg = ""
let mut sideband = false
let band1_bytes : Array[Byte] = []
for item in decoded {
let (line_bytes, is_flush) = item
if is_flush || line_bytes.length() == 0 {
continue
}
let first = line_bytes[0].to_int()
if first >= 1 && first <= 3 {
sideband = true
let (channel, content) = parse_sideband(line_bytes)
if channel == 1 {
for b in content {
band1_bytes.push(b)
}
} else if channel == 3 {
let msg = remote_bytes_to_string(sanitize_sideband_text(content))
.trim(chars=" \n\r\t")
.to_owned()
if msg.length() > 0 {
error_msg = msg
}
}
continue
}
let line = remote_bytes_to_string(line_bytes)
if line.has_prefix("unpack ok") {
unpack_ok = true
} else if line.has_prefix("unpack ") {
error_msg = line
} else if line.has_prefix("ok ") {
ref_ok = true
} else if line.has_prefix("ng ") {
error_msg = line
}
}
if sideband {
if error_msg.length() > 0 {
return error_msg |> @bit.GitError::ProtocolError |> Err
}
let band1 = remote_array_to_bytes(band1_bytes)
let band_decoded = pktline_decode(band1) catch { e => return Err(e) }
unpack_ok = false
ref_ok = false
error_msg = ""
for item in band_decoded {
let (line_bytes, is_flush) = item
if is_flush || line_bytes.length() == 0 {
continue
}
let line = remote_bytes_to_string(line_bytes)
if line.has_prefix("unpack ok") {
unpack_ok = true
} else if line.has_prefix("unpack ") {
error_msg = line
} else if line.has_prefix("ok ") {
ref_ok = true
} else if line.has_prefix("ng ") {
error_msg = line
}
}
}
if unpack_ok && ref_ok {
Ok("Push successful")
} else if error_msg.length() > 0 {
error_msg |> @bit.GitError::ProtocolError |> Err
} else {
"Unknown response" |> @bit.GitError::ProtocolError |> Err
}
}
///|
/// Parse side-band data
pub fn parse_sideband(data : Bytes) -> (Int, Bytes) {
if data.length() == 0 {
return (0, Bytes::from_array([]))
}
let channel = data[0].to_int()
let content : Array[Byte] = []
for i in 1.. Bytes {
let out : Array[Byte] = []
let mut i = 0
while i < data.length() {
let b = data[i].to_int()
if b == 0x1B && i + 1 < data.length() && data[i + 1].to_int() == 0x5B {
// CSI: ESC '[' params final.
let mut j = i + 2
while j < data.length() {
let c = data[j].to_int()
// Parameter / intermediate bytes: 0x20–0x3F.
if c >= 0x20 && c < 0x40 {
j = j + 1
continue
}
break
}
if j < data.length() && data[j].to_int() == 0x6D {
// CSI ... m — SGR (color/style). Keep verbatim.
while i <= j {
out.push(data[i])
i = i + 1
}
continue
}
// Non-color CSI: drop the whole sequence (replace with one ?).
out.push(b'?')
i = if j < data.length() { j + 1 } else { data.length() }
continue
}
if b == 0x09 ||
b == 0x0A ||
b == 0x0D ||
(b >= 0x20 && b < 0x7F) ||
b >= 0x80 {
out.push(data[i])
} else {
// C0 (other than tab/LF/CR), DEL, lone ESC.
out.push(b'?')
}
i = i + 1
}
remote_array_to_bytes(out)
}
///|
/// Extract capabilities from refs response
pub fn extract_capabilities(data : Bytes) -> Array[String] {
let result : Array[String] = []
let decoded = pktline_decode(data) catch { _ => return result }
for item in decoded {
let (line_bytes, _) = item
if line_bytes.length() == 0 {
continue
}
let line = remote_bytes_to_string(line_bytes)
match line.find("\u0000") {
None => continue
Some(idx) => {
let caps_str = remote_substring_from(line, idx + 1)
let mut current : Array[Char] = []
for c in caps_str {
if c == ' ' || c == '\n' {
if current.length() > 0 {
let sb = StringBuilder::new()
for ch in current {
sb.write_char(ch)
}
result.push(sb.to_string())
current = []
}
} else {
current.push(c)
}
}
if current.length() > 0 {
let sb = StringBuilder::new()
for ch in current {
sb.write_char(ch)
}
result.push(sb.to_string())
}
break
}
}
}
result
}
///|
fn remote_substring_from(s : String, start : Int) -> String {
let chars = s.to_array()
if start >= chars.length() {
return ""
}
let result = StringBuilder::new()
for i in start.. String {
let result = StringBuilder::new()
for b in data {
result.write_char(b.to_int().unsafe_to_char())
}
result.to_string()
}
///|
/// Discover refs using provided HTTP client functions
pub async fn discover_refs_with_http(
remote : Remote,
http_get : async (String, Map[String, String]) -> (@bit.HttpResponse, Bytes) raise @bit.GitError,
) -> (Array[(@bit.ObjectId, String)], Array[String]) raise @bit.GitError {
let url = remote.info_refs_url()
let headers : Map[String, String] = {
"User-Agent": "git/moonbit",
"Accept": "application/x-git-receive-pack-advertisement",
}
let (response, data) = http_get(url, headers)
if response.code != 200 {
raise @bit.GitError::ProtocolError(
"Failed to discover refs: HTTP \{response.code}",
)
}
let refs = parse_refs(data)
let caps = extract_capabilities(data)
(refs, caps)
}
///|
/// Push using provided HTTP client functions
pub async fn push_with_http(
remote : Remote,
req : PushRequest,
http_get : async (String, Map[String, String]) -> (@bit.HttpResponse, Bytes) raise @bit.GitError,
http_post : async (String, Bytes, Map[String, String]) -> (
@bit.HttpResponse,
Bytes,
) raise @bit.GitError,
) -> String raise @bit.GitError {
let (_, caps) = discover_refs_with_http(remote, http_get)
let has_report_status = caps.contains("report-status")
if !has_report_status {
raise @bit.GitError::ProtocolError("Server does not support report-status")
}
let url = remote.receive_pack_url()
let body = build_receive_pack_body(req)
let headers : Map[String, String] = {
"User-Agent": "git/moonbit",
"Content-Type": "application/x-git-receive-pack-request",
"Accept": "application/x-git-receive-pack-result",
}
let (response, response_data) = http_post(url, body, headers)
if response.code != 200 {
raise @bit.GitError::ProtocolError("Failed to push: HTTP \{response.code}")
}
match parse_receive_pack_response(response_data) {
Ok(msg) => msg
Err(e) => raise e
}
}