///| Upload-pack over HTTP - common logic (all targets)
///| Uses HTTP client functions passed as arguments
///|
pub async fn upload_pack_info_refs_with_http(
remote : String,
prefer_v2 : Bool,
http_get : async (String, Map[String, String]) -> (@bit.HttpResponse, Bytes) raise @bit.GitError,
) -> Bytes raise @bit.GitError {
let normalized_remote = normalize_remote_for_http_transport(remote)
let spec = parse_remote(normalized_remote)
if spec.kind != RemoteKind::Http {
raise @bit.GitError::ProtocolError("HTTP transport required")
}
let base = spec.base_url.unwrap_or(normalized_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 @bit.GitError::ProtocolError(
"Failed to fetch info/refs: HTTP \{response.code}",
)
}
data
}
///|
pub async fn upload_pack_request_with_http(
remote : String,
body : Bytes,
prefer_v2 : Bool,
http_post : async (String, Bytes, Map[String, String]) -> (
@bit.HttpResponse,
Bytes,
) raise @bit.GitError,
) -> Bytes raise @bit.GitError {
let normalized_remote = normalize_remote_for_http_transport(remote)
let spec = parse_remote(normalized_remote)
if spec.kind != RemoteKind::Http {
raise @bit.GitError::ProtocolError("HTTP transport required")
}
let base = spec.base_url.unwrap_or(normalized_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 @bit.GitError::ProtocolError(
"Failed to upload-pack: HTTP \{response.code}",
)
}
data
}
///|
pub async fn discover_upload_refs_with_http(
remote : String,
prefer_v2 : Bool,
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,
) -> (
Array[(@bit.ObjectId, String)],
Array[String],
ProtocolVersion,
Map[String, String],
) raise @bit.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)
let symrefs = parse_symrefs_from_capabilities(caps)
(refs, caps, ProtocolVersion::V0, symrefs)
}
///|
fn normalize_remote_for_http_transport(
remote : String,
) -> String raise @bit.GitError {
let spec = parse_remote(remote)
match spec.kind {
RemoteKind::Http => remote
RemoteKind::Ssh =>
match ssh_remote_to_https(remote) {
Some(url) => url
None => raise @bit.GitError::ProtocolError("HTTP transport required")
}
RemoteKind::File =>
raise @bit.GitError::ProtocolError("HTTP transport required")
}
}
///|
pub async fn fetch_pack_with_http_result(
remote : String,
wants : Array[@bit.ObjectId],
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
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,
haves? : Array[@bit.ObjectId] = [],
shallow? : Array[@bit.ObjectId] = [],
) -> FetchPackResult raise @bit.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[@bit.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~,
haves~,
shallow~,
)
let resp = upload_pack_request_with_http(remote, req, true, http_post)
return parse_fetch_response_v2(resp)
}
let req = build_fetch_request_v0(
"git/moonbit",
wants,
caps,
depth~,
filter~,
haves~,
shallow~,
)
let resp = upload_pack_request_with_http(remote, req, false, http_post)
parse_fetch_response_v0(resp)
}
///|
pub async fn fetch_pack_with_http(
remote : String,
wants : Array[@bit.ObjectId],
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
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,
haves? : Array[@bit.ObjectId] = [],
shallow? : Array[@bit.ObjectId] = [],
) -> Bytes raise @bit.GitError {
fetch_pack_with_http_result(
remote,
wants,
prefer_v2,
depth,
filter,
http_get,
http_post,
haves~,
shallow~,
).pack
}
///|
pub async fn fetch_objects_with_http(
remote : String,
wants : Array[@bit.ObjectId],
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
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,
haves? : Array[@bit.ObjectId] = [],
shallow? : Array[@bit.ObjectId] = [],
) -> Array[@bit.PackObject] raise @bit.GitError {
let pack = fetch_pack_with_http(
remote,
wants,
prefer_v2,
depth,
filter,
http_get,
http_post,
haves~,
shallow~,
)
@pack.parse_packfile(pack)
}
///|
pub async fn clone_with_http(
remote : String,
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
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,
) -> (Array[(@bit.ObjectId, String)], Array[@bit.PackObject]) raise @bit.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 = @pack.parse_packfile(pack)
(refs, objects)
}
///|
priv struct PreparedClone {
default_ref : (String, @bit.ObjectId)
pack : Bytes
pack_id : @bit.ObjectId
objects : Array[@bit.PackObject]
}
///|
/// Keep protocol negotiation shared by sync and async filesystem adapters.
async fn prepare_clone_with_http(
remote : String,
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
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,
) -> (Array[(@bit.ObjectId, String)], PreparedClone?) raise @bit.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)
if default_ref is None || wants.length() == 0 {
return (refs, None)
}
let pack = fetch_pack_with_http(
remote, wants, prefer_v2, depth, filter, http_get, http_post,
)
(
refs,
Some({
default_ref: default_ref.unwrap(),
pack,
pack_id: read_pack_trailer_id_http(pack),
objects: @pack.parse_packfile(pack),
}),
)
}
///|
pub async fn clone_to_fs_with_http(
remote : String,
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
fs : &@bit.FileSystem,
root : String,
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,
rfs : &@bit.RepoFileSystem,
no_checkout? : Bool = false,
) -> Array[(@bit.ObjectId, String)] raise @bit.GitError {
let (refs, prepared) = prepare_clone_with_http(
remote, prefer_v2, depth, filter, http_get, http_post,
)
let prepared = match prepared {
None => return refs
Some(value) => value
}
let git_dir = @bit.join_path(root, ".git")
fs.mkdir_p(@bit.join_path(git_dir, "objects/pack"))
@pack.write_packfile_with_index(fs, git_dir, prepared.pack, prepared.objects)
if filter.is_partial() {
write_promisor_file(fs, git_dir, remote)
write_pack_promisor_marker_http(
fs,
git_dir,
prepared.pack_id,
refs,
Some(prepared.default_ref),
)
let (refname, commit_id) = prepared.default_ref
write_partial_clone_refs(fs, git_dir, refname, commit_id, remote, filter)
} else {
let store = @bit.ObjectStore::from_pack(prepared.objects)
let (refname, commit_id) = prepared.default_ref
if no_checkout {
@bit.write_git_metadata(store, commit_id, refname, remote, fs, root, rfs)
} else {
@bit.materialize_clone_to_fs(
store, commit_id, refname, remote, fs, root, rfs,
)
}
}
refs
}
///|
/// Clone through asynchronous HTTP and filesystem implementations.
pub async fn[
FS : @types.AsyncFileSystem + @types.AsyncRepoFileSystem,
Client : @types.AsyncHttpClient,
] clone_to_async_fs_with_http(
remote : String,
prefer_v2 : Bool,
depth : Int,
filter : FilterSpec,
fs : FS,
root : String,
client : Client,
no_checkout? : Bool = false,
) -> Array[(@bit.ObjectId, String)] raise @bit.GitError {
let http_get = async fn(
url : String,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
@types.AsyncHttpClient::get(client, url, headers)
}
let http_post = async fn(
url : String,
body : Bytes,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
@types.AsyncHttpClient::post(client, url, body, headers)
}
let (refs, prepared) = prepare_clone_with_http(
remote, prefer_v2, depth, filter, http_get, http_post,
)
let prepared = match prepared {
None => return refs
Some(value) => value
}
let git_dir = @bit.join_path(root, ".git")
@types.AsyncFileSystem::mkdir_p(fs, @bit.join_path(git_dir, "objects/pack"))
@pack.write_packfile_with_index_async(
fs,
git_dir,
prepared.pack,
prepared.objects,
)
if filter.is_partial() {
write_promisor_file_async(fs, git_dir, remote)
write_pack_promisor_marker_http_async(
fs,
git_dir,
prepared.pack_id,
refs,
Some(prepared.default_ref),
)
let (refname, commit_id) = prepared.default_ref
write_partial_clone_refs_async(
fs, git_dir, refname, commit_id, remote, filter,
)
} else {
let store = @bit.ObjectStore::from_pack(prepared.objects)
let (refname, commit_id) = prepared.default_ref
if no_checkout {
@repo.write_git_metadata_async(
store, commit_id, refname, remote, fs, root,
)
} else {
@repo.materialize_clone_to_fs_async(
store, commit_id, refname, remote, fs, root,
)
}
}
refs
}
///|
async fn[FS : @types.AsyncFileSystem] write_promisor_file_async(
fs : FS,
git_dir : String,
remote : String,
) -> Unit raise @bit.GitError {
let promisor_dir = @bit.join_path(git_dir, "objects/info")
@types.AsyncFileSystem::mkdir_p(fs, promisor_dir)
@types.AsyncFileSystem::write_string(
fs,
@bit.join_path(promisor_dir, "promisor"),
remote + "\n",
)
}
///|
async fn[FS : @types.AsyncFileSystem] write_pack_promisor_marker_http_async(
fs : FS,
git_dir : String,
pack_id : @bit.ObjectId,
refs : Array[(@bit.ObjectId, String)],
default_ref : (String, @bit.ObjectId)?,
) -> Unit raise @bit.GitError {
let pack_dir = @bit.join_path(git_dir, "objects/pack")
@types.AsyncFileSystem::mkdir_p(fs, pack_dir)
let marker_lines : Array[String] = []
let seen : Map[String, Bool] = Map([])
for item in refs {
let (id, name) = item
if name.length() == 0 {
continue
}
let line = id.to_hex() + " " + name
if seen.contains(line) {
continue
}
seen[line] = true
marker_lines.push(line)
}
match default_ref {
Some((_, head_id)) => {
let head_line = head_id.to_hex() + " HEAD"
if !seen.contains(head_line) {
seen[head_line] = true
marker_lines.push(head_line)
}
}
None => ()
}
if marker_lines.length() == 0 {
return
}
let marker_path = @bit.join_path(
pack_dir,
"pack-\{pack_id.to_hex()}.promisor",
)
@types.AsyncFileSystem::write_string(
fs,
marker_path,
marker_lines.join("\n") + "\n",
)
}
///|
async fn[FS : @types.AsyncFileSystem] write_partial_clone_refs_async(
fs : FS,
git_dir : String,
refname : String,
commit_id : @bit.ObjectId,
remote : String,
filter : FilterSpec,
) -> Unit raise @bit.GitError {
let head_path = @bit.join_path(git_dir, "HEAD")
if refname == "HEAD" {
@types.AsyncFileSystem::write_string(
fs,
head_path,
commit_id.to_hex() + "\n",
)
} else {
@types.AsyncFileSystem::write_string(fs, head_path, "ref: \{refname}\n")
let ref_path = @bit.join_path(git_dir, refname)
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
}
}
@types.AsyncFileSystem::mkdir_p(fs, ref_dir)
@types.AsyncFileSystem::write_string(
fs,
ref_path,
commit_id.to_hex() + "\n",
)
}
let config = "[core]\n\trepositoryformatversion = 1\n\tfilemode = true\n\tbare = false\n\tlogallaliases = false\n[extensions]\n\tpartialclone = origin\n[remote \"origin\"]\n\turl = \{remote}\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n\tpromisor = true\n\tpartialclonefilter = \{filter.to_string()}\n"
@types.AsyncFileSystem::write_string(
fs,
@bit.join_path(git_dir, "config"),
config,
)
}
///|
fn write_promisor_file(
fs : &@bit.FileSystem,
git_dir : String,
remote : String,
) -> Unit raise @bit.GitError {
let promisor_dir = @bit.join_path(git_dir, "objects/info")
fs.mkdir_p(promisor_dir)
let promisor_path = @bit.join_path(promisor_dir, "promisor")
fs.write_string(promisor_path, remote + "\n")
}
///|
fn read_pack_trailer_id_http(pack : Bytes) -> @bit.ObjectId raise @bit.GitError {
if pack.length() < 20 {
raise @bit.GitError::PackfileError("Packfile too short")
}
let trailer_start = pack.length() - 20
let bytes = FixedArray::makei(20, i => pack[trailer_start + i])
@bit.ObjectId::new(bytes)
}
///|
fn write_pack_promisor_marker_http(
fs : &@bit.FileSystem,
git_dir : String,
pack_id : @bit.ObjectId,
refs : Array[(@bit.ObjectId, String)],
default_ref : (String, @bit.ObjectId)?,
) -> Unit raise @bit.GitError {
let pack_dir = @bit.join_path(git_dir, "objects/pack")
fs.mkdir_p(pack_dir)
let marker_lines : Array[String] = []
let seen : Map[String, Bool] = Map([])
for item in refs {
let (id, name) = item
if name.length() == 0 {
continue
}
let line = id.to_hex() + " " + name
if seen.contains(line) {
continue
}
seen[line] = true
marker_lines.push(line)
}
match default_ref {
Some((_, head_id)) => {
let head_line = head_id.to_hex() + " HEAD"
if !seen.contains(head_line) {
seen[head_line] = true
marker_lines.push(head_line)
}
}
None => ()
}
if marker_lines.length() == 0 {
return ()
}
let marker_content = marker_lines.join("\n") + "\n"
let base = "pack-\{pack_id.to_hex()}"
let marker_path = @bit.join_path(pack_dir, base + ".promisor")
fs.write_string(marker_path, marker_content)
}
///|
fn write_partial_clone_refs(
fs : &@bit.FileSystem,
git_dir : String,
refname : String,
commit_id : @bit.ObjectId,
remote : String,
filter : FilterSpec,
) -> Unit raise @bit.GitError {
let head_path = @bit.join_path(git_dir, "HEAD")
if refname == "HEAD" {
fs.write_string(head_path, commit_id.to_hex() + "\n")
} else {
fs.write_string(head_path, "ref: \{refname}\n")
let ref_path = @bit.join_path(git_dir, refname)
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")
}
let config_path = @bit.join_path(git_dir, "config")
let config = "[core]\n\trepositoryformatversion = 1\n\tfilemode = true\n\tbare = false\n\tlogallaliases = false\n[extensions]\n\tpartialclone = origin\n[remote \"origin\"]\n\turl = \{remote}\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n\tpromisor = true\n\tpartialclonefilter = \{filter.to_string()}\n"
fs.write_string(config_path, config)
}