diff options
| author | seth <[email protected]> | 2024-10-10 07:26:29 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-10-10 07:26:29 -0400 |
| commit | c61b701095a1f6b52777d317275f34687e57ee3e (patch) | |
| tree | e5b1e80fa8040b593aeebb9daf83bcc9d78c6e81 /crates/git-tracker | |
| parent | 4e1fab6ff1d17a0fff50e1a87af8c0bbe8c075f9 (diff) | |
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
Diffstat (limited to 'crates/git-tracker')
| -rw-r--r-- | crates/git-tracker/Cargo.toml | 4 | ||||
| -rw-r--r-- | crates/git-tracker/src/lib.rs | 239 | ||||
| -rw-r--r-- | crates/git-tracker/src/managed_repository.rs | 95 | ||||
| -rw-r--r-- | crates/git-tracker/src/tracker.rs | 109 |
4 files changed, 209 insertions, 238 deletions
diff --git a/crates/git-tracker/Cargo.toml b/crates/git-tracker/Cargo.toml index 3027769..14af5d0 100644 --- a/crates/git-tracker/Cargo.toml +++ b/crates/git-tracker/Cargo.toml @@ -9,9 +9,9 @@ repository.workspace = true publish = false [dependencies] -git2 = { version = "0.19.0", default-features = false, features = ["https"] } +git2 = { version = "0.19", default-features = false, features = ["https"] } log.workspace = true -thiserror = "1.0.64" +thiserror = "1.0" [lints] workspace = true 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<String>, -) -> Result<Vec<(String, bool)>, 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<Repository, Error> { + 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<bool, Error> { + trace!( + "Checking for commit {commit} in {}", + reference.name().unwrap_or("<branch>") + ); + 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<Item = &'a String>, + commit_sha: &str, + ) -> Result<Vec<(&'a String, bool)>, 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) + } } diff --git a/crates/git-tracker/src/managed_repository.rs b/crates/git-tracker/src/managed_repository.rs deleted file mode 100644 index 0a41bd0..0000000 --- a/crates/git-tracker/src/managed_repository.rs +++ /dev/null @@ -1,95 +0,0 @@ -use git2::{AutotagOption, FetchOptions, RemoteCallbacks, RemoteUpdateFlags, Repository}; -use log::{debug, info, trace, warn}; -use std::{io::Write, path::PathBuf}; - -// much of this is shamelessly lifted from -// https://github.com/rust-lang/git2-rs/blob/9a5c9706ff578c936be644dd1e8fe155bdc4d129/examples/pull.rs - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("libgit2 error")] - Git(#[from] git2::Error), -} - -pub struct ManagedRepository { - pub path: PathBuf, - pub tracked_branches: Vec<String>, - pub upstream_remote_url: String, - pub upstream_remote_name: String, -} - -impl ManagedRepository { - /// basic set of options for fetching from remotes - fn fetch_options<'a>() -> FetchOptions<'a> { - let mut remote_callbacks = RemoteCallbacks::new(); - remote_callbacks.transfer_progress(|progress| { - if progress.received_objects() == progress.total_objects() { - trace!( - "Resolving deltas {}/{}\r", - progress.indexed_deltas(), - progress.total_deltas() - ); - } else { - trace!( - "Received {}/{} objects ({}) in {} bytes\r", - progress.received_objects(), - progress.total_objects(), - progress.indexed_objects(), - progress.received_bytes() - ); - } - std::io::stdout().flush().ok(); - true - }); - - let mut fetch_opts = FetchOptions::new(); - fetch_opts.remote_callbacks(remote_callbacks); - - fetch_opts - } - - /// Update the given branches in the [`repository`] using the nixpkgs remote - fn update_branches_in(&self, repository: &Repository) -> Result<(), Error> { - let mut remote = repository.find_remote(&self.upstream_remote_name)?; - // download all the refs - remote.download(&self.tracked_branches, Some(&mut Self::fetch_options()))?; - remote.disconnect()?; - // and (hopefully) update what they refer to for later - remote.update_tips( - None, - RemoteUpdateFlags::UPDATE_FETCHHEAD, - AutotagOption::Auto, - None, - )?; - - Ok(()) - } - - /// Fetch the repository or update it if it exists - /// - /// # Errors - /// Will return [`Err`] if the repository cannot be opened, cloned, or updated - pub fn fetch_or_update(&self) -> Result<(), Error> { - // Open our repository or clone it if it doesn't exist - let repository = if self.path.exists() { - Repository::open(self.path.as_path())? - } else { - warn!( - "Couldn't find repository at {}! Cloning a fresh one from {}", - self.path.display(), - self.upstream_remote_url - ); - Repository::clone(&self.upstream_remote_url, self.path.as_path())?; - info!("Finished cloning to {}", self.path.display()); - - // bail early as we already have a fresh copy - return Ok(()); - }; - - debug!("Updating repository at {}", self.path.display()); - self.update_branches_in(&repository)?; - debug!("Finished updating!"); - - Ok(()) - } -} diff --git a/crates/git-tracker/src/tracker.rs b/crates/git-tracker/src/tracker.rs deleted file mode 100644 index e6a3f54..0000000 --- a/crates/git-tracker/src/tracker.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::path::Path; - -use git2::{Branch, BranchType, Commit, ErrorCode, Oid, Reference, Repository}; - -/// Helper struct for tracking Git objects -pub struct Tracker { - repository: Repository, -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("libgit2 error")] - Git(#[from] git2::Error), - #[error("Repository path not found at `{0}`")] - RepositoryPathNotFound(String), -} - -impl Tracker { - /// Create a new [`Tracker`] using the repository at [`path`] - /// - /// # Errors - /// - /// Will return [`Err`] if the repository can not be opened - pub fn from_path(path: &str) -> Result<Self, Error> { - let repository_path = Path::new(path); - if repository_path.exists() { - let repository = Repository::open(repository_path)?; - Ok(Self { repository }) - } else { - Err(Error::RepositoryPathNotFound(path.to_string())) - } - } - - /// Finds a branch of name [`name`] - /// - /// # Errors - /// - /// Will return [`Err`] if the branch cannot be found locally - pub fn branch_by_name(&self, name: &str) -> Result<Branch, Error> { - Ok(self.repository.find_branch(name, BranchType::Remote)?) - } - - /// Finds a commit with a SHA match [`sha`] - /// - /// # Errors - /// - /// Will return [`Err`] if [`sha`] cannot be converted an [`Oid`] or - /// a commit matching it cannot be found - pub fn commit_by_sha(&self, sha: &str) -> Result<Commit, Error> { - let oid = Oid::from_str(sha)?; - let commit = self.repository.find_commit(oid)?; - - Ok(commit) - } - - /// Check if [`Reference`] [`ref`] contains [`Commit`] [`commit`] - /// - /// # Errors - /// - /// Will return [`Err`] if the reference cannot be resolved to a commit or the descendants - /// of the reference cannot be resolved - pub fn ref_contains_commit( - &self, - reference: &Reference, - commit: &Commit, - ) -> Result<bool, Error> { - 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 - let is_head = head.id() == commit.id(); - - let has_commit = self - .repository - .graph_descendant_of(head.id(), commit.id())?; - - Ok(is_head || has_commit) - } - - /// Check if a [`Branch`] named [`branch_name`] has a commit with the SHA [`commit_sha`] - /// - /// # Errors - /// - /// Will return [`Err`] if the commit SHA cannot be resolved to an object id, the branch name cannot - /// be resolved to a branch, or the descendants of the resolved branch cannot be resolved - pub fn branch_contains_sha(&self, branch_name: &str, commit_sha: &str) -> Result<bool, Error> { - let commit = match self.commit_by_sha(commit_sha) { - Ok(commit) => commit, - Err(why) => { - // NOTE: we assume commits not found are just not in the branch *yet*, not an error - // this is because github decides to report merge commit shas for unmerged PRs...yeah - if let Error::Git(git_error) = &why { - if git_error.code() == ErrorCode::NotFound { - return Ok(false); - } - } - - return Err(why); - } - }; - - let branch = self.branch_by_name(branch_name)?; - let has_pr = self.ref_contains_commit(&branch.into_reference(), &commit)?; - - Ok(has_pr) - } -} |
