From c61b701095a1f6b52777d317275f34687e57ee3e Mon Sep 17 00:00:00 2001 From: seth Date: Thu, 10 Oct 2024 07:26:29 -0400 Subject: rise once again my glorious creation (#51) * git-tracker: update tips after fetch + cleanup * nix: use nix-filter * ci: cleanup * crates: update * git-tracker: don't spam log while transferring * nix: fix static package eval --- crates/git-tracker/src/lib.rs | 239 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 207 insertions(+), 32 deletions(-) (limited to 'crates/git-tracker/src/lib.rs') diff --git a/crates/git-tracker/src/lib.rs b/crates/git-tracker/src/lib.rs index 0bf17dc..e2feb32 100644 --- a/crates/git-tracker/src/lib.rs +++ b/crates/git-tracker/src/lib.rs @@ -1,35 +1,210 @@ -//! A library that helps you track commits and branches in a Git repository -use log::trace; - -mod managed_repository; -mod tracker; -pub use managed_repository::ManagedRepository; -pub use tracker::Tracker; - -/// Collect the status of the commit SHA [`commit_sha`] in each of the nixpkgs -/// branches in [`branches`], using the repository at path [`repository_path`] -/// -/// NOTE: `branches` should contain the full ref (i.e., `origin/main`) -/// -/// # Errors -/// -/// Will return [`Err`] if we can't start tracking a repository at the given path, -/// or if we can't determine if the branch has given commit -pub fn collect_statuses_in( - repository_path: &str, - commit_sha: &str, - branches: &Vec, -) -> Result, tracker::Error> { - // start tracking nixpkgs - let tracker = Tracker::from_path(repository_path)?; - - // check to see what branches it's in - let mut status_results = Vec::new(); - for branch_name in branches { - trace!("Checking for commit in {branch_name}"); - let has_pr = tracker.branch_contains_sha(branch_name, commit_sha)?; - status_results.push((branch_name.to_string(), has_pr)); +//! Library for helping you track commits and branches in a Git repository +use std::path::PathBuf; + +use git2::{ + BranchType, FetchOptions, FetchPrune, Oid, Reference, RemoteCallbacks, RemoteUpdateFlags, + Repository, +}; +use log::{debug, info, trace}; + +/// Used when logging Git transfer progress +const INCREMENT_TO_LOG: i32 = 5; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("libgit2 error")] + Git(#[from] git2::Error), + #[error("i/o error")] + IOError(#[from] std::io::Error), +} + +/// Helper struct for tracking Git objects +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct TrackedRepository { + /// Path to repository + path: PathBuf, + /// URL of the Git remote + remote_url: String, + /// Name of the remote referring to `remote_url` + remote_name: String, +} + +impl TrackedRepository { + #[must_use] + pub fn new(path: PathBuf, remote_url: String, remote_name: String) -> Self { + Self { + path, + remote_url, + remote_name, + } + } + + /// Open a [`Repository`] + /// + /// # Errors + /// + /// Will return [`Err`] if the repository cannot be opened + pub fn open(&self) -> Result { + trace!("Opening repository at {}", self.path.display()); + Ok(Repository::open(&self.path)?) + } + + /// Clone a (small) fresh copy of your repository + /// + /// # Errors + /// + /// Will return [`Err`] if the path, repository, or remote cannot be created + pub fn clone_repository(&self) -> Result<(), Error> { + // Setup a bare repository to save space + info!("Creating repository at {}", self.path.display()); + std::fs::create_dir_all(&self.path)?; + let repository = Repository::init_bare(&self.path)?; + + debug!("Adding remote {} for {}", self.remote_name, self.remote_url,); + repository.remote(&self.remote_name, &self.remote_url)?; + self.fetch()?; + + Ok(()) + } + + #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] + fn fetch_options<'a>() -> FetchOptions<'a> { + let mut rc = RemoteCallbacks::new(); + + // Log transfer progress + let mut current_percentage = 1; + rc.transfer_progress(move |stats| { + if stats.received_objects() == stats.total_objects() { + // HACK: Avoid dividing by zero + // I have no idea how this can ever be zero but ok + let total_deltas = stats.total_deltas(); + if total_deltas == 0 { + return true; + } + + let percentage = + (stats.indexed_deltas() as f32 / stats.total_deltas() as f32 * 100.0) as i32; + if percentage != current_percentage && percentage % INCREMENT_TO_LOG == 0 { + info!( + "Resolving deltas {}/{}\r", + stats.indexed_deltas(), + stats.total_deltas() + ); + current_percentage = percentage; + } + } else if stats.total_objects() > 0 { + let percentage = + (stats.received_objects() as f32 / stats.total_objects() as f32 * 100.0) as i32; + if percentage != current_percentage && percentage % INCREMENT_TO_LOG == 0 { + info!( + "Received {}/{} objects ({}) in {} bytes\r", + stats.received_objects(), + stats.total_objects(), + stats.indexed_objects(), + stats.received_bytes() + ); + current_percentage = percentage; + } + } + + true + }); + + // Log ref updates + rc.update_tips(|refname, orig_oid, new_oid| { + if orig_oid.is_zero() { + info!("[new] {:20} {}", new_oid, refname); + } else { + info!("[updated] {:10}..{:10} {}", orig_oid, new_oid, refname); + } + true + }); + + let mut fetch_options = FetchOptions::new(); + // Make sure we prune on fetch + fetch_options.prune(FetchPrune::On).remote_callbacks(rc); + + fetch_options + } + + /// Fetch the tracked remote + /// + /// # Errors + /// + /// Will return [`Err`] if the repository cannot be opened, the remote cannot be found, the + /// refs cannot be fetched, or the tips of the refs cannot be updated + pub fn fetch(&self) -> Result<(), Error> { + let repository = self.open()?; + + let mut remote = repository.find_remote(&self.remote_name)?; + + info!("Fetching repository"); + remote.download(&[] as &[&str], Some(&mut Self::fetch_options()))?; + remote.disconnect()?; + + debug!("Updating tips"); + remote.update_tips( + None, + RemoteUpdateFlags::UPDATE_FETCHHEAD, + git2::AutotagOption::None, + None, + )?; + + Ok(()) } - Ok(status_results) + /// Check if a [`Reference`] contains a given Git object + /// + /// # Errors + /// + /// Will return [`Err`] if the repository cannot be opened, HEAD cannot be resolved, or the + /// relation between commits cannot be resolved + pub fn ref_contains_object(&self, reference: &Reference, commit: Oid) -> Result { + trace!( + "Checking for commit {commit} in {}", + reference.name().unwrap_or("") + ); + let repository = self.open()?; + let head = reference.peel_to_commit()?; + + // NOTE: we have to check this as `Repository::graph_descendant_of()` (like the name says) + // only finds *descendants* of it's parent commit, and will not tell us if the parent commit + // *is* the child commit. i have no idea why i didn't think of this, but that's why this + // comment is here now + if head.id() == commit { + return Ok(true); + } + + let has_commit = repository.graph_descendant_of(head.id(), commit)?; + + Ok(has_commit) + } + + /// Check if multiple [`Reference`]s contain a commit SHA + /// + /// # Errors + /// + /// Will return [`Err`] if an [`Oid`] could not be resolved from the commit SHA + /// or when it can't be determined if a reference contains a commit + pub fn branches_contain_sha<'a>( + &self, + branch_names: impl IntoIterator, + commit_sha: &str, + ) -> Result, Error> { + let repository = self.open()?; + let commit = Oid::from_str(commit_sha)?; + + let mut results = vec![]; + for branch_name in branch_names { + let branch = repository.find_branch( + &format!("{}/{branch_name}", self.remote_name), + BranchType::Remote, + )?; + + let has_commit = self.ref_contains_object(&branch.into_reference(), commit)?; + results.push((branch_name, has_commit)); + } + + Ok(results) + } } -- cgit v1.2.3