///| 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(), fn(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 = 0; i < chars.length() - 4; i = i + 1 {
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 : ObjectId
new_id : ObjectId
refname : String
packfile : Bytes
}
///|
pub fn PushRequest::new(
old_id : ObjectId,
new_id : 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, 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 = ""
for item in 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 {
Err(GitError::ProtocolError(error_msg))
} else {
Err(GitError::ProtocolError("Unknown response"))
}
}
///|
/// 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 = 1; i < data.length(); i = i + 1 {
content.push(data[i])
}
(channel, remote_array_to_bytes(content))
}
///|
/// 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 = start; i < chars.length(); i = i + 1 {
result.write_char(chars[i])
}
result.to_string()
}
///|
fn remote_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()
}
///|
/// Discover refs using provided HTTP client functions
pub async fn discover_refs_with_http(
remote : Remote,
http_get : async (String, Map[String, String]) -> (HttpResponse, Bytes) raise GitError,
) -> (Array[(ObjectId, String)], Array[String]) raise 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 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]) -> (HttpResponse, Bytes) raise GitError,
http_post : async (String, Bytes, Map[String, String]) -> (
HttpResponse,
Bytes,
) raise GitError,
) -> String raise GitError {
let (_, caps) = discover_refs_with_http(remote, http_get)
let has_report_status = caps.contains("report-status")
if not(has_report_status) {
raise 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 GitError::ProtocolError("Failed to push: HTTP \{response.code}")
}
match parse_receive_pack_response(response_data) {
Ok(msg) => msg
Err(e) => raise e
}
}