summaryrefslogtreecommitdiff
path: root/crates/git-tracker
diff options
context:
space:
mode:
authorseth <[email protected]>2024-10-10 07:26:29 -0400
committerGitHub <[email protected]>2024-10-10 07:26:29 -0400
commitc61b701095a1f6b52777d317275f34687e57ee3e (patch)
treee5b1e80fa8040b593aeebb9daf83bcc9d78c6e81 /crates/git-tracker
parent4e1fab6ff1d17a0fff50e1a87af8c0bbe8c075f9 (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.toml4
-rw-r--r--crates/git-tracker/src/lib.rs239
-rw-r--r--crates/git-tracker/src/managed_repository.rs95
-rw-r--r--crates/git-tracker/src/tracker.rs109
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)
- }
-}