///| JS-exported local Git operations backed by `src/lib`.
///|
fn[T] js_wrap_error(f : () -> T raise @bit.GitError) -> Result[T, String] {
Ok(f()) catch {
@bit.GitError::IoError(error) => Err(error)
@bit.GitError::InvalidObject(error) => Err(error)
@bit.GitError::HashMismatch(expected, actual) =>
Err("hash mismatch: \{expected} vs \{actual}")
@bit.GitError::PackfileError(error) => Err("packfile error: \{error}")
@bit.GitError::ProtocolError(error) => Err("protocol error: \{error}")
}
}
///|
async fn[T] js_wrap_error_async(
f : async () -> T raise @bit.GitError,
) -> Result[T, String] {
Ok(f()) catch {
@bit.GitError::IoError(error) => Err(error)
@bit.GitError::InvalidObject(error) => Err(error)
@bit.GitError::HashMismatch(expected, actual) =>
Err("hash mismatch: \{expected} vs \{actual}")
@bit.GitError::PackfileError(error) => Err("packfile error: \{error}")
@bit.GitError::ProtocolError(error) => Err("protocol error: \{error}")
}
}
///|
fn[T] js_export_promise(f : async () -> T) -> @js_async.Promise[T] {
@js_async.Promise::from_async(f)
}
///|
extern "js" fn js_webcrypto_resolve_ssh_ed25519_public_key_impl(
private_key_pem : String,
comment : String,
) -> @js_async.Promise[String] =
#| async (privateKeyPem, comment) => {
#| const helpers = globalThis.__bitGitJsSshSigHelpers ??= (() => {
#| const encoder = new TextEncoder();
#| const ensureSubtle = () => {
#| if (!globalThis.crypto?.subtle) {
#| throw new Error('Web Crypto SubtleCrypto is not available');
#| }
#| return globalThis.crypto.subtle;
#| };
#| const decodeBase64 = (value) => {
#| if (typeof atob === 'function') {
#| return Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
#| }
#| if (typeof Buffer !== 'undefined') {
#| return new Uint8Array(Buffer.from(value, 'base64'));
#| }
#| throw new Error('base64 decode is not available in this runtime');
#| };
#| const encodeBase64 = (bytes) => {
#| if (typeof btoa === 'function') {
#| let text = '';
#| for (let i = 0; i < bytes.length; i += 1) {
#| text += String.fromCharCode(bytes[i]);
#| }
#| return btoa(text);
#| }
#| if (typeof Buffer !== 'undefined') {
#| return Buffer.from(bytes).toString('base64');
#| }
#| throw new Error('base64 encode is not available in this runtime');
#| };
#| const base64UrlToBytes = (value) => {
#| const normalized = String(value ?? '').replace(/-/g, '+').replace(/_/g, '/');
#| const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
#| return decodeBase64(padded);
#| };
#| const decodePem = (pem) => {
#| const body = String(pem ?? '')
#| .replace(/-----BEGIN[^-]*-----/, '')
#| .replace(/-----END[^-]*-----/, '')
#| .replace(/\s+/g, '');
#| if (!body) {
#| throw new Error('private key PEM is empty');
#| }
#| return decodeBase64(body);
#| };
#| const importPrivateKey = async (pem) => {
#| return ensureSubtle().importKey(
#| 'pkcs8',
#| decodePem(pem),
#| { name: 'Ed25519' },
#| true,
#| ['sign'],
#| );
#| };
#| const exportPublicKeyRaw = async (pem) => {
#| const privateKey = await importPrivateKey(pem);
#| const jwk = await ensureSubtle().exportKey('jwk', privateKey);
#| if (typeof jwk?.x !== 'string' || jwk.x.length === 0) {
#| throw new Error('missing Ed25519 public key component');
#| }
#| return base64UrlToBytes(jwk.x);
#| };
#| const concatBytes = (...parts) => {
#| let total = 0;
#| for (const part of parts) total += part.length;
#| const out = new Uint8Array(total);
#| let offset = 0;
#| for (const part of parts) {
#| out.set(part, offset);
#| offset += part.length;
#| }
#| return out;
#| };
#| const uint32be = (value) => new Uint8Array([
#| (value >>> 24) & 0xff,
#| (value >>> 16) & 0xff,
#| (value >>> 8) & 0xff,
#| value & 0xff,
#| ]);
#| const sshString = (value) => {
#| const bytes = typeof value === 'string' ? encoder.encode(value) : value;
#| return concatBytes(uint32be(bytes.length), bytes);
#| };
#| const wrapBase64 = (value, width = 70) => {
#| const parts = [];
#| for (let i = 0; i < value.length; i += width) {
#| parts.push(value.slice(i, i + width));
#| }
#| return parts.join('\n');
#| };
#| const resolveSshEd25519PublicKey = async (pem, keyComment) => {
#| const publicKeyRaw = await exportPublicKeyRaw(pem);
#| const publicKeyWire = concatBytes(
#| sshString('ssh-ed25519'),
#| sshString(publicKeyRaw),
#| );
#| const suffix = String(keyComment ?? '').length > 0 ? ` ${String(keyComment)}` : '';
#| return `ssh-ed25519 ${encodeBase64(publicKeyWire)}${suffix}`;
#| };
#| const signGitPayloadSshEd25519 = async (pem, messageText) => {
#| const subtle = ensureSubtle();
#| const privateKey = await importPrivateKey(pem);
#| const publicKeyRaw = await exportPublicKeyRaw(pem);
#| const payloadBytes = encoder.encode(String(messageText ?? ''));
#| const payloadHash = new Uint8Array(
#| await subtle.digest('SHA-512', payloadBytes),
#| );
#| const signedData = concatBytes(
#| encoder.encode('SSHSIG'),
#| sshString('git'),
#| sshString(new Uint8Array()),
#| sshString('sha512'),
#| sshString(payloadHash),
#| );
#| const rawSignature = new Uint8Array(
#| await subtle.sign('Ed25519', privateKey, signedData),
#| );
#| const publicKeyBlob = concatBytes(
#| sshString('ssh-ed25519'),
#| sshString(publicKeyRaw),
#| );
#| const signatureBlob = concatBytes(
#| sshString('ssh-ed25519'),
#| sshString(rawSignature),
#| );
#| const sshSigBlob = concatBytes(
#| encoder.encode('SSHSIG'),
#| uint32be(1),
#| sshString(publicKeyBlob),
#| sshString('git'),
#| sshString(new Uint8Array()),
#| sshString('sha512'),
#| sshString(signatureBlob),
#| );
#| return (
#| '-----BEGIN SSH SIGNATURE-----\n' +
#| wrapBase64(encodeBase64(sshSigBlob)) +
#| '\n-----END SSH SIGNATURE-----\n'
#| );
#| };
#| return { resolveSshEd25519PublicKey, signGitPayloadSshEd25519 };
#| })();
#| return helpers.resolveSshEd25519PublicKey(privateKeyPem, comment);
#| }
///|
extern "js" fn js_webcrypto_sign_git_payload_ssh_ed25519_impl(
private_key_pem : String,
payload : String,
) -> @js_async.Promise[String] =
#| async (privateKeyPem, payload) => {
#| const helpers = globalThis.__bitGitJsSshSigHelpers ??= (() => {
#| const encoder = new TextEncoder();
#| const ensureSubtle = () => {
#| if (!globalThis.crypto?.subtle) {
#| throw new Error('Web Crypto SubtleCrypto is not available');
#| }
#| return globalThis.crypto.subtle;
#| };
#| const decodeBase64 = (value) => {
#| if (typeof atob === 'function') {
#| return Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
#| }
#| if (typeof Buffer !== 'undefined') {
#| return new Uint8Array(Buffer.from(value, 'base64'));
#| }
#| throw new Error('base64 decode is not available in this runtime');
#| };
#| const encodeBase64 = (bytes) => {
#| if (typeof btoa === 'function') {
#| let text = '';
#| for (let i = 0; i < bytes.length; i += 1) {
#| text += String.fromCharCode(bytes[i]);
#| }
#| return btoa(text);
#| }
#| if (typeof Buffer !== 'undefined') {
#| return Buffer.from(bytes).toString('base64');
#| }
#| throw new Error('base64 encode is not available in this runtime');
#| };
#| const base64UrlToBytes = (value) => {
#| const normalized = String(value ?? '').replace(/-/g, '+').replace(/_/g, '/');
#| const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
#| return decodeBase64(padded);
#| };
#| const decodePem = (pem) => {
#| const body = String(pem ?? '')
#| .replace(/-----BEGIN[^-]*-----/, '')
#| .replace(/-----END[^-]*-----/, '')
#| .replace(/\s+/g, '');
#| if (!body) {
#| throw new Error('private key PEM is empty');
#| }
#| return decodeBase64(body);
#| };
#| const importPrivateKey = async (pem) => {
#| return ensureSubtle().importKey(
#| 'pkcs8',
#| decodePem(pem),
#| { name: 'Ed25519' },
#| true,
#| ['sign'],
#| );
#| };
#| const exportPublicKeyRaw = async (pem) => {
#| const privateKey = await importPrivateKey(pem);
#| const jwk = await ensureSubtle().exportKey('jwk', privateKey);
#| if (typeof jwk?.x !== 'string' || jwk.x.length === 0) {
#| throw new Error('missing Ed25519 public key component');
#| }
#| return base64UrlToBytes(jwk.x);
#| };
#| const concatBytes = (...parts) => {
#| let total = 0;
#| for (const part of parts) total += part.length;
#| const out = new Uint8Array(total);
#| let offset = 0;
#| for (const part of parts) {
#| out.set(part, offset);
#| offset += part.length;
#| }
#| return out;
#| };
#| const uint32be = (value) => new Uint8Array([
#| (value >>> 24) & 0xff,
#| (value >>> 16) & 0xff,
#| (value >>> 8) & 0xff,
#| value & 0xff,
#| ]);
#| const sshString = (value) => {
#| const bytes = typeof value === 'string' ? encoder.encode(value) : value;
#| return concatBytes(uint32be(bytes.length), bytes);
#| };
#| const wrapBase64 = (value, width = 70) => {
#| const parts = [];
#| for (let i = 0; i < value.length; i += width) {
#| parts.push(value.slice(i, i + width));
#| }
#| return parts.join('\n');
#| };
#| const resolveSshEd25519PublicKey = async (pem, keyComment) => {
#| const publicKeyRaw = await exportPublicKeyRaw(pem);
#| const publicKeyWire = concatBytes(
#| sshString('ssh-ed25519'),
#| sshString(publicKeyRaw),
#| );
#| const suffix = String(keyComment ?? '').length > 0 ? ` ${String(keyComment)}` : '';
#| return `ssh-ed25519 ${encodeBase64(publicKeyWire)}${suffix}`;
#| };
#| const signGitPayloadSshEd25519 = async (pem, messageText) => {
#| const subtle = ensureSubtle();
#| const privateKey = await importPrivateKey(pem);
#| const publicKeyRaw = await exportPublicKeyRaw(pem);
#| const payloadBytes = encoder.encode(String(messageText ?? ''));
#| const payloadHash = new Uint8Array(
#| await subtle.digest('SHA-512', payloadBytes),
#| );
#| const signedData = concatBytes(
#| encoder.encode('SSHSIG'),
#| sshString('git'),
#| sshString(new Uint8Array()),
#| sshString('sha512'),
#| sshString(payloadHash),
#| );
#| const rawSignature = new Uint8Array(
#| await subtle.sign('Ed25519', privateKey, signedData),
#| );
#| const publicKeyBlob = concatBytes(
#| sshString('ssh-ed25519'),
#| sshString(publicKeyRaw),
#| );
#| const signatureBlob = concatBytes(
#| sshString('ssh-ed25519'),
#| sshString(rawSignature),
#| );
#| const sshSigBlob = concatBytes(
#| encoder.encode('SSHSIG'),
#| uint32be(1),
#| sshString(publicKeyBlob),
#| sshString('git'),
#| sshString(new Uint8Array()),
#| sshString('sha512'),
#| sshString(signatureBlob),
#| );
#| return (
#| '-----BEGIN SSH SIGNATURE-----\n' +
#| wrapBase64(encodeBase64(sshSigBlob)) +
#| '\n-----END SSH SIGNATURE-----\n'
#| );
#| };
#| return { resolveSshEd25519PublicKey, signGitPayloadSshEd25519 };
#| })();
#| return helpers.signGitPayloadSshEd25519(privateKeyPem, payload);
#| }
///|
extern "js" fn js_webcrypto_ensure_ssh_ed25519_verify_helpers() -> Unit =
#| () => {
#| const existing = globalThis.__bitGitJsSshSigHelpers;
#| if (
#| existing &&
#| typeof existing.resolveSshEd25519PublicKey === 'function' &&
#| typeof existing.signGitPayloadSshEd25519 === 'function' &&
#| typeof existing.verifyGitPayloadSshEd25519 === 'function'
#| ) {
#| return;
#| }
#| globalThis.__bitGitJsSshSigHelpers = (() => {
#| const encoder = new TextEncoder();
#| const decoder = new TextDecoder();
#| const ensureSubtle = () => {
#| if (!globalThis.crypto?.subtle) {
#| throw new Error('Web Crypto SubtleCrypto is not available');
#| }
#| return globalThis.crypto.subtle;
#| };
#| const decodeBase64 = (value) => {
#| if (typeof atob === 'function') {
#| return Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
#| }
#| if (typeof Buffer !== 'undefined') {
#| return new Uint8Array(Buffer.from(value, 'base64'));
#| }
#| throw new Error('base64 decode is not available in this runtime');
#| };
#| const encodeBase64 = (bytes) => {
#| if (typeof btoa === 'function') {
#| let text = '';
#| for (let i = 0; i < bytes.length; i += 1) {
#| text += String.fromCharCode(bytes[i]);
#| }
#| return btoa(text);
#| }
#| if (typeof Buffer !== 'undefined') {
#| return Buffer.from(bytes).toString('base64');
#| }
#| throw new Error('base64 encode is not available in this runtime');
#| };
#| const base64UrlToBytes = (value) => {
#| const normalized = String(value ?? '').replace(/-/g, '+').replace(/_/g, '/');
#| const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
#| return decodeBase64(padded);
#| };
#| const decodePem = (pem) => {
#| const body = String(pem ?? '')
#| .replace(/-----BEGIN[^-]*-----/, '')
#| .replace(/-----END[^-]*-----/, '')
#| .replace(/\s+/g, '');
#| if (!body) {
#| throw new Error('private key PEM is empty');
#| }
#| return decodeBase64(body);
#| };
#| const importPrivateKey = async (pem) => {
#| return ensureSubtle().importKey(
#| 'pkcs8',
#| decodePem(pem),
#| { name: 'Ed25519' },
#| true,
#| ['sign'],
#| );
#| };
#| const importPublicKeyRaw = async (publicKeyRaw) => {
#| return ensureSubtle().importKey(
#| 'raw',
#| publicKeyRaw,
#| { name: 'Ed25519' },
#| true,
#| ['verify'],
#| );
#| };
#| const exportPublicKeyRaw = async (pem) => {
#| const privateKey = await importPrivateKey(pem);
#| const jwk = await ensureSubtle().exportKey('jwk', privateKey);
#| if (typeof jwk?.x !== 'string' || jwk.x.length === 0) {
#| throw new Error('missing Ed25519 public key component');
#| }
#| return base64UrlToBytes(jwk.x);
#| };
#| const concatBytes = (...parts) => {
#| let total = 0;
#| for (const part of parts) total += part.length;
#| const out = new Uint8Array(total);
#| let offset = 0;
#| for (const part of parts) {
#| out.set(part, offset);
#| offset += part.length;
#| }
#| return out;
#| };
#| const uint32be = (value) => new Uint8Array([
#| (value >>> 24) & 0xff,
#| (value >>> 16) & 0xff,
#| (value >>> 8) & 0xff,
#| value & 0xff,
#| ]);
#| const readUint32be = (bytes, offset) => {
#| if (offset + 4 > bytes.length) {
#| throw new Error('truncated sshsig data');
#| }
#| return (
#| ((bytes[offset] << 24) >>> 0) |
#| (bytes[offset + 1] << 16) |
#| (bytes[offset + 2] << 8) |
#| bytes[offset + 3]
#| ) >>> 0;
#| };
#| const sshString = (value) => {
#| const bytes = typeof value === 'string' ? encoder.encode(value) : value;
#| return concatBytes(uint32be(bytes.length), bytes);
#| };
#| const readSshString = (bytes, offset) => {
#| const length = readUint32be(bytes, offset);
#| const start = offset + 4;
#| const end = start + length;
#| if (end > bytes.length) {
#| throw new Error('truncated ssh string');
#| }
#| return [bytes.slice(start, end), end];
#| };
#| const wrapBase64 = (value, width = 70) => {
#| const parts = [];
#| for (let i = 0; i < value.length; i += width) {
#| parts.push(value.slice(i, i + width));
#| }
#| return parts.join('\n');
#| };
#| const decodeUtf8 = (bytes) => decoder.decode(bytes);
#| const bytesEqual = (left, right) => {
#| if (left.length !== right.length) {
#| return false;
#| }
#| for (let i = 0; i < left.length; i += 1) {
#| if (left[i] !== right[i]) {
#| return false;
#| }
#| }
#| return true;
#| };
#| const normalizeNewlines = (value) =>
#| String(value ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
#| const resolveSshEd25519PublicKey = async (pem, keyComment) => {
#| const publicKeyRaw = await exportPublicKeyRaw(pem);
#| const publicKeyWire = concatBytes(
#| sshString('ssh-ed25519'),
#| sshString(publicKeyRaw),
#| );
#| const suffix = String(keyComment ?? '').length > 0 ? ` ${String(keyComment)}` : '';
#| return `ssh-ed25519 ${encodeBase64(publicKeyWire)}${suffix}`;
#| };
#| const parsePublicKeyText = (publicKeyText) => {
#| const parts = String(publicKeyText ?? '').trim().split(/\s+/);
#| if (parts.length < 2 || parts[0] !== 'ssh-ed25519') {
#| throw new Error('expected ssh-ed25519 public key text');
#| }
#| const publicKeyBlob = decodeBase64(parts[1]);
#| let offset = 0;
#| const [keyTypeBytes, nextOffset] = readSshString(publicKeyBlob, offset);
#| offset = nextOffset;
#| const keyType = decodeUtf8(keyTypeBytes);
#| if (keyType !== 'ssh-ed25519') {
#| throw new Error(`unsupported ssh public key type: ${keyType}`);
#| }
#| const [publicKeyRaw, endOffset] = readSshString(publicKeyBlob, offset);
#| if (endOffset !== publicKeyBlob.length) {
#| throw new Error('unexpected trailing data in ssh public key');
#| }
#| return publicKeyRaw;
#| };
#| const parseSshSignatureArmor = (signatureText) => {
#| const begin = '-----BEGIN SSH SIGNATURE-----';
#| const end = '-----END SSH SIGNATURE-----';
#| const normalized = normalizeNewlines(signatureText).trim();
#| if (!normalized.startsWith(begin) || !normalized.endsWith(end)) {
#| throw new Error('expected SSH SIGNATURE armor');
#| }
#| const body = normalized
#| .slice(begin.length, normalized.length - end.length)
#| .replace(/\s+/g, '');
#| if (body.length === 0) {
#| throw new Error('empty SSH SIGNATURE body');
#| }
#| const bytes = decodeBase64(body);
#| if (decodeUtf8(bytes.slice(0, 6)) !== 'SSHSIG') {
#| throw new Error('invalid SSHSIG magic');
#| }
#| let offset = 6;
#| const version = readUint32be(bytes, offset);
#| offset += 4;
#| if (version !== 1) {
#| throw new Error(`unsupported SSHSIG version: ${version}`);
#| }
#| const [publicKeyBlob, publicKeyOffset] = readSshString(bytes, offset);
#| offset = publicKeyOffset;
#| const [namespaceBytes, namespaceOffset] = readSshString(bytes, offset);
#| offset = namespaceOffset;
#| const [reservedBytes, reservedOffset] = readSshString(bytes, offset);
#| offset = reservedOffset;
#| const [hashAlgorithmBytes, hashAlgorithmOffset] = readSshString(bytes, offset);
#| offset = hashAlgorithmOffset;
#| const [signatureBlob, endOffset] = readSshString(bytes, offset);
#| offset = endOffset;
#| if (offset !== bytes.length) {
#| throw new Error('unexpected trailing data in SSH SIGNATURE');
#| }
#| let keyOffset = 0;
#| const [publicKeyTypeBytes, publicKeyTypeOffset] = readSshString(publicKeyBlob, keyOffset);
#| keyOffset = publicKeyTypeOffset;
#| const [publicKeyRaw, publicKeyEndOffset] = readSshString(publicKeyBlob, keyOffset);
#| if (publicKeyEndOffset !== publicKeyBlob.length) {
#| throw new Error('unexpected trailing data in SSH signature public key');
#| }
#| let sigOffset = 0;
#| const [signatureTypeBytes, signatureTypeOffset] = readSshString(signatureBlob, sigOffset);
#| sigOffset = signatureTypeOffset;
#| const [rawSignature, signatureEndOffset] = readSshString(signatureBlob, sigOffset);
#| if (signatureEndOffset !== signatureBlob.length) {
#| throw new Error('unexpected trailing data in SSH signature blob');
#| }
#| return {
#| namespace: decodeUtf8(namespaceBytes),
#| reserved: reservedBytes,
#| hashAlgorithm: decodeUtf8(hashAlgorithmBytes).toLowerCase(),
#| publicKeyType: decodeUtf8(publicKeyTypeBytes),
#| publicKeyRaw,
#| signatureType: decodeUtf8(signatureTypeBytes),
#| rawSignature,
#| };
#| };
#| const signGitPayloadSshEd25519 = async (pem, messageText) => {
#| const subtle = ensureSubtle();
#| const privateKey = await importPrivateKey(pem);
#| const publicKeyRaw = await exportPublicKeyRaw(pem);
#| const payloadBytes = encoder.encode(String(messageText ?? ''));
#| const payloadHash = new Uint8Array(
#| await subtle.digest('SHA-512', payloadBytes),
#| );
#| const signedData = concatBytes(
#| encoder.encode('SSHSIG'),
#| sshString('git'),
#| sshString(new Uint8Array()),
#| sshString('sha512'),
#| sshString(payloadHash),
#| );
#| const rawSignature = new Uint8Array(
#| await subtle.sign('Ed25519', privateKey, signedData),
#| );
#| const publicKeyBlob = concatBytes(
#| sshString('ssh-ed25519'),
#| sshString(publicKeyRaw),
#| );
#| const signatureBlob = concatBytes(
#| sshString('ssh-ed25519'),
#| sshString(rawSignature),
#| );
#| const sshSigBlob = concatBytes(
#| encoder.encode('SSHSIG'),
#| uint32be(1),
#| sshString(publicKeyBlob),
#| sshString('git'),
#| sshString(new Uint8Array()),
#| sshString('sha512'),
#| sshString(signatureBlob),
#| );
#| return (
#| '-----BEGIN SSH SIGNATURE-----\n' +
#| wrapBase64(encodeBase64(sshSigBlob)) +
#| '\n-----END SSH SIGNATURE-----\n'
#| );
#| };
#| const verifyGitPayloadSshEd25519 = async (publicKeyText, messageText, signatureText) => {
#| const subtle = ensureSubtle();
#| const parsedSignature = parseSshSignatureArmor(signatureText);
#| if (parsedSignature.namespace !== 'git') {
#| return false;
#| }
#| if (
#| parsedSignature.publicKeyType !== 'ssh-ed25519' ||
#| parsedSignature.signatureType !== 'ssh-ed25519'
#| ) {
#| return false;
#| }
#| const expectedPublicKeyRaw = parsePublicKeyText(publicKeyText);
#| if (!bytesEqual(expectedPublicKeyRaw, parsedSignature.publicKeyRaw)) {
#| return false;
#| }
#| const hashAlgorithm = parsedSignature.hashAlgorithm;
#| const digestName =
#| hashAlgorithm === 'sha512'
#| ? 'SHA-512'
#| : hashAlgorithm === 'sha256'
#| ? 'SHA-256'
#| : '';
#| if (!digestName) {
#| return false;
#| }
#| const payloadBytes = encoder.encode(String(messageText ?? ''));
#| const payloadHash = new Uint8Array(await subtle.digest(digestName, payloadBytes));
#| const signedData = concatBytes(
#| encoder.encode('SSHSIG'),
#| sshString(parsedSignature.namespace),
#| sshString(parsedSignature.reserved),
#| sshString(hashAlgorithm),
#| sshString(payloadHash),
#| );
#| const publicKey = await importPublicKeyRaw(expectedPublicKeyRaw);
#| return subtle.verify(
#| 'Ed25519',
#| publicKey,
#| parsedSignature.rawSignature,
#| signedData,
#| );
#| };
#| return {
#| resolveSshEd25519PublicKey,
#| signGitPayloadSshEd25519,
#| verifyGitPayloadSshEd25519,
#| };
#| })();
#| }
///|
extern "js" fn js_webcrypto_verify_git_payload_ssh_ed25519_impl(
public_key : String,
payload : String,
signature : String,
) -> @js_async.Promise[Bool] =
#| (publicKey, payload, signature) => {
#| return globalThis.__bitGitJsSshSigHelpers.verifyGitPayloadSshEd25519(
#| publicKey,
#| payload,
#| signature,
#| );
#| }
///|
fn js_resolve_git_dir(
fs : LibJsHostFs,
root : String,
) -> String raise @bit.GitError {
let git_path = join_path(root, ".git")
if @bit.RepoFileSystem::is_file(fs, git_path) {
resolve_gitdir(fs, git_path)
} else {
resolve_worktree_git_dir(fs, root)
}
}
///|
fn js_repo_object_format(fs : LibJsHostFs, git_dir : String) -> String {
match
read_config_value(fs, git_dir + "/config", "extensions", "objectformat") {
Some(raw) => {
let normalized = config_strip_quotes(raw).to_lower()
if normalized == "sha256" {
"sha256"
} else {
"sha1"
}
}
None => "sha1"
}
}
///|
fn js_require_sha1_signing_repo(
fs : LibJsHostFs,
git_dir : String,
) -> Unit raise @bit.GitError {
if js_repo_object_format(fs, git_dir) != "sha1" {
raise @bit.GitError::InvalidObject(
"js commit signing only supports sha1 repositories",
)
}
}
///|
priv struct JsPreparedCommit {
git_dir : String
parent : @bit.ObjectId?
tree_id : @bit.ObjectId
entries : Array[IndexEntry]
committer : String
timestamp : Int64
timezone : String
message : String
payload : Bytes
}
///|
fn js_prepare_commit(
fs : LibJsHostFs,
root : String,
message : String,
author : String,
timestamp_secs : Int,
) -> JsPreparedCommit raise @bit.GitError {
let actual_git_dir = js_resolve_git_dir(fs, root)
let parent = resolve_head_commit(fs, actual_git_dir)
let entries = read_index_entries(fs, actual_git_dir)
let tree_id = if entries.length() == 0 {
raise @bit.GitError::InvalidObject("Empty index")
} else {
write_tree_from_index(fs, fs, actual_git_dir, entries, missing_ok=true)
}
let parents = match parent {
Some(id) => [id]
None => []
}
let timestamp = timestamp_secs.to_int64()
let timezone = "+0000"
let commit = @bit.Commit::new(
tree_id, parents, author, timestamp, timezone, author, timestamp, timezone, message,
)
{
git_dir: actual_git_dir,
parent,
tree_id,
entries,
committer: author,
timestamp,
timezone,
message,
payload: @bit.serialize_commit_content(commit),
}
}
///|
fn js_commit_payload_text(payload : Bytes) -> String {
@utf8.decode_lossy(payload[:])
}
///|
async fn js_resolve_ssh_ed25519_public_key_text(
private_key_pem : String,
comment : String,
) -> String raise @bit.GitError {
js_webcrypto_resolve_ssh_ed25519_public_key_impl(private_key_pem, comment).wait() catch {
err =>
raise @bit.GitError::InvalidObject(
"failed to resolve ssh public key from private key: \{err}",
)
}
}
///|
async fn js_sign_git_payload_ssh_ed25519_text(
private_key_pem : String,
payload : String,
) -> String raise @bit.GitError {
js_webcrypto_sign_git_payload_ssh_ed25519_impl(private_key_pem, payload).wait() catch {
err =>
raise @bit.GitError::InvalidObject(
"failed to sign git payload with ssh ed25519: \{err}",
)
}
}
///|
async fn js_verify_git_payload_ssh_ed25519_text(
public_key : String,
payload : String,
signature : String,
) -> Bool raise @bit.GitError {
js_webcrypto_ensure_ssh_ed25519_verify_helpers()
js_webcrypto_verify_git_payload_ssh_ed25519_impl(
public_key, payload, signature,
).wait() catch {
err =>
raise @bit.GitError::InvalidObject(
"failed to verify git payload with ssh ed25519: \{err}",
)
}
}
///|
fn js_require_matching_commit_payload(
expected_payload : String,
actual_payload : Bytes,
) -> Unit raise @bit.GitError {
if expected_payload != js_commit_payload_text(actual_payload) {
raise @bit.GitError::InvalidObject(
"commit payload changed while signing; rebuild payload and retry",
)
}
}
///|
fn js_append_bytes(out : Array[Byte], data : BytesView) -> Unit {
for b in data {
out.push(b)
}
}
///|
fn js_append_utf8(out : Array[Byte], value : String) -> Unit {
js_append_bytes(out, @utf8.encode(value))
}
///|
fn js_normalize_newlines(value : String) -> String {
let out = StringBuilder::new()
let mut saw_cr = false
for c in value {
if saw_cr {
if c == '\n' {
out.write_char('\n')
saw_cr = false
continue
} else {
out.write_char('\n')
saw_cr = false
}
}
if c == '\r' {
saw_cr = true
} else {
out.write_char(c)
}
}
if saw_cr {
out.write_char('\n')
}
out.to_string()
}
///|
fn js_signature_lines(signature : String) -> Array[String] raise @bit.GitError {
let lines : Array[String] = []
let normalized = js_normalize_newlines(signature)
for line_view in normalized.split("\n") {
lines.push(line_view.to_owned())
}
while lines.length() > 0 && lines[lines.length() - 1] == "" {
ignore(lines.pop())
}
if lines.length() == 0 {
raise @bit.GitError::InvalidObject("Empty commit signature")
}
lines
}
///|
fn js_find_commit_header_end(payload : Bytes) -> Int raise @bit.GitError {
let mut i = 0
while i + 1 < payload.length() {
if payload[i] == b'\n' && payload[i + 1] == b'\n' {
return i
}
i += 1
}
raise @bit.GitError::InvalidObject("Malformed commit payload")
}
///|
fn js_bytes_starts_with(
data : Bytes,
start : Int,
end_ : Int,
prefix : String,
) -> Bool {
let prefix_bytes = @utf8.encode(prefix)
if end_ - start < prefix_bytes.length() {
return false
}
for i in 0.. Array[(Int, Int, Bool)] {
let lines : Array[(Int, Int, Bool)] = []
if data.length() == 0 {
return lines
}
let mut i = 0
while i < data.length() {
let start = i
while i < data.length() && data[i] != b'\n' {
i += 1
}
let end_ = i
let has_newline = i < data.length() && data[i] == b'\n'
if has_newline {
i += 1
}
lines.push((start, end_, has_newline))
}
lines
}
///|
fn js_copy_line_range(
data : Bytes,
out : Array[Byte],
line : (Int, Int, Bool),
) -> Unit {
for b in data[line.0:line.1] {
out.push(b)
}
if line.2 {
out.push(b'\n')
}
}
///|
fn js_join_signature_lines(lines : Array[String]) -> String {
if lines.length() == 0 {
return ""
}
let mut out = lines.join("\n")
if !out.has_suffix("\n") {
out = out + "\n"
}
out
}
///|
fn js_extract_commit_signature(data : Bytes) -> (Bytes, String)? {
let lines = js_collect_line_ranges(data)
let mut header_end = lines.length()
let mut sig_start = -1
let mut sig_end = -1
let mut prefix_len = 0
for i in 0.. line.0 && data[line.0] == b' ' {
sig_end += 1
} else {
break
}
}
let signature_lines : Array[String] = []
let first_line = lines[sig_start]
signature_lines.push(
@utf8.decode_lossy(
Bytes::from_array(data[first_line.0 + prefix_len:first_line.1].to_array())[:],
),
)
for i in (sig_start + 1)..= sig_start && i < sig_end {
continue
}
js_copy_line_range(data, out, lines[i])
}
Some((Bytes::from_iter(out.iter()), js_join_signature_lines(signature_lines)))
}
///|
fn js_resolve_revision(
fs : LibJsHostFs,
root : String,
spec : String,
) -> @bit.ObjectId raise @bit.GitError {
let git_dir = js_resolve_git_dir(fs, root)
match rev_parse(fs, git_dir, spec) {
Some(id) => id
None => raise @bit.GitError::InvalidObject("Unknown revision: " + spec)
}
}
///|
fn js_commit_id_or_empty(id : @bit.ObjectId?) -> String {
match id {
Some(value) => value.to_hex()
None => ""
}
}
///|
fn js_merge_status_text(status : MergeStatus) -> String {
match status {
AlreadyUpToDate => "already_up_to_date"
FastForward => "fast_forward"
Merged => "merged"
Conflicted => "conflicted"
}
}
///|
fn js_rebase_status_text(status : RebaseStatus) -> String {
match status {
RebaseStatus::Complete => "complete"
RebaseStatus::Conflict => "conflict"
RebaseStatus::NothingToRebase => "nothing_to_rebase"
}
}
///|
fn js_cherry_pick_status_text(status : CherryPickStatus) -> String {
match status {
CherryPickStatus::Success => "success"
CherryPickStatus::Conflict => "conflict"
}
}
///|
fn js_reset_mode_text(mode : String) -> ResetMode raise @bit.GitError {
match mode {
"soft" => ResetMode::Soft
"mixed" => ResetMode::Mixed
"hard" => ResetMode::Hard
_ => raise @bit.GitError::InvalidObject("Unknown reset mode: " + mode)
}
}
///|
fn js_optional_text(value : String) -> String? {
if value.length() == 0 {
None
} else {
Some(value)
}
}
///|
fn js_convert_merge_result(result : MergeResult) -> JsMergeResult {
{
status: js_merge_status_text(result.status),
commit_id: js_commit_id_or_empty(result.commit_id),
conflicts: result.conflicts,
}
}
///|
fn js_convert_rebase_result(result : RebaseResult) -> JsRebaseResult {
{
status: js_rebase_status_text(result.status),
commit_id: js_commit_id_or_empty(result.commit_id),
conflicts: result.conflicts,
}
}
///|
fn js_convert_cherry_pick_result(
result : CherryPickResult,
) -> JsCherryPickResult {
{
status: js_cherry_pick_status_text(result.status),
commit_id: js_commit_id_or_empty(result.commit_id),
conflicts: result.conflicts,
}
}
///|
fn js_inject_commit_signature(
payload : Bytes,
signature : String,
) -> Bytes raise @bit.GitError {
let lines = js_signature_lines(signature)
let header_end = js_find_commit_header_end(payload)
let out : Array[Byte] = []
js_append_bytes(out, payload[:header_end + 1])
js_append_utf8(out, "gpgsig ")
js_append_utf8(out, lines[0])
out.push(b'\n')
for i in 1.. @bit.ObjectId raise @bit.GitError {
let (commit_id, compressed) = @bit.create_object(
@bit.ObjectType::Commit,
payload,
)
write_object_bytes(fs, prepared.git_dir, commit_id, compressed)
update_head_ref(fs, fs, prepared.git_dir, commit_id)
let committer = prepared.committer
append_head_update_reflogs(
fs,
fs,
prepared.git_dir,
prepared.parent.unwrap_or(@bit.ObjectId::zero()),
commit_id,
if prepared.parent is None {
"commit (initial): " + worktree_commit_subject(prepared.message)
} else {
"commit: " + worktree_commit_subject(prepared.message)
},
committer~,
timestamp=prepared.timestamp,
timezone=prepared.timezone,
)
if prepared.entries.length() > 0 {
write_index_entries_with_tree(
fs,
fs,
prepared.git_dir,
prepared.entries,
prepared.tree_id,
)
}
commit_id
}
///|
pub fn js_git_init(
host_id : Int,
root : String,
default_branch : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => init_repo(fs, root, default_branch~))
}
///|
pub fn js_add_paths(
host_id : Int,
root : String,
paths : Array[String],
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => add_paths(fs, fs, root, paths))
}
///|
pub fn js_status(host_id : Int, root : String) -> Result[Status, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let (entries, cache_tree) = read_index_entries_with_cache_tree(fs, git_dir)
let index_map : Map[String, IndexEntry] = Map([])
for entry in entries {
index_map[entry.path] = entry
}
let files = list_working_files(fs, root)
files.sort()
let visited : Map[String, Bool] = Map([])
let untracked : Array[String] = []
let unstaged_modified : Array[String] = []
for rel_path in files {
visited[rel_path] = true
match index_map.get(rel_path) {
Some(index_entry) => {
let abs_path = join_path(root, rel_path)
let content = (fs as &@bit.RepoFileSystem).read_file(abs_path) catch {
_ => continue
}
let worktree_id = @bit.hash_blob(content)
if worktree_id != index_entry.id {
unstaged_modified.push(rel_path)
}
}
None => untracked.push(rel_path)
}
}
let unstaged_deleted : Array[String] = []
for entry in entries {
if visited.get(entry.path) is Some(_) {
continue
}
let abs_path = join_path(root, entry.path)
if !@bit.RepoFileSystem::is_file(fs, abs_path) {
unstaged_deleted.push(entry.path)
}
}
let staged_index_map : Map[String, IndexEntry] = Map([])
for entry in entries {
staged_index_map[entry.path] = entry
}
let staged_modified : Array[String] = []
let staged_deleted : Array[String] = []
let skip_worktree_paths : Map[String, Bool] = Map([])
collect_staged_changes_from_head(
fs, git_dir, cache_tree, staged_index_map, skip_worktree_paths, staged_modified,
staged_deleted,
)
let staged_added = staged_index_map.keys().to_array()
staged_added.sort()
staged_modified.sort()
staged_deleted.sort()
unstaged_modified.sort()
unstaged_deleted.sort()
untracked.sort()
{
staged_added,
staged_modified,
staged_deleted,
unstaged_modified,
unstaged_deleted,
untracked,
}
})
}
///|
pub fn js_status_porcelain(
host_id : Int,
root : String,
) -> Result[Array[String], String] {
match js_status(host_id, root) {
Ok(status_result) => Ok(status_porcelain_from(status_result))
Err(error) => Err(error)
}
}
///|
pub async fn js_status_text(
host_id : Int,
root : String,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> String raise @bit.GitError {
status_text(fs, root)
})
}
///|
pub fn js_status_text_promise(
host_id : Int,
root : String,
) -> @js_async.Promise[Result[String, String]] {
js_export_promise(async fn() { js_status_text(host_id, root) })
}
///|
pub fn js_commit(
host_id : Int,
root : String,
message : String,
author : String,
timestamp_secs : Int,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let prepared = js_prepare_commit(fs, root, message, author, timestamp_secs)
js_finalize_commit(fs, prepared, prepared.payload).to_hex()
})
}
///|
pub fn js_commit_amend(
host_id : Int,
root : String,
message : String,
author : String,
author_timestamp_secs : Int,
committer : String,
committer_timestamp_secs : Int,
timezone : String,
encoding : String,
author_timezone : String,
committer_timezone : String,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
commit_amend(
fs,
fs,
root,
message,
author,
author_timestamp_secs.to_int64(),
committer=if committer.length() == 0 { author } else { committer },
committer_timestamp=committer_timestamp_secs.to_int64(),
timezone=if timezone.length() == 0 { "+0000" } else { timezone },
encoding=if encoding.length() == 0 { "UTF-8" } else { encoding },
author_timezone=js_optional_text(author_timezone),
committer_timezone=js_optional_text(committer_timezone),
).to_hex()
})
}
///|
pub fn js_build_commit_payload(
host_id : Int,
root : String,
message : String,
author : String,
timestamp_secs : Int,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let prepared = js_prepare_commit(fs, root, message, author, timestamp_secs)
js_commit_payload_text(prepared.payload)
})
}
///|
pub fn js_commit_signed(
host_id : Int,
root : String,
message : String,
author : String,
timestamp_secs : Int,
signature : String,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let prepared = js_prepare_commit(fs, root, message, author, timestamp_secs)
js_require_sha1_signing_repo(fs, prepared.git_dir)
let signed_payload = js_inject_commit_signature(prepared.payload, signature)
js_finalize_commit(fs, prepared, signed_payload).to_hex()
})
}
///|
pub fn js_commit_signed_checked(
host_id : Int,
root : String,
message : String,
author : String,
timestamp_secs : Int,
expected_payload : String,
signature : String,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let prepared = js_prepare_commit(fs, root, message, author, timestamp_secs)
js_require_sha1_signing_repo(fs, prepared.git_dir)
js_require_matching_commit_payload(expected_payload, prepared.payload)
let signed_payload = js_inject_commit_signature(prepared.payload, signature)
js_finalize_commit(fs, prepared, signed_payload).to_hex()
})
}
///|
pub async fn js_resolve_ssh_ed25519_public_key(
private_key_pem : String,
comment : String,
) -> Result[String, String] {
js_wrap_error_async(async fn() -> String raise @bit.GitError {
js_resolve_ssh_ed25519_public_key_text(private_key_pem, comment)
})
}
///|
pub fn js_resolve_ssh_ed25519_public_key_promise(
private_key_pem : String,
comment : String,
) -> @js_async.Promise[Result[String, String]] {
js_export_promise(async fn() {
js_resolve_ssh_ed25519_public_key(private_key_pem, comment)
})
}
///|
pub async fn js_sign_git_payload_ssh_ed25519(
private_key_pem : String,
payload : String,
) -> Result[String, String] {
js_wrap_error_async(async fn() -> String raise @bit.GitError {
js_sign_git_payload_ssh_ed25519_text(private_key_pem, payload)
})
}
///|
pub fn js_sign_git_payload_ssh_ed25519_promise(
private_key_pem : String,
payload : String,
) -> @js_async.Promise[Result[String, String]] {
js_export_promise(async fn() {
js_sign_git_payload_ssh_ed25519(private_key_pem, payload)
})
}
///|
pub async fn js_verify_git_payload_ssh_ed25519(
public_key : String,
payload : String,
signature : String,
) -> Result[Bool, String] {
js_wrap_error_async(async fn() -> Bool raise @bit.GitError {
js_verify_git_payload_ssh_ed25519_text(public_key, payload, signature)
})
}
///|
pub fn js_verify_git_payload_ssh_ed25519_promise(
public_key : String,
payload : String,
signature : String,
) -> @js_async.Promise[Result[Bool, String]] {
js_export_promise(async fn() {
js_verify_git_payload_ssh_ed25519(public_key, payload, signature)
})
}
///|
pub async fn js_verify_commit_ssh_ed25519(
host_id : Int,
root : String,
spec : String,
public_key : String,
) -> Result[Bool, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> Bool raise @bit.GitError {
let git_dir = js_resolve_git_dir(fs, root)
let commit_id = js_resolve_revision(fs, root, spec)
let db = ObjectDb::load(fs, git_dir)
let obj = match db.get(fs, commit_id) {
Some(value) => value
None => raise @bit.GitError::InvalidObject("Unknown revision: " + spec)
}
if obj.obj_type != @bit.ObjectType::Commit {
raise @bit.GitError::InvalidObject("Revision is not a commit: " + spec)
}
let (payload, signature) = match js_extract_commit_signature(obj.data) {
Some(value) => value
None =>
raise @bit.GitError::InvalidObject(
"Commit has no gpgsig header: " + spec,
)
}
js_verify_git_payload_ssh_ed25519_text(
public_key,
js_commit_payload_text(payload),
signature,
)
})
}
///|
pub fn js_verify_commit_ssh_ed25519_promise(
host_id : Int,
root : String,
spec : String,
public_key : String,
) -> @js_async.Promise[Result[Bool, String]] {
js_export_promise(async fn() {
js_verify_commit_ssh_ed25519(host_id, root, spec, public_key)
})
}
///|
pub fn js_log(
host_id : Int,
root : String,
max_count : Int,
) -> Result[Array[JsLogEntry], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let entries = log_head(fs, git_dir, max_count~)
let result : Array[JsLogEntry] = []
for entry in entries {
result.push({
id: entry.id.to_hex(),
author: entry.author,
message: entry.message,
timestamp: entry.timestamp.to_int(),
})
}
result
})
}
///|
pub fn js_log_oneline(
host_id : Int,
root : String,
max_count : Int,
) -> Result[Array[String], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
log_head_oneline(fs, git_dir, max_count~)
})
}
///|
pub fn js_rev_parse(
host_id : Int,
root : String,
spec : String,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
match rev_parse(fs, git_dir, spec) {
Some(id) => id.to_hex()
None => ""
}
})
}
///|
pub fn js_show_ref(
host_id : Int,
root : String,
) -> Result[Array[JsRefInfo], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let refs = show_ref(fs, git_dir)
let result : Array[JsRefInfo] = []
for item in refs {
let (name, id) = item
result.push({ name, id: id.to_hex() })
}
result
})
}
///|
pub fn js_reflog(
host_id : Int,
root : String,
refname : String,
) -> Result[Array[JsReflogEntry], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let entries = read_reflog(fs, git_dir, refname)
let result : Array[JsReflogEntry] = []
for entry in entries {
result.push({
old_id: entry.old_id.to_hex(),
new_id: entry.new_id.to_hex(),
author: entry.author,
email: entry.email,
timestamp: entry.timestamp.to_int(),
timezone: entry.timezone,
message: entry.message,
})
}
result
})
}
///|
pub fn js_gc(host_id : Int, root : String) -> Result[JsGcResult, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let gc_result = gc_repo(fs, fs, root)
let dangling : Array[String] = []
for id in gc_result.dangling {
dangling.push(id.to_hex())
}
match gc_result.pack {
Some(pack) =>
{
pack_id: pack.pack_id.to_hex(),
object_count: pack.object_count,
pack_bytes: pack.pack_bytes,
dangling,
}
None => { pack_id: "", object_count: 0, pack_bytes: 0, dangling }
}
})
}
///|
pub fn js_fsck(host_id : Int, root : String) -> Result[JsFsckResult, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let db = ObjectDb::load(fs, git_dir)
let tips : Array[@bit.ObjectId] = []
for item in show_ref(fs, git_dir) {
let (_, id) = item
tips.push(id)
}
let fsck_result = fsck_connectivity_check(db, fs, tips, collect_extra=true)
let missing : Array[String] = []
for hex in fsck_result.missing.keys() {
missing.push(hex)
}
{
errors: fsck_result.errors,
reachable: fsck_result.reachable.length(),
missing,
root_commits: fsck_result.root_commits,
tag_objects: fsck_result.tag_objects,
}
})
}
///|
pub fn js_worktree_list(
host_id : Int,
root : String,
) -> Result[Array[JsWorktreeInfo], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let worktrees = list_worktrees(fs, root)
let result : Array[JsWorktreeInfo] = []
for info in worktrees {
result.push({
path: info.path,
head_id: match info.head_id {
Some(id) => id.to_hex()
None => ""
},
branch: info.branch.unwrap_or(""),
locked: info.locked,
is_main: info.is_main,
is_bare: info.is_bare,
})
}
result
})
}
///|
pub fn js_merge(
host_id : Int,
root : String,
target : String,
message : String,
author : String,
timestamp_secs : Int,
) -> Result[JsMergeResult, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let target_id = js_resolve_revision(fs, root, target)
let merged = merge(
fs,
fs,
root,
target_id,
message,
author,
timestamp_secs.to_int64(),
)
js_convert_merge_result(merged)
})
}
///|
pub fn js_merge_base(
host_id : Int,
root : String,
a_spec : String,
b_spec : String,
) -> Result[Array[String], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let a = js_resolve_revision(fs, root, a_spec)
let b = js_resolve_revision(fs, root, b_spec)
let db = ObjectDb::load(fs, git_dir)
let bases = merge_base_all(db, fs, a, b)
let result : Array[String] = []
for base in bases {
result.push(base.to_hex())
}
result
})
}
///|
pub fn js_merge_base_is_ancestor(
host_id : Int,
root : String,
ancestor_spec : String,
descendant_spec : String,
) -> Result[Bool, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let ancestor = js_resolve_revision(fs, root, ancestor_spec)
let descendant = js_resolve_revision(fs, root, descendant_spec)
let db = ObjectDb::load(fs, git_dir)
merge_base_is_ancestor(db, fs, ancestor, descendant)
})
}
///|
pub fn js_checkout(
host_id : Int,
root : String,
target : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => ignore(checkout(fs, fs, root, target)))
}
///|
pub fn js_checkout_b(
host_id : Int,
root : String,
branch_name : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
create_branch(fs, fs, root, branch_name)
ignore(checkout(fs, fs, root, branch_name))
})
}
///|
pub fn js_switch_branch(
host_id : Int,
root : String,
name : String,
create : Bool,
checkout_files : Bool,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
switch_branch(fs, fs, root, name, create~, checkout_files~)
})
}
///|
pub fn js_restore_paths(
host_id : Int,
root : String,
paths : Array[String],
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => restore_paths(fs, fs, root, paths))
}
///|
pub fn js_branch_list(
host_id : Int,
root : String,
) -> Result[Array[JsBranchInfo], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let (_, branches) = list_branches(fs, git_dir)
let result : Array[JsBranchInfo] = []
for branch in branches {
result.push({
name: branch.name,
commit_id: branch.id.to_hex(),
is_current: branch.current,
})
}
result
})
}
///|
pub fn js_branch_create(
host_id : Int,
root : String,
name : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => create_branch(fs, fs, root, name))
}
///|
pub fn js_branch_delete(
host_id : Int,
root : String,
name : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => delete_branch(fs, fs, root, name))
}
///|
pub fn js_branch_rename(
host_id : Int,
root : String,
old_name : String,
new_name : String,
force : Bool,
author : String,
email : String,
timestamp_secs : Int,
timezone : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
rename_branch(
fs,
fs,
root,
old_name,
new_name,
force~,
author~,
email~,
timestamp=timestamp_secs.to_int64(),
timezone~,
)
})
}
///|
pub fn js_tag_list(
host_id : Int,
root : String,
) -> Result[Array[String], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => list_tags(fs, js_resolve_git_dir(fs, root)))
}
///|
pub fn js_tag_delete(
host_id : Int,
root : String,
name : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => delete_tag(fs, fs, js_resolve_git_dir(fs, root), name))
}
///|
pub fn js_tag_create_lightweight(
host_id : Int,
root : String,
name : String,
target : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let target_id = js_resolve_revision(fs, root, target)
create_lightweight_tag(fs, fs, git_dir, name, target_id)
})
}
///|
pub fn js_tag_create_annotated(
host_id : Int,
root : String,
name : String,
target : String,
message : String,
tagger : String,
timestamp_secs : Int,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let target_id = js_resolve_revision(fs, root, target)
create_annotated_tag(
fs,
fs,
git_dir,
name,
target_id,
message,
tagger,
timestamp_secs.to_int64(),
)
})
}
///|
pub async fn js_rebase_start(
host_id : Int,
root : String,
upstream : String,
) -> Result[JsRebaseResult, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> JsRebaseResult raise @bit.GitError {
let upstream_id = js_resolve_revision(fs, root, upstream)
js_convert_rebase_result(rebase_start(fs, fs, root, upstream_id))
})
}
///|
pub async fn js_rebase_start_with_onto(
host_id : Int,
root : String,
onto : String,
upstream : String,
) -> Result[JsRebaseResult, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> JsRebaseResult raise @bit.GitError {
let onto_id = js_resolve_revision(fs, root, onto)
let upstream_id = js_resolve_revision(fs, root, upstream)
js_convert_rebase_result(
rebase_start_with_onto(fs, fs, root, onto_id, upstream_id),
)
})
}
///|
pub async fn js_rebase_continue(
host_id : Int,
root : String,
) -> Result[JsRebaseResult, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> JsRebaseResult raise @bit.GitError {
js_convert_rebase_result(rebase_continue(fs, fs, root))
})
}
///|
pub async fn js_rebase_abort(
host_id : Int,
root : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> Unit raise @bit.GitError {
rebase_abort(fs, fs, root)
})
}
///|
pub async fn js_rebase_skip(
host_id : Int,
root : String,
) -> Result[JsRebaseResult, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> JsRebaseResult raise @bit.GitError {
js_convert_rebase_result(rebase_skip(fs, fs, root))
})
}
///|
pub fn js_reset(
host_id : Int,
root : String,
spec : String,
mode : String,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
reset(fs, fs, root, spec, js_reset_mode_text(mode)).to_hex()
})
}
///|
pub fn js_stash_list(
host_id : Int,
root : String,
) -> Result[Array[JsStashEntry], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let git_dir = js_resolve_git_dir(fs, root)
let entries = stash_list(fs, git_dir)
let result : Array[JsStashEntry] = []
for entry in entries {
result.push({ id: entry.id.to_hex(), message: entry.message })
}
result
})
}
///|
pub async fn js_stash_push(
host_id : Int,
root : String,
message : String,
author : String,
timestamp_secs : Int,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> String raise @bit.GitError {
match stash_push(fs, fs, root, message, author, timestamp_secs.to_int64()) {
Some(id) => id.to_hex()
None => ""
}
})
}
///|
pub fn js_stash_push_promise(
host_id : Int,
root : String,
message : String,
author : String,
timestamp_secs : Int,
) -> @js_async.Promise[Result[String, String]] {
js_export_promise(async fn() {
js_stash_push(host_id, root, message, author, timestamp_secs)
})
}
///|
pub fn js_stash_apply(
host_id : Int,
root : String,
index : Int,
drop : Bool,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => stash_apply(fs, fs, root, index, drop))
}
///|
pub fn js_stash_drop(
host_id : Int,
root : String,
index : Int,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => stash_drop(fs, fs, root, index))
}
///|
pub fn js_cherry_pick(
host_id : Int,
root : String,
target : String,
author : String,
timestamp_secs : Int,
no_commit : Bool,
message_suffix : String,
signoff_committer : String,
) -> Result[JsCherryPickResult, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let target_id = js_resolve_revision(fs, root, target)
js_convert_cherry_pick_result(
cherry_pick(
fs,
fs,
root,
target_id,
author,
timestamp_secs.to_int64(),
no_commit~,
message_suffix~,
signoff_committer~,
),
)
})
}
///|
pub async fn js_diff_worktree(
host_id : Int,
root : String,
) -> Result[Array[String], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> Array[String] raise @bit.GitError {
diff_text(diff_worktree(fs, root))
})
}
///|
pub fn js_diff_worktree_promise(
host_id : Int,
root : String,
) -> @js_async.Promise[Result[Array[String], String]] {
js_export_promise(async fn() { js_diff_worktree(host_id, root) })
}
///|
pub async fn js_diff_worktree_stat(
host_id : Int,
root : String,
) -> Result[Array[String], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> Array[String] raise @bit.GitError {
diff_stat(diff_worktree(fs, root))
})
}
///|
pub fn js_diff_worktree_stat_promise(
host_id : Int,
root : String,
) -> @js_async.Promise[Result[Array[String], String]] {
js_export_promise(async fn() { js_diff_worktree_stat(host_id, root) })
}
///|
pub fn js_diff_index(
host_id : Int,
root : String,
) -> Result[Array[String], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => diff_text(diff_index(fs, root)))
}
///|
pub fn js_diff_index_stat(
host_id : Int,
root : String,
) -> Result[Array[String], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => diff_stat(diff_index(fs, root)))
}
///|
pub fn js_list_remotes(
host_id : Int,
root : String,
) -> Result[Array[String], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => list_remotes(fs, js_resolve_git_dir(fs, root)))
}
///|
pub fn js_list_remotes_verbose(
host_id : Int,
root : String,
) -> Result[Array[JsRemoteVerboseEntry], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
let entries = list_remotes_verbose(fs, js_resolve_git_dir(fs, root))
let result : Array[JsRemoteVerboseEntry] = []
for item in entries {
let (name, value) = item
result.push({ name, value })
}
result
})
}
///|
pub fn js_get_remote_url(
host_id : Int,
root : String,
name : String,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
get_remote_url(fs, js_resolve_git_dir(fs, root), name).unwrap_or("")
})
}
///|
pub fn js_sparse_checkout_init(
host_id : Int,
root : String,
cone : Bool,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => sparse_checkout_init(fs, fs, root, cone~))
}
///|
pub fn js_sparse_checkout_set(
host_id : Int,
root : String,
patterns : Array[String],
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => sparse_checkout_set(fs, fs, root, patterns))
}
///|
pub fn js_sparse_checkout_add(
host_id : Int,
root : String,
patterns : Array[String],
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => sparse_checkout_add(fs, fs, root, patterns))
}
///|
pub fn js_sparse_checkout_disable(
host_id : Int,
root : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => sparse_checkout_disable(fs, fs, root))
}
///|
pub fn js_sparse_checkout_reapply(
host_id : Int,
root : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => sparse_checkout_reapply(fs, fs, root))
}
///|
pub fn js_sparse_checkout_enabled(
host_id : Int,
root : String,
) -> Result[Bool, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
is_sparse_checkout_enabled(fs, js_resolve_git_dir(fs, root))
})
}
///|
pub fn js_sparse_checkout_cone_enabled(
host_id : Int,
root : String,
) -> Result[Bool, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
is_sparse_checkout_cone_enabled(fs, js_resolve_git_dir(fs, root))
})
}
///|
pub fn js_sparse_checkout_patterns(
host_id : Int,
root : String,
) -> Result[Array[String], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => read_sparse_patterns(fs, js_resolve_git_dir(fs, root)))
}
///|
pub fn js_sparse_checkout_display_patterns(
host_id : Int,
root : String,
) -> Result[Array[String], String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => {
read_sparse_display_patterns(fs, js_resolve_git_dir(fs, root))
})
}
///|
fn js_normalize_push_refname(refname : String) -> String raise @bit.GitError {
let normalized = normalize_repo_path(refname) catch {
_ => raise @bit.GitError::InvalidObject("invalid refname: " + refname)
}
if normalized.has_prefix("refs/") {
normalized
} else {
"refs/heads/" + normalized
}
}
///|
fn js_default_push_refname(
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> String raise @bit.GitError {
match read_head_ref(rfs, git_dir) {
HeadRef::Branch(name) => "refs/heads/" + name
HeadRef::Detached(_) =>
raise @bit.GitError::InvalidObject(
"refname is required when HEAD is detached",
)
}
}
///|
fn js_head_refname(
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> String raise @bit.GitError {
match read_head_ref(rfs, git_dir) {
HeadRef::Branch(name) => "refs/heads/" + name
HeadRef::Detached(_) => "HEAD"
}
}
///|
fn js_find_remote_ref(
refs : Array[(@bit.ObjectId, String)],
refname : String,
) -> @bit.ObjectId {
for item in refs {
let (id, current_refname) = item
if current_refname == refname {
return id
}
}
@bit.ObjectId::zero()
}
///|
fn js_is_ancestor(
rfs : &@bit.RepoFileSystem,
git_dir : String,
base : @bit.ObjectId,
target : @bit.ObjectId,
) -> Bool raise @bit.GitError {
let db = ObjectDb::load(rfs, git_dir)
let mut current = target
while true {
if current == base {
return true
}
let obj = db.get(rfs, current)
match obj {
None => return false
Some(value) => {
if value.obj_type != @bit.ObjectType::Commit {
return false
}
let info = @bit.parse_commit(value.data)
if info.parents.length() == 0 {
return false
}
current = info.parents[0]
}
}
}
false
}
///|
fn js_remote_tracking_refname(refname : String) -> String raise @bit.GitError {
let normalized = normalize_repo_path(refname) catch {
_ => raise @bit.GitError::InvalidObject("invalid refname: " + refname)
}
if normalized.has_prefix("refs/heads/") {
let name = String::unsafe_substring(
normalized,
start=11,
end=normalized.length(),
)
"refs/remotes/origin/" + name
} else if normalized == "HEAD" {
"refs/remotes/origin/HEAD"
} else if normalized.has_prefix("refs/") {
"refs/remotes/origin/" + normalized
} else {
raise @bit.GitError::InvalidObject("invalid refname: " + refname)
}
}
///|
fn js_format_fetch_head_description(
source_refname : String?,
remote_url : String,
) -> String {
match source_refname {
Some(refname) if refname.has_prefix("refs/heads/") => {
let branch = String::unsafe_substring(
refname,
start=11,
end=refname.length(),
)
"branch '" + branch + "' of " + remote_url
}
Some(refname) if refname.has_prefix("refs/tags/") => {
let tag = String::unsafe_substring(
refname,
start=10,
end=refname.length(),
)
"tag '" + tag + "' of " + remote_url
}
Some(refname) => refname + " of " + remote_url
None => remote_url
}
}
///|
fn js_select_fetch_target(
refs : Array[(@bit.ObjectId, String)],
symrefs : Map[String, String],
refspec : String,
) -> (String, @bit.ObjectId)? raise @bit.GitError {
if refspec.length() == 0 {
match @protocol.select_default_ref(refs, symrefs) {
Some((refname, id)) => return Some((refname, id))
None => return None
}
}
let candidates : Array[String] = []
let normalized = normalize_repo_path(refspec) catch {
_ => raise @bit.GitError::InvalidObject("invalid refspec: " + refspec)
}
if normalized == "HEAD" {
candidates.push("HEAD")
match symrefs.get("HEAD") {
Some(target) => candidates.push(target)
None => ()
}
} else if normalized.has_prefix("refs/") {
candidates.push(normalized)
} else {
candidates.push("refs/heads/" + normalized)
candidates.push("refs/tags/" + normalized)
candidates.push(normalized)
}
for candidate in candidates {
for item in refs {
let (id, refname) = item
if refname == candidate {
return Some((refname, id))
}
}
}
None
}
///|
fn js_branch_name_from_refname(refname : String) -> String? {
if refname.has_prefix("refs/heads/") {
Some(String::unsafe_substring(refname, start=11, end=refname.length()))
} else {
None
}
}
///|
fn js_default_clone_branch(
refs : Array[(@bit.ObjectId, String)],
symrefs : Map[String, String],
) -> String {
match symrefs.get("HEAD") {
Some(target) =>
match js_branch_name_from_refname(target) {
Some(branch) => return branch
None => ()
}
None => ()
}
for item in refs {
let (_, refname) = item
match js_branch_name_from_refname(refname) {
Some(branch) => return branch
None => ()
}
}
"main"
}
///|
fn js_write_clone_remote_config(
fs : LibJsHostFs,
git_dir : String,
remote_url : String,
) -> Unit raise @bit.GitError {
set_config_key(fs, fs, git_dir, "remote", "origin", "url", remote_url)
set_config_key(
fs, fs, git_dir, "remote", "origin", "fetch", "+refs/heads/*:refs/remotes/origin/*",
)
}
///|
fn js_set_clone_branch_tracking(
fs : LibJsHostFs,
git_dir : String,
branch : String,
) -> Unit raise @bit.GitError {
set_config_key(fs, fs, git_dir, "branch", branch, "remote", "origin")
set_config_key(
fs,
fs,
git_dir,
"branch",
branch,
"merge",
"refs/heads/" + branch,
)
}
///|
fn js_write_clone_remote_tracking_ref(
fs : LibJsHostFs,
git_dir : String,
refname : String,
commit_id : @bit.ObjectId,
) -> Unit raise @bit.GitError {
match js_branch_name_from_refname(refname) {
Some(branch) => {
let remote_ref = "refs/remotes/origin/" + branch
let remote_ref_path = join_path(git_dir, remote_ref)
@bit.FileSystem::mkdir_p(fs, rebase_parent_dir(remote_ref_path))
@bit.FileSystem::write_string(
fs,
remote_ref_path,
commit_id.to_hex() + "\n",
)
let remote_head_path = join_path(git_dir, "refs/remotes/origin/HEAD")
@bit.FileSystem::mkdir_p(fs, rebase_parent_dir(remote_head_path))
@bit.FileSystem::write_string(
fs,
remote_head_path,
"ref: " + remote_ref + "\n",
)
js_set_clone_branch_tracking(fs, git_dir, branch)
}
None => ()
}
}
///|
pub async fn js_clone_remote(
host_id : Int,
root : String,
remote_url : String,
transport_id : Int,
prefer_v2 : Bool,
depth : Int,
) -> Result[JsCloneResult, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> JsCloneResult raise @bit.GitError {
let (refs, _caps, _version, symrefs) = @bitnative.discover_upload_refs_with_http(
remote_url,
prefer_v2,
async fn(
url : String,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_get(transport_id, url, headers)
},
async fn(
url : String,
body : Bytes,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_post(transport_id, url, body, headers)
},
)
let wants = @protocol.select_default_wants(refs, symrefs)
let default_ref = @protocol.select_default_ref(refs, symrefs)
match default_ref {
None => {
let default_branch = js_default_clone_branch(refs, symrefs)
init_repo(fs, root, default_branch~)
let git_dir = js_resolve_git_dir(fs, root)
js_write_clone_remote_config(fs, git_dir, remote_url)
js_set_clone_branch_tracking(fs, git_dir, default_branch)
}
Some((refname, commit_id)) => {
if wants.length() > 0 {
let pack = @bitnative.fetch_pack_with_http(
remote_url,
wants,
prefer_v2,
depth,
@protocol.FilterSpec::NoFilter,
async fn(
url : String,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_get(transport_id, url, headers)
},
async fn(
url : String,
body : Bytes,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_post(transport_id, url, body, headers)
},
)
let objects = @pack.parse_packfile(pack)
let git_dir = join_path(root, ".git")
@bit.FileSystem::mkdir_p(fs, join_path(git_dir, "objects/pack"))
@pack.write_packfile_with_index(fs, git_dir, pack, objects)
let store = @bit.ObjectStore::from_pack(objects)
@bit.materialize_clone_to_fs(
store, commit_id, refname, remote_url, fs, root, fs,
)
} else {
init_repo(
fs,
root,
default_branch=js_default_clone_branch(refs, symrefs),
)
}
let git_dir = js_resolve_git_dir(fs, root)
js_write_clone_remote_config(fs, git_dir, remote_url)
js_write_clone_remote_tracking_ref(fs, git_dir, refname, commit_id)
}
}
let git_dir = js_resolve_git_dir(fs, root)
let commit_id = resolve_head_commit(fs, git_dir)
{
status: if commit_id is Some(_) {
"cloned"
} else {
"empty"
},
commit_id: js_commit_id_or_empty(commit_id),
refname: js_head_refname(fs, git_dir),
}
})
}
///|
pub fn js_clone_remote_promise(
host_id : Int,
root : String,
remote_url : String,
transport_id : Int,
prefer_v2 : Bool,
depth : Int,
) -> @js_async.Promise[Result[JsCloneResult, String]] {
js_export_promise(async fn() {
js_clone_remote(host_id, root, remote_url, transport_id, prefer_v2, depth)
})
}
///|
pub async fn js_fetch_remote(
host_id : Int,
root : String,
remote_url : String,
transport_id : Int,
refspec : String,
prefer_v2 : Bool,
depth : Int,
) -> Result[JsFetchResult, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> JsFetchResult raise @bit.GitError {
let git_dir = js_resolve_git_dir(fs, root)
let (refs, _caps, _version, symrefs) = @bitnative.discover_upload_refs_with_http(
remote_url,
prefer_v2,
async fn(
url : String,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_get(transport_id, url, headers)
},
async fn(
url : String,
body : Bytes,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_post(transport_id, url, body, headers)
},
)
match js_select_fetch_target(refs, symrefs, refspec) {
None => { status: "empty", commit_id: "", refname: "", remote_ref: "" }
Some((refname, commit_id)) => {
let pack = @bitnative.fetch_pack_with_http(
remote_url,
[commit_id],
prefer_v2,
depth,
@protocol.FilterSpec::NoFilter,
async fn(
url : String,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_get(transport_id, url, headers)
},
async fn(
url : String,
body : Bytes,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_post(transport_id, url, body, headers)
},
)
let objects = @pack.parse_packfile(pack)
@bit.FileSystem::mkdir_p(fs, join_path(git_dir, "objects/pack"))
@pack.write_packfile_with_index(fs, git_dir, pack, objects)
let remote_ref = js_remote_tracking_refname(refname)
let remote_ref_path = join_path(git_dir, remote_ref)
@bit.FileSystem::mkdir_p(fs, rebase_parent_dir(remote_ref_path))
@bit.FileSystem::write_string(
fs,
remote_ref_path,
commit_id.to_hex() + "\n",
)
@bit.FileSystem::write_string(
fs,
join_path(git_dir, "FETCH_HEAD"),
commit_id.to_hex() +
"\t\t" +
js_format_fetch_head_description(Some(refname), remote_url) +
"\n",
)
{
status: "fetched",
commit_id: commit_id.to_hex(),
refname,
remote_ref,
}
}
}
})
}
///|
pub fn js_fetch_remote_promise(
host_id : Int,
root : String,
remote_url : String,
transport_id : Int,
refspec : String,
prefer_v2 : Bool,
depth : Int,
) -> @js_async.Promise[Result[JsFetchResult, String]] {
js_export_promise(async fn() {
js_fetch_remote(
host_id, root, remote_url, transport_id, refspec, prefer_v2, depth,
)
})
}
///|
pub async fn js_push_remote(
host_id : Int,
root : String,
remote_url : String,
transport_id : Int,
refname : String,
force : Bool,
) -> Result[String, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error_async(async fn() -> String raise @bit.GitError {
let git_dir = js_resolve_git_dir(fs, root)
let head = resolve_head_commit(fs, git_dir)
guard head is Some(head_id) else {
raise @bit.GitError::InvalidObject("HEAD not found")
}
let target_ref = if refname.length() == 0 {
js_default_push_refname(fs, git_dir)
} else {
js_normalize_push_refname(refname)
}
let db = ObjectDb::load(fs, git_dir)
let objects = collect_reachable_objects(db, fs, head_id)
let pack = @pack.create_packfile(objects)
let remote = @protocol.Remote::new(remote_url)
let (refs, _caps) = @protocol.discover_refs_with_http(remote, async fn(
url : String,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_get(transport_id, url, headers)
})
let old_id = js_find_remote_ref(refs, target_ref)
if !force && old_id != @bit.ObjectId::zero() {
if !js_is_ancestor(fs, git_dir, old_id, head_id) {
raise @bit.GitError::InvalidObject(
"Updates were rejected because the tip of your current branch is behind its remote counterpart. Use force to override.",
)
}
}
let req = @protocol.PushRequest::new(old_id, head_id, target_ref, pack)
@protocol.push_with_http(
remote,
req,
async fn(
url : String,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_get(transport_id, url, headers)
},
async fn(
url : String,
body : Bytes,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
js_transport_post(transport_id, url, body, headers)
},
)
})
}
///|
pub fn js_push_remote_promise(
host_id : Int,
root : String,
remote_url : String,
transport_id : Int,
refname : String,
force : Bool,
) -> @js_async.Promise[Result[String, String]] {
js_export_promise(async fn() {
js_push_remote(host_id, root, remote_url, transport_id, refname, force)
})
}
///|
pub fn js_rm_paths(
host_id : Int,
root : String,
paths : Array[String],
cached : Bool,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => rm_paths(fs, fs, root, paths, cached~))
}
///|
pub fn js_mv_path(
host_id : Int,
root : String,
source : String,
dest : String,
) -> Result[Unit, String] {
let fs = js_make_host_fs(host_id)
js_wrap_error(() => mv_path(fs, fs, root, source, dest))
}