Skip to content

Commit

Permalink
WIP: Start migrating fetch_repository to gix
Browse files Browse the repository at this point in the history
I'm not sure this is worth it. gix provides ... a much more complex API.
While a project goal is speed, a personal value is simplicity. git2 is a
much simpler C dependency, and ... it "just works". gix is a monster of
a crate with _tons_ of flexibility that I don't want.
  • Loading branch information
Notgnoshi committed Nov 10, 2024
1 parent b56bdee commit 8417263
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 9 deletions.
60 changes: 60 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,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"
Expand Down
2 changes: 1 addition & 1 deletion herostratus/src/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub fn check(args: &CheckArgs) -> eyre::Result<()> {

pub fn check_all(args: &CheckAllArgs, config: &Config, data_dir: &Path) -> eyre::Result<()> {
if !args.no_fetch {
crate::commands::fetch_all(&args.into(), config, data_dir)?
let _fetched = crate::commands::fetch_all(&args.into(), config, data_dir)?;
}

tracing::info!("Checking repositories ...");
Expand Down
7 changes: 4 additions & 3 deletions herostratus/src/commands/fetch_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ use crate::cli::FetchAllArgs;
use crate::config::Config;
use crate::git::clone::{clone_repository, fetch_remote, find_local_repository};

pub fn fetch_all(_args: &FetchAllArgs, config: &Config, _data_dir: &Path) -> eyre::Result<()> {
pub fn fetch_all(_args: &FetchAllArgs, config: &Config, _data_dir: &Path) -> eyre::Result<usize> {
tracing::info!("Fetching repositories ...");
let start = Instant::now();
let mut fetched_commits = 0;
for (name, config) in config.repositories.iter() {
let span = tracing::debug_span!("fetch", name = name);
let _enter = span.enter();
Expand All @@ -27,7 +28,7 @@ pub fn fetch_all(_args: &FetchAllArgs, config: &Config, _data_dir: &Path) -> eyr
};

if !skip_fetch {
fetch_remote(config, &repo)?
fetched_commits += fetch_remote(config, &repo)?
}
}
tracing::info!(
Expand All @@ -36,5 +37,5 @@ pub fn fetch_all(_args: &FetchAllArgs, config: &Config, _data_dir: &Path) -> eyr
start.elapsed()
);

Ok(())
Ok(fetched_commits)
}
129 changes: 125 additions & 4 deletions herostratus/src/git/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@ use std::path::{Path, PathBuf};

use eyre::WrapErr;

pub fn find_local_repository(path: &Path) -> eyre::Result<git2::Repository> {
pub fn find_local_repository<P: AsRef<Path> + std::fmt::Debug>(
path: P,
) -> eyre::Result<git2::Repository> {
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<P: AsRef<Path> + std::fmt::Debug>(
path: P,
) -> eyre::Result<gix::Repository> {
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://[email protected]/path.git => path.git
// [email protected]:Notgnoshi/herostratus.git => Notgnoshi/herostratus.git
// https://example.com/foo => foo
Expand Down Expand Up @@ -118,8 +129,13 @@ fn fetch_options(config: &crate::config::RepositoryConfig) -> git2::FetchOptions
pub fn fetch_remote(
config: &crate::config::RepositoryConfig,
repo: &git2::Repository,
) -> eyre::Result<()> {
) -> eyre::Result<usize> {
let mut remote = repo.find_remote("origin")?;
assert_eq!(
remote.url().unwrap_or_default(),
config.url.as_str(),
"RepositoryConfig and remote 'origin' don't agree on the URL"
);
let reference_name = config.branch.as_deref().unwrap_or("HEAD");
// If this is the first time this reference is being fetched, fetch it like
// git fetch origin branch:branch
Expand All @@ -145,11 +161,11 @@ pub fn fetch_remote(
let reference = repo.resolve_reference_from_short_name(reference_name)?;
let after = reference.peel_to_commit()?;

let mut new_commits: usize = 0;
if before.is_some() && before.as_ref().unwrap().id() == after.id() {
tracing::debug!("... done. No new commits");
} else {
let commits = crate::git::rev::walk(after.id(), repo)?;
let mut new_commits: usize = 0;
for commit_id in commits {
if let Some(before) = &before {
if commit_id? == before.id() {
Expand All @@ -161,7 +177,61 @@ pub fn fetch_remote(
tracing::debug!("... done. {new_commits} new commits");
}

Ok(())
Ok(new_commits)
}

pub fn fetch_remote2(
config: &crate::config::RepositoryConfig,
repo: &gix::Repository,
) -> eyre::Result<usize> {
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
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(
Expand Down Expand Up @@ -227,8 +297,18 @@ pub fn clone_repository(

#[cfg(test)]
mod tests {
use herostratus_tests::fixtures;

use super::*;

#[test]
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 = [
Expand All @@ -249,4 +329,45 @@ mod tests {
assert_eq!(expected, actual);
}
}

#[test]
#[ignore = "Slow; performs fetch"]
// TODO: This may or may not require SSH in the CI runner
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
}

#[test]
#[ignore = "Slow; performs fetch"]
// TODO: This may or may not require SSH in the CI runner
fn required_fetch_remote2() {
// this is a workspace crate, so its tests are *not* run from the workspace root, rather
// from the workspace member.
let this = find_local_repository2("..").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("new".to_string()),
url: remote
.url(gix::remote::Direction::Fetch)
.unwrap()
.to_string(),
..Default::default()
};
fetch_remote2(&config, &this).unwrap(); // assert that fetching doesn't fail
}
}

0 comments on commit 8417263

Please sign in to comment.