///| Upload-pack over HTTP - common logic (all targets)
///| Uses HTTP client functions passed as arguments
// Type aliases for HTTP client functions (for documentation)
// HttpGetFn = async (String, Map[String, String]) -> (HttpResponse, Bytes) raise GitError
// HttpPostFn = async (String, Bytes, Map[String, String]) -> (HttpResponse, Bytes) raise GitError
///|
/// Fetch info/refs using provided HTTP client
pub async fn upload_pack_info_refs_with_http(
remote : String,
prefer_v2 : Bool,
http_get : async (String, Map[String, String]) -> (HttpResponse, Bytes) raise GitError,
) -> Bytes raise GitError {
let spec = parse_remote(remote)
if spec.kind != RemoteKind::Http {
raise GitError::ProtocolError("HTTP transport required")
}
let base = spec.base_url.unwrap_or(remote)
let url = "\{base}/info/refs?service=git-upload-pack"
let headers : Map[String, String] = {
"User-Agent": "git/moonbit",
"Accept": "application/x-git-upload-pack-advertisement",
}
if prefer_v2 {
headers.set("Git-Protocol", "version=2")
}
let (response, data) = http_get(url, headers)
if response.code != 200 {
raise GitError::ProtocolError(
"Failed to fetch info/refs: HTTP \{response.code}",
)
}
data
}
///|
/// Send upload-pack request using provided HTTP client
pub async fn upload_pack_request_with_http(
remote : String,
body : Bytes,
prefer_v2 : Bool,
http_post : async (String, Bytes, Map[String, String]) -> (
HttpResponse,
Bytes,
) raise GitError,
) -> Bytes raise GitError {
let spec = parse_remote(remote)
if spec.kind != RemoteKind::Http {
raise GitError::ProtocolError("HTTP transport required")
}
let base = spec.base_url.unwrap_or(remote)
let url = "\{base}/git-upload-pack"
let headers : Map[String, String] = {
"User-Agent": "git/moonbit",
"Content-Type": "application/x-git-upload-pack-request",
"Accept": "application/x-git-upload-pack-result",
}
if prefer_v2 {
headers.set("Git-Protocol", "version=2")
}
let (response, data) = http_post(url, body, headers)
if response.code != 200 {
raise GitError::ProtocolError(
"Failed to upload-pack: HTTP \{response.code}",
)
}
data
}
///|
/// Discover upload refs using provided HTTP client
pub async fn discover_upload_refs_with_http(
remote : String,
prefer_v2 : Bool,
http_get : async (String, Map[String, String]) -> (HttpResponse, Bytes) raise GitError,
http_post : async (String, Bytes, Map[String, String]) -> (
HttpResponse,
Bytes,
) raise GitError,
) -> (
Array[(ObjectId, String)],
Array[String],
ProtocolVersion,
Map[String, String],
) raise GitError {
let adv = upload_pack_info_refs_with_http(remote, prefer_v2, http_get)
let version = detect_protocol_version(adv)
if version == ProtocolVersion::V2 {
let caps = parse_v2_advertised_caps(adv)
let req = build_ls_refs_request("git/moonbit", [
"refs/heads/", "refs/tags/", "HEAD",
])
let resp = upload_pack_request_with_http(remote, req, true, http_post)
let parsed = parse_ls_refs_response(resp)
return (parsed.refs, caps, version, parsed.symrefs)
}
let refs = parse_refs(adv)
let caps = extract_capabilities(adv)
(refs, caps, ProtocolVersion::V0, {})
}
///|
/// Fetch pack using provided HTTP client.
/// - depth > 0: shallow clone with that depth (v2 only)
/// - filter: partial clone filter (v2 only)
pub async fn fetch_pack_with_http(
remote : String,
wants : Array[ObjectId],
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
http_get : async (String, Map[String, String]) -> (HttpResponse, Bytes) raise GitError,
http_post : async (String, Bytes, Map[String, String]) -> (
HttpResponse,
Bytes,
) raise GitError,
) -> Bytes raise GitError {
let (refs, caps, version, _symrefs) = discover_upload_refs_with_http(
remote, prefer_v2, http_get, http_post,
)
let wants = if wants.length() == 0 {
let ids : Array[ObjectId] = []
for item in refs {
let (id, _) = item
ids.push(id)
}
ids
} else {
wants
}
if version == ProtocolVersion::V2 {
let req = build_fetch_request_v2("git/moonbit", wants, depth, filter~)
let resp = upload_pack_request_with_http(remote, req, true, http_post)
return extract_pack_from_v2_response(resp)
}
// v0 protocol doesn't support shallow/filter, ignore them
let req = build_fetch_request_v0("git/moonbit", wants, caps)
let resp = upload_pack_request_with_http(remote, req, false, http_post)
extract_pack_from_v0_response(resp)
}
///|
/// Fetch objects using provided HTTP client.
/// - depth > 0: shallow clone with that depth
/// - filter: partial clone filter
pub async fn fetch_objects_with_http(
remote : String,
wants : Array[ObjectId],
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
http_get : async (String, Map[String, String]) -> (HttpResponse, Bytes) raise GitError,
http_post : async (String, Bytes, Map[String, String]) -> (
HttpResponse,
Bytes,
) raise GitError,
) -> Array[PackObject] raise GitError {
let pack = fetch_pack_with_http(
remote, wants, prefer_v2, depth, filter, http_get, http_post,
)
parse_packfile(pack)
}
///|
/// Clone using provided HTTP client.
/// - depth > 0: shallow clone with that depth
/// - filter: partial clone filter
pub async fn clone_with_http(
remote : String,
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
http_get : async (String, Map[String, String]) -> (HttpResponse, Bytes) raise GitError,
http_post : async (String, Bytes, Map[String, String]) -> (
HttpResponse,
Bytes,
) raise GitError,
) -> (Array[(ObjectId, String)], Array[PackObject]) raise GitError {
let (refs, _caps, _version, symrefs) = discover_upload_refs_with_http(
remote, prefer_v2, http_get, http_post,
)
let wants = select_default_wants(refs, symrefs)
let pack = fetch_pack_with_http(
remote, wants, prefer_v2, depth, filter, http_get, http_post,
)
let objects = parse_packfile(pack)
(refs, objects)
}
///|
/// Clone and checkout into filesystem using provided HTTP client.
/// - depth > 0: shallow clone with that depth
/// - filter: partial clone filter (if BlobNone, skip checkout)
pub async fn clone_to_fs_with_http(
remote : String,
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
fs : &FileSystem,
root : String,
http_get : async (String, Map[String, String]) -> (HttpResponse, Bytes) raise GitError,
http_post : async (String, Bytes, Map[String, String]) -> (
HttpResponse,
Bytes,
) raise GitError,
) -> Array[(ObjectId, String)] raise GitError {
let (refs, _caps, _version, symrefs) = discover_upload_refs_with_http(
remote, prefer_v2, http_get, http_post,
)
let wants = select_default_wants(refs, symrefs)
let default_ref = select_default_ref(refs, symrefs)
match default_ref {
None => return refs
Some(_) => ()
}
if wants.length() == 0 {
return refs
}
// Phase 1: Fetch pack, parse, and write pack to disk
// This allows pack bytes to be released after writing
let objects = {
let pack = fetch_pack_with_http(
remote, wants, prefer_v2, depth, filter, http_get, http_post,
)
let objects = parse_packfile(pack)
// Write pack and index to disk immediately
match default_ref {
Some(_) => {
let git_dir = join_path(root, ".git")
fs.mkdir_p(join_path(git_dir, "objects/pack"))
write_packfile_with_index(fs, git_dir, pack, objects)
// Write promisor file if partial clone
if filter.is_partial() {
write_promisor_file(fs, git_dir, remote)
}
}
None => ()
}
objects
// pack goes out of scope here and can be GC'd
}
// Phase 2: Checkout (pack bytes no longer needed)
// Skip checkout for partial clone with blob:none (no blobs to checkout)
match filter {
BlobNone =>
// For partial clone, only write refs and config, don't checkout files
match default_ref {
Some((refname, commit_id)) => {
let git_dir = join_path(root, ".git")
write_partial_clone_refs(fs, git_dir, refname, commit_id, remote)
}
None => ()
}
_ => {
let store = ObjectStore::from_pack(objects)
match default_ref {
None => ()
Some((refname, commit_id)) =>
materialize_clone_to_fs(store, commit_id, refname, remote, fs, root)
}
}
}
refs
}
///|
/// Write promisor file to mark this as a partial clone repository
fn write_promisor_file(
fs : &FileSystem,
git_dir : String,
remote : String,
) -> Unit raise GitError {
let promisor_dir = join_path(git_dir, "objects/info")
fs.mkdir_p(promisor_dir)
let promisor_path = join_path(promisor_dir, "promisor")
// Format: remote URL that can fulfill missing objects
fs.write_string(promisor_path, remote + "\n")
}
///|
/// Write refs for partial clone (without checkout)
fn write_partial_clone_refs(
fs : &FileSystem,
git_dir : String,
refname : String,
commit_id : ObjectId,
remote : String,
) -> Unit raise GitError {
// Write HEAD
let head_path = join_path(git_dir, "HEAD")
fs.write_string(head_path, "ref: \{refname}\n")
// Write the ref - compute parent directory
let ref_path = join_path(git_dir, refname)
// Get parent directory by finding last /
let ref_dir = {
let mut last_slash = -1
for i, c in ref_path {
if c == '/' {
last_slash = i
}
}
if last_slash > 0 {
String::unsafe_substring(ref_path, start=0, end=last_slash)
} else {
git_dir
}
}
fs.mkdir_p(ref_dir)
fs.write_string(ref_path, commit_id.to_hex() + "\n")
// Write config with remote
let config_path = join_path(git_dir, "config")
let config = "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallaliases = false\n[remote \"origin\"]\n\turl = \{remote}\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n\tpromisor = true\n\tpartialclonefilter = blob:none\n"
fs.write_string(config_path, config)
}