From 6cf8007b05ae4635b60a50ac0c84c7726b583e91 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Sat, 30 Nov 2024 14:00:54 -0600 Subject: [PATCH] WIP: Improve fetch tests, switch fetching to gix --- Cargo.lock | 60 ++++++++++++ Cargo.toml | 2 +- herostratus/src/git/clone.rs | 178 ++++++++++++++++++++++++++++++++++- 3 files changed, 238 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c7ba04..eefc177 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -525,6 +525,7 @@ dependencies = [ "gix-path", "gix-pathspec", "gix-prompt", + "gix-protocol", "gix-ref", "gix-refspec", "gix-revision", @@ -1001,12 +1002,26 @@ dependencies = [ "gix-hashtable", "gix-object", "gix-path", + "gix-tempfile", "memmap2", + "parking_lot", "smallvec", "thiserror", "uluru", ] +[[package]] +name = "gix-packetline" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f14a110eb16e27b4ebdae4ca8b389df3ad637d3020077e6b606b1d078745b65" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror", +] + [[package]] name = "gix-packetline-blocking" version = "0.18.0" @@ -1060,6 +1075,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gix-protocol" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4ebf25f20ac6055728eaa80951acf2cf83948a64af6565b98e7d42b1ab6691" +dependencies = [ + "bstr", + "gix-credentials", + "gix-date", + "gix-features", + "gix-hash", + "gix-transport", + "gix-utils", + "maybe-async", + "thiserror", + "winnow", +] + [[package]] name = "gix-quote" version = "0.4.13" @@ -1214,6 +1247,22 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "gix-transport" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c485a345f41b8c0256cb86e95ed93e0692d203fd6c769b0433f7352c13608ad" +dependencies = [ + "bstr", + "gix-command", + "gix-features", + "gix-packetline", + "gix-quote", + "gix-sec", + "gix-url", + "thiserror", +] + [[package]] name = "gix-traverse" version = "0.42.0" @@ -1595,6 +1644,17 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "memchr" version = "2.7.4" diff --git a/Cargo.toml b/Cargo.toml index 3eda5f8..ddc7a26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ ctor = "0.2.7" directories = "5.0.1" eyre = "0.6.12" git2 = "0.19" -gix = { version = "0.67.0", features = ["credentials", "mailmap", "revision", "blob-diff", "tracing", "index", "tree-editor", "excludes"] } +gix = { version = "0.67.0", features = ["credentials", "mailmap", "revision", "blob-diff", "tracing", "index", "tree-editor", "excludes", "blocking-network-client"] } inventory = "0.3.15" lazy_static = "1.4.0" predicates = "3.1.0" diff --git a/herostratus/src/git/clone.rs b/herostratus/src/git/clone.rs index 10a6acf..b659adf 100644 --- a/herostratus/src/git/clone.rs +++ b/herostratus/src/git/clone.rs @@ -2,13 +2,24 @@ use std::path::{Path, PathBuf}; use eyre::WrapErr; -pub fn find_local_repository(path: &Path) -> eyre::Result { +pub fn find_local_repository + std::fmt::Debug>( + path: P, +) -> eyre::Result { tracing::debug!("Searching local path {path:?} for a Git repository"); let repo = git2::Repository::discover(path)?; tracing::debug!("Found local git repository at {:?}", repo.path()); Ok(repo) } +pub fn find_local_repository2 + std::fmt::Debug>( + path: P, +) -> eyre::Result { + tracing::debug!("Searching local path {path:?} for a Git repository"); + let repo = gix::discover(path)?; + tracing::debug!("Found local Git repository at {:?}", repo.path()); + Ok(repo) +} + // ssh://git@example.com/path.git => path.git // git@github.com:Notgnoshi/herostratus.git => Notgnoshi/herostratus.git // https://example.com/foo => foo @@ -169,6 +180,62 @@ pub fn fetch_remote( Ok(new_commits) } +pub fn fetch_remote2( + config: &crate::config::RepositoryConfig, + repo: &gix::Repository, +) -> eyre::Result { + let remote = repo.find_remote("origin")?; + assert_eq!( + remote + .url(gix::remote::Direction::Fetch) + .unwrap() + .to_bstring(), + config.url.as_str(), + "RepositoryConfig and remote 'origin' don't agree on the URL" + ); + let reference_name = config.branch.as_deref().unwrap_or("HEAD"); + // TODO: Fetch just the specified branch, not all of them + // // If this is the first time this reference is being fetched, fetch it like + // // git fetch origin branch:branch + // // which updates the local branch to match the remote + // let fetch_reference_name = if reference_name != "HEAD" { + // format!("{reference_name}:{reference_name}") + // } else { + // reference_name.to_string() + // }; + + let before = repo.rev_parse_single(reference_name).ok(); + + // TODO: Need to figure out how to override HTTPS/SSH default details. Is this actually + // important? In what scenarios is a user going to try to run Herostratus on a repository they + // can't 'git clone'? + let connection = remote.connect(gix::remote::Direction::Fetch)?; + let options = gix::remote::ref_map::Options::default(); + let prepare = connection.prepare_fetch(gix::progress::Discard, options)?; + let interrupt = std::sync::atomic::AtomicBool::new(false); + let _outcome = prepare.receive(gix::progress::Discard, &interrupt)?; + + let after = repo.rev_parse_single(reference_name)?; + + let mut new_commits: usize = 0; + if before.is_some() && before.as_ref().unwrap().detach() == after.detach() { + tracing::debug!("... done. No new commits"); + } else { + let commits = crate::git::rev::walk2(after.detach(), repo)?; + for commit_id in commits { + if let Some(before) = &before { + if commit_id? == before.detach() { + break; + } + } + new_commits += 1; + } + tracing::debug!("... done. {new_commits} new commits"); + } + + Ok(new_commits) +} + pub fn clone_repository( config: &crate::config::RepositoryConfig, force: bool, @@ -232,8 +299,19 @@ pub fn clone_repository( #[cfg(test)] mod tests { + use herostratus_tests::fixtures; + use super::*; + #[test] + #[cfg_attr(feature = "ci", ignore = "Requires .gitconfig not available in CI")] + fn test_find_local_repository() { + let temp_repo = fixtures::repository::simplest2().unwrap(); + + let repo = find_local_repository2(temp_repo.tempdir.path()).unwrap(); + assert_eq!(repo.path(), temp_repo.repo.path()); + } + #[test] fn test_parse_path_from_url() { let url_paths = [ @@ -254,4 +332,102 @@ mod tests { assert_eq!(expected, actual); } } + + #[test] + #[cfg_attr(not(feature = "ci"), ignore = "Slow; performs fetch")] + fn required_fetch_remote() { + // this is a workspace crate, so its tests are *not* run from the workspace root, rather + // from the workspace member. + let this = find_local_repository("..").unwrap(); + // NOTE: There's awkard duplication between the RepositoryConfig, and the repository + // remotes. This is because the same RepositoryConfig is used to clone the repository as is + // used to fetch. + let remote = this.find_remote("origin").unwrap(); + let config = crate::config::RepositoryConfig { + branch: Some("main".to_string()), + url: remote.url().unwrap().to_string(), + ..Default::default() + }; + fetch_remote(&config, &this).unwrap(); // assert that fetching doesn't fail + + let this = find_local_repository2("..").unwrap(); + fetch_remote2(&config, &this).unwrap(); // assert that fetching doesn't fail + } + + #[test] + #[cfg_attr(not(feature = "ci"), ignore = "Slow; performs fetch")] + fn test_fetch_remote_branch_doesnt_exist() { + let this = find_local_repository("..").unwrap(); + let remote = this.find_remote("origin").unwrap(); + let config = crate::config::RepositoryConfig { + branch: Some("THIS_BRANCH_DOESNT_EXIST".to_string()), + url: remote.url().unwrap().to_string(), + ..Default::default() + }; + let result = fetch_remote(&config, &this); + assert!(result.is_err()); + + let this = find_local_repository2("..").unwrap(); + let result = fetch_remote2(&config, &this); + assert!(result.is_err()); + } + + #[test] + fn test_fast_fetch_single_ref() { + let (upstream, downstream) = fixtures::repository::upstream_downstream().unwrap(); + // TODO + } + + #[test] + fn test_fetch_remote_branch_creates_or_updates_local_branch() { + let (upstream, downstream) = fixtures::repository::upstream_downstream().unwrap(); + // TODO + } + + #[test] + fn test_fetch_remote_branch_that_doesnt_exist_locally() { + let (upstream, downstream) = fixtures::repository::upstream_downstream().unwrap(); + let upstream_commit = + fixtures::repository::add_empty_commit(&upstream.repo, "First upstream commit") + .unwrap(); + fixtures::repository::add_empty_commit(&downstream.repo, "First downstream commit") + .unwrap(); + + let remote = downstream.repo.find_remote("origin").unwrap(); + let config = crate::config::RepositoryConfig { + branch: Some("HEAD".to_string()), + url: remote.url().unwrap().to_string(), + ..Default::default() + }; + let result = downstream.repo.find_commit(upstream_commit.id()); + assert!(result.is_err()); // can't find the upstream commit until you fetch + fetch_remote(&config, &downstream.repo).unwrap(); + let result = downstream.repo.find_commit(upstream_commit.id()); + assert!(result.is_ok()); + } + + #[test] + fn test_fetch_remote_branch_that_doesnt_exist_locally2() { + let (upstream, downstream) = fixtures::repository::upstream_downstream2().unwrap(); + let upstream_commit = + fixtures::repository::add_empty_commit2(&upstream.repo, "First upstream commit") + .unwrap(); + fixtures::repository::add_empty_commit2(&downstream.repo, "First downstream commit") + .unwrap(); + + let remote = downstream.repo.find_remote("origin").unwrap(); + let config = crate::config::RepositoryConfig { + branch: Some("HEAD".to_string()), + url: remote + .url(gix::remote::Direction::Fetch) + .unwrap() + .to_string(), + ..Default::default() + }; + let result = downstream.repo.find_commit(upstream_commit.id()); + assert!(result.is_err()); // can't find the upstream commit until you fetch + fetch_remote2(&config, &downstream.repo).unwrap(); + let result = downstream.repo.find_commit(upstream_commit.id()); + assert!(result.is_ok()); + } }