impl GitCloner {
#[must_use]
pub fn new(cache_dir: PathBuf) -> Self {
Self {
cache_dir,
progress: Arc::new(Mutex::new(CloneProgress {
stage: "Initializing".to_string(),
current: 0,
total: 0,
bytes_transferred: 0,
})),
timeout: Duration::from_secs(300), max_size_bytes: 500_000_000, }
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn with_max_size(mut self, max_size_bytes: u64) -> Self {
self.max_size_bytes = max_size_bytes;
self
}
pub async fn get_progress(&self) -> CloneProgress {
self.progress.lock().await.clone()
}
pub async fn clone_or_update(&self, url: &str) -> Result<ClonedRepo, CloneError> {
let _parsed_url = self.parse_github_url(url)?;
let cache_key = self.compute_cache_key(url);
let target_path = self.cache_dir.join(&cache_key);
if target_path.exists() {
if let Ok(repo) = Repository::open(&target_path) {
if self.is_cache_fresh(&repo).await.unwrap_or(false) {
return Ok(ClonedRepo {
path: target_path,
url: url.to_string(),
cached: true,
});
}
if self.update_repository(&repo).await.is_ok() {
return Ok(ClonedRepo {
path: target_path,
url: url.to_string(),
cached: true,
});
}
}
let _ = tokio::fs::remove_dir_all(&target_path).await;
}
tokio::fs::create_dir_all(&self.cache_dir)
.await
.map_err(CloneError::IoError)?;
let progress = self.progress.clone();
let url_clone = url.to_string();
let target_clone = target_path.clone();
let clone_future = tokio::task::spawn_blocking(move || {
let temp_cloner = GitCloner {
cache_dir: PathBuf::new(), progress,
timeout: Duration::from_secs(300),
max_size_bytes: 0,
};
temp_cloner.clone_shallow(&url_clone, &target_clone)
});
let _start = Instant::now();
let result = tokio::select! {
result = clone_future => {
match result {
Ok(Ok(())) => Ok(ClonedRepo {
path: target_path.clone(),
url: url.to_string(),
cached: false,
}),
Ok(Err(e)) => Err(e),
Err(e) => Err(CloneError::GitError(git2::Error::from_str(&e.to_string()))),
}
}
() = tokio::time::sleep(self.timeout) => {
Err(CloneError::Timeout)
}
};
if result.is_err() && target_path.exists() {
let _ = tokio::fs::remove_dir_all(&target_path).await;
}
result
}
fn clone_shallow(&self, url: &str, target: &Path) -> Result<(), CloneError> {
let progress = self.progress.clone();
let mut fetch_opts = FetchOptions::new();
fetch_opts.depth(1);
let mut callbacks = RemoteCallbacks::new();
callbacks.transfer_progress(move |stats: Progress| {
let progress_update = CloneProgress {
stage: "Receiving objects".to_string(),
current: stats.received_objects(),
total: stats.total_objects(),
bytes_transferred: stats.received_bytes(),
};
if let Ok(mut p) = progress.try_lock() {
*p = progress_update;
}
true
});
fetch_opts.remote_callbacks(callbacks);
let mut builder = RepoBuilder::new();
builder.fetch_options(fetch_opts);
builder.clone(url, target).map_err(CloneError::GitError)?;
Ok(())
}
async fn update_repository(&self, repo: &Repository) -> Result<()> {
let mut remote = repo.find_remote("origin")?;
let mut fetch_opts = FetchOptions::new();
fetch_opts.download_tags(git2::AutotagOption::All);
remote.fetch(&["HEAD"], Some(&mut fetch_opts), None)?;
let fetch_head = repo.find_reference("FETCH_HEAD")?;
let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
let analysis = repo.merge_analysis(&[&fetch_commit])?;
if analysis.0.is_fast_forward() {
let refname = "refs/heads/master"; let mut reference = repo.find_reference(refname)?;
reference.set_target(fetch_commit.id(), "Fast-forward")?;
repo.set_head(refname)?;
repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?;
}
Ok(())
}
async fn is_cache_fresh(&self, _repo: &Repository) -> Result<bool> {
if let Ok(metadata) = tokio::fs::metadata(_repo.path().join(".git")).await {
if let Ok(modified) = metadata.modified() {
if let Ok(elapsed) = modified.elapsed() {
return Ok(elapsed < Duration::from_secs(3600));
}
}
}
Ok(false)
}
}