git download method

This commit is contained in:
Stefano Fontana 2024-12-14 17:59:55 +01:00
parent ec3bc7dd7e
commit 3436f825f2
6 changed files with 233 additions and 7 deletions

100
Cargo.lock generated
View File

@ -129,6 +129,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "auth-git2"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3810b5af212b013fe7302b12d86616c6c39a48e18f2e4b812a5a9e5710213791"
dependencies = [
"dirs",
"git2",
"terminal-prompt",
]
[[package]]
name = "autocfg"
version = "1.4.0"
@ -863,6 +874,21 @@ dependencies = [
"weezl",
]
[[package]]
name = "git2"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 2.6.0",
"libc",
"libgit2-sys",
"log",
"openssl-probe",
"openssl-sys",
"url",
]
[[package]]
name = "half"
version = "2.4.1"
@ -1322,6 +1348,20 @@ dependencies = [
"cc",
]
[[package]]
name = "libgit2-sys"
version = "0.17.0+1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
dependencies = [
"cc",
"libc",
"libssh2-sys",
"libz-sys",
"openssl-sys",
"pkg-config",
]
[[package]]
name = "libm"
version = "0.2.8"
@ -1339,6 +1379,32 @@ dependencies = [
"redox_syscall",
]
[[package]]
name = "libssh2-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@ -2505,6 +2571,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "terminal-prompt"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572818b3472910acbd5dff46a3413715c18e934b071ab2ba464a7b2c2af16376"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "terminal_size"
version = "0.4.0"
@ -2854,11 +2930,13 @@ dependencies = [
name = "typst-kit"
version = "0.12.0"
dependencies = [
"auth-git2",
"dirs",
"ecow",
"env_proxy",
"flate2",
"fontdb",
"git2",
"native-tls",
"once_cell",
"openssl",
@ -3422,6 +3500,22 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
@ -3431,6 +3525,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"

View File

@ -34,6 +34,7 @@ typst-timing = { path = "crates/typst-timing", version = "0.12.0" }
typst-utils = { path = "crates/typst-utils", version = "0.12.0" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "b07d156" }
auth-git2 = "0.5.5"
arrayvec = "0.7.4"
az = "1.2"
base64 = "0.22"
@ -58,6 +59,7 @@ env_proxy = "0.4"
flate2 = "1"
fontdb = { version = "0.21", default-features = false }
fs_extra = "1.3"
git2 = "0.19.0"
hayagriva = "0.8"
heck = "0.5"
hypher = "0.1.4"

View File

@ -25,6 +25,8 @@ native-tls = { workspace = true, optional = true }
once_cell = { workspace = true }
tar = { workspace = true, optional = true }
ureq = { workspace = true, optional = true }
git2 = { workspace = true, optional = true }
auth-git2 = { workspace = true, optional = true }
# Explicitly depend on OpenSSL if applicable, so that we can add the
# `openssl/vendored` feature to it if `vendor-openssl` is enabled.
@ -40,7 +42,7 @@ fonts = ["dep:fontdb", "fontdb/memmap", "fontdb/fontconfig"]
# Add generic downloading utilities
downloads = ["downloads_http", "downloads_git"]
downloads_http = ["dep:env_proxy", "dep:native-tls", "dep:ureq", "dep:openssl"]
downloads_git = []
downloads_git = ["git2", "auth-git2"]
# Add package downloading utilities, implies `downloads`
packages = ["downloads", "dep:dirs", "dep:flate2", "dep:tar"]

View File

@ -0,0 +1,117 @@
use std::cell::{RefCell, RefMut};
use std::collections::VecDeque;
use std::fmt::Debug;
use std::fs;
use std::io::{self, ErrorKind, Read};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use auth_git2::GitAuthenticator;
use crate::package_downloads::{
DownloadState, PackageDownloader, Progress, DEFAULT_NAMESPACE,
};
use ecow::{eco_format, EcoString};
use native_tls::{Certificate, TlsConnector};
use once_cell::sync::OnceCell;
use typst_library::diag::{bail, PackageError, PackageResult};
use typst_syntax::package::{PackageInfo, PackageSpec, VersionlessPackageSpec};
use git2::{AutotagOption, FetchOptions, Progress as GitProgress, RemoteCallbacks};
use git2::build::{CheckoutBuilder, RepoBuilder};
use typst_library::html::tag::form;
#[derive(Debug)]
pub struct GitDownloader;
impl GitDownloader {
pub fn new() -> Self {
Self {}
}
pub fn download_with_progress(
&self,
repo: &str,
tag: &str,
dest: &Path,
progress: &mut dyn Progress,
) -> Result<(), EcoString> {
progress.print_start();
eprintln!("{} {} {}", repo, tag, dest.display());
let state = DownloadState{
content_len: None,
total_downloaded: 0,
bytes_per_second: VecDeque::from(vec![0; 5]),
start_time: Instant::now(),
};
let auth = GitAuthenticator::default();
let git_config = git2::Config::open_default().map_err(|err| {EcoString::from(format!("{:?}", err))})?;
let mut fetch_options = FetchOptions::new();
let mut remote_callbacks = RemoteCallbacks::new();
remote_callbacks.credentials(auth.credentials(&git_config));
fetch_options
.remote_callbacks(remote_callbacks);
let repo = RepoBuilder::new()
.fetch_options(fetch_options)
.clone(repo, dest).map_err(|err| {EcoString::from(format!("{:?}", err))})?;
let (object, reference) = repo
.revparse_ext(tag).map_err(|err| {EcoString::from(format!("{:?}", err))})?;
repo.checkout_tree(&object, None).map_err(|err| {EcoString::from(format!("{:?}", err))})?;
match reference {
// gref is an actual reference like branches or tags
Some(gref) => repo.set_head(gref.name().unwrap()),
// this is a commit, not a reference
None => repo.set_head_detached(object.id()),
}.map_err(|err| {EcoString::from(format!("{:?}", err))})?;
progress.print_finish(&state);
Ok(())
}
fn parse_namespace(ns: &str, name: &str) -> Result<String, EcoString> {
let mut parts = ns.splitn(2, ":");
let schema = parts.next().ok_or_else(|| {
eco_format!("expected schema in {}", ns)
})?;
let repo = parts.next().ok_or_else(|| {
eco_format!("invalid package repo {}", ns)
})?;
if !schema.eq("git") {
Err(eco_format!("invalid schema in {}", ns))?
}
Ok(format!("{}/{}.git", repo, name))
}
}
impl PackageDownloader for GitDownloader {
fn download_index(
&self,
spec: &VersionlessPackageSpec,
) -> Result<Vec<PackageInfo>, EcoString> {
//todo ls-remote
todo!()
}
fn download(
&self,
spec: &PackageSpec,
package_dir: &Path,
progress: &mut dyn Progress,
) -> PackageResult<()> {
let repo = Self::parse_namespace(spec.namespace.as_str(), spec.name.as_str()).map_err(|x| PackageError::Other(Some(x)))?;
let tag = format!("v{}", spec.version);
self.download_with_progress(repo.as_str(), tag.as_str(), package_dir, progress).map_err(|x| PackageError::Other(Some(x)))
}
}

View File

@ -89,7 +89,7 @@ impl HttpDownloader {
/// Download binary data from the given url.
#[allow(clippy::result_large_err)]
pub fn download(&self, url: &str) -> Result<ureq::Response, ureq::Error> {
pub fn perform_download(&self, url: &str) -> Result<ureq::Response, ureq::Error> {
let mut builder = ureq::AgentBuilder::new();
let mut tls = TlsConnector::builder();
@ -125,7 +125,7 @@ impl HttpDownloader {
progress: &mut dyn Progress,
) -> Result<Vec<u8>, ureq::Error> {
progress.print_start();
let response = self.download(url)?;
let response = self.perform_download(url)?;
Ok(RemoteReader::from_response(response, progress).download()?)
}
@ -145,6 +145,10 @@ impl HttpDownloader {
eco_format!("invalid package namespace in {}", ns)
})?;
if !schema.eq("http") && !schema.eq("https") {
Err(eco_format!("invalid schema in {}", ns))?
}
Ok((format!("{}://{}", schema, registry), ns.to_string()))
}
}
@ -264,7 +268,7 @@ impl PackageDownloader for HttpDownloader {
fn download_index(&self, spec: &VersionlessPackageSpec) -> Result<Vec<PackageInfo>, EcoString> {
let (registry, namespace) = Self::parse_namespace(spec.namespace.as_str())?;
let url = format!("{registry}/{namespace}/index.json");
match self.download(&url) {
match self.perform_download(&url) {
Ok(response) => response.into_json().map_err(|err| {
eco_format!("failed to parse package index: {err}")
}),

View File

@ -5,6 +5,7 @@ use std::time::Instant;
use ecow::{eco_format, EcoString};
use typst_library::diag::{PackageError, PackageResult};
use typst_syntax::package::{PackageInfo, PackageSpec, VersionlessPackageSpec};
use crate::package_downloads::git::GitDownloader;
/// The public namespace in the default Typst registry.
pub const DEFAULT_NAMESPACE: &str = "preview";
@ -76,12 +77,12 @@ impl Downloader {
}
fn make_git_downloader(_cert: Option<PathBuf>) -> Option<Box<dyn PackageDownloader>>{
#[cfg(not(feature = "downloads_http"))]
#[cfg(not(feature = "downloads_git"))]
{ None }
#[cfg(feature = "downloads_http")]
#[cfg(feature = "downloads_git")]
{
None
Some(Box::new(GitDownloader::new()))
}
}