Skip to content

Commit

Permalink
Implement Brioche.gitRef() for locked git refs (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
kylewlacy authored Sep 23, 2024
1 parent 1f09391 commit e30ba51
Show file tree
Hide file tree
Showing 9 changed files with 1,596 additions and 151 deletions.
1,292 changes: 1,181 additions & 111 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/brioche-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ urlencoding = "2.1.3"
walkdir = "2.5.0"
petgraph = "0.6.5"
wax = { version = "0.6.0", default-features = false }
gix = { version = "0.66.0", features = ["blocking-network-client", "blocking-http-transport-reqwest"] }

[dev-dependencies]
assert_matches = "1.5.0"
Expand Down
123 changes: 123 additions & 0 deletions crates/brioche-core/src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,126 @@ pub async fn download(

Ok(blob_hash)
}

pub async fn fetch_git_commit_for_ref(repository: &url::Url, ref_: &str) -> anyhow::Result<String> {
let (tx, rx) = tokio::sync::oneshot::channel::<anyhow::Result<_>>();

// gix uses a blocking client, so spawn a separate thread to fetch
std::thread::spawn({
let repository: gix::Url = repository
.as_str()
.try_into()
.with_context(|| format!("failed to parse git repository URL: {repository}"))?;
move || {
// Connect to the repository by URL
let transport = gix::protocol::transport::connect(repository, Default::default());
let mut transport = match transport {
Ok(transport) => transport,
Err(error) => {
let _ = tx.send(Err(error.into()));
return;
}
};

// Perform a handshake to get the remote's capabilities.
// Authentication is disabled
let empty_auth = |_| Ok(None);
let outcome = gix::protocol::fetch::handshake(
&mut transport,
empty_auth,
vec![],
&mut gix::progress::Discard,
);
let outcome = match outcome {
Ok(outcome) => outcome,
Err(error) => {
let _ = gix::protocol::indicate_end_of_interaction(&mut transport, false);
let _ = tx.send(Err(error.into()));
return;
}
};

let refs = match outcome.refs {
Some(refs) => {
// The handshake will sometimes return the refs directly,
// depending on protocol version. If that happens, we're
// done
refs
}
None => {
// Fetch the refs
let refs = gix::protocol::ls_refs(
&mut transport,
&outcome.capabilities,
|_, _, _| Ok(gix::protocol::ls_refs::Action::Continue),
&mut gix::progress::Discard,
false,
);
match refs {
Ok(refs) => refs,
Err(error) => {
let _ =
gix::protocol::indicate_end_of_interaction(&mut transport, false);
let _ = tx.send(Err(error.into()));
return;
}
}
}
};

// End the interaction with the remote
let _ = gix::protocol::indicate_end_of_interaction(&mut transport, false);

let _ = tx.send(Ok(refs));
}
});

let remote_refs = rx.await?;
let remote_refs = match remote_refs {
Ok(remote_refs) => remote_refs,
Err(error) => {
anyhow::bail!("{error}");
}
};

// Find the ref that matches the requested ref name
let object_id = remote_refs
.iter()
.find_map(|remote_ref| {
let (name, object) = match remote_ref {
gix::protocol::handshake::Ref::Peeled {
full_ref_name,
object,
..
} => (full_ref_name, object),
gix::protocol::handshake::Ref::Direct {
full_ref_name,
object,
} => (full_ref_name, object),
gix::protocol::handshake::Ref::Symbolic {
full_ref_name,
object,
..
} => (full_ref_name, object),
gix::protocol::handshake::Ref::Unborn { .. } => {
return None;
}
};

if let Some(tag_name) = name.strip_prefix(b"refs/tags/") {
if tag_name == ref_.as_bytes() {
return Some(object);
}
} else if let Some(head_name) = name.strip_prefix(b"refs/heads/") {
if head_name == ref_.as_bytes() {
return Some(object);
}
}

None
})
.with_context(|| format!("git ref '{ref_}' not found in repo {repository}"))?;

let commit = object_id.to_string();
Ok(commit)
}
95 changes: 71 additions & 24 deletions crates/brioche-core/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ use std::{
sync::Arc,
};

use analyze::{StaticOutput, StaticOutputKind, StaticQuery};
use analyze::{GitRefOptions, StaticOutput, StaticOutputKind, StaticQuery};
use anyhow::Context as _;
use futures::{StreamExt as _, TryStreamExt as _};
use relative_path::{PathExt as _, RelativePath, RelativePathBuf};
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};

use crate::recipe::{Artifact, RecipeHash};
use crate::recipe::Artifact;

use super::{vfs::FileId, Brioche};

Expand Down Expand Up @@ -314,7 +314,7 @@ impl Projects {
&self,
specifier: &super::script::specifier::BriocheModuleSpecifier,
static_: &StaticQuery,
) -> anyhow::Result<Option<RecipeHash>> {
) -> anyhow::Result<Option<StaticOutput>> {
let projects = self
.inner
.read()
Expand Down Expand Up @@ -450,7 +450,7 @@ impl ProjectsInner {
&self,
specifier: &super::script::specifier::BriocheModuleSpecifier,
static_: &StaticQuery,
) -> anyhow::Result<Option<RecipeHash>> {
) -> anyhow::Result<Option<StaticOutput>> {
let path = match specifier {
super::script::specifier::BriocheModuleSpecifier::File { path } => path,
_ => {
Expand Down Expand Up @@ -485,8 +485,7 @@ impl ProjectsInner {
let Some(Some(output)) = statics.get(static_) else {
return Ok(None);
};
let recipe_hash = static_.output_recipe_hash(output)?;
Ok(Some(recipe_hash))
Ok(Some(output.clone()))
}
}

Expand Down Expand Up @@ -994,7 +993,9 @@ async fn fetch_project_from_registry(
};

let recipe_hash = static_.output_recipe_hash(output)?;
statics_recipes.insert(recipe_hash);
if let Some(recipe_hash) = recipe_hash {
statics_recipes.insert(recipe_hash);
}
}

crate::registry::fetch_recipes_deep(brioche, statics_recipes).await?;
Expand All @@ -1012,6 +1013,7 @@ async fn fetch_project_from_registry(

match static_ {
StaticQuery::Include(include) => {
let recipe_hash = recipe_hash.context("no recipe hash for include static")?;
let recipe = crate::recipe::get_recipe(brioche, recipe_hash).await?;
let artifact: crate::recipe::Artifact = recipe.try_into().map_err(|_| {
anyhow::anyhow!("included static recipe is not an artifact")
Expand All @@ -1031,6 +1033,7 @@ async fn fetch_project_from_registry(
.await?;
}
StaticQuery::Glob { .. } => {
let recipe_hash = recipe_hash.context("no recipe hash for glob static")?;
let recipe = crate::recipe::get_recipe(brioche, recipe_hash).await?;
let artifact: crate::recipe::Artifact = recipe.try_into().map_err(|_| {
anyhow::anyhow!("included static recipe is not an artifact")
Expand All @@ -1048,7 +1051,7 @@ async fn fetch_project_from_registry(
)
.await?;
}
StaticQuery::Download { .. } => {
StaticQuery::Download { .. } | StaticQuery::GitRef { .. } => {
// No need to do anything while fetching the project
}
}
Expand Down Expand Up @@ -1091,27 +1094,37 @@ async fn fetch_project_from_registry(
.iter()
.map(|(name, hash)| (name.clone(), *hash))
.collect();
let downloads = project
.statics
.values()
.flatten()
.filter_map(|(static_, output)| {
let url = match static_ {
StaticQuery::Download { url, .. } => url,
_ => return None,
};

let hash = match output {
Some(StaticOutput::Kind(StaticOutputKind::Download { hash })) => hash,
_ => return None,
};
let mut downloads = BTreeMap::new();
let mut git_refs = BTreeMap::new();
for (static_, output) in project.statics.values().flatten() {
match static_ {
StaticQuery::Include(_) | StaticQuery::Glob { .. } => {
continue;
}
StaticQuery::Download { url } => {
let Some(StaticOutput::Kind(StaticOutputKind::Download { hash })) = output else {
continue;
};

downloads.insert(url.clone(), hash.clone());
}
StaticQuery::GitRef(GitRefOptions { repository, ref_ }) => {
let Some(StaticOutput::Kind(StaticOutputKind::GitRef { commit })) = output else {
continue;
};

let repo_refs: &mut BTreeMap<_, _> =
git_refs.entry(repository.clone()).or_default();
repo_refs.insert(ref_.clone(), commit.clone());
}
}
}

Some((url.clone(), hash.clone()))
})
.collect();
let lockfile = Lockfile {
dependencies,
downloads,
git_refs,
};
let lockfile_path = temp_project_path.join("brioche.lock");
let lockfile_contents =
Expand Down Expand Up @@ -1439,6 +1452,37 @@ async fn resolve_static(
hash: download_hash,
}))
}
StaticQuery::GitRef(GitRefOptions { repository, ref_ }) => {
let current_commit = lockfile.and_then(|lockfile| {
lockfile
.git_refs
.get(repository)
.and_then(|repo_refs| repo_refs.get(ref_))
});

let commit = if let Some(commit) = current_commit {
commit.clone()
} else if lockfile_required {
// Error out if the git ref isn't in the lockfile but where
// updating the lockfile is disabled
anyhow::bail!(
"commit for git repo '{repository}' ref '{ref_}' not found in lockfile"
);
} else {
// Fetch the current commit hash of the git ref from the repo
crate::download::fetch_git_commit_for_ref(repository, ref_)
.await
.with_context(|| {
format!("failed to fetch ref '{ref_}' from git repo '{repository}'")
})?
};

// Update the new lockfile with the commit
let repo_refs = new_lockfile.git_refs.entry(repository.clone()).or_default();
repo_refs.insert(ref_.clone(), commit.clone());

Ok(StaticOutput::Kind(StaticOutputKind::GitRef { commit }))
}
}
}

Expand Down Expand Up @@ -1671,4 +1715,7 @@ pub struct Lockfile {

#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub downloads: BTreeMap<url::Url, crate::Hash>,

#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub git_refs: BTreeMap<url::Url, BTreeMap<String, String>>,
}
Loading

0 comments on commit e30ba51

Please sign in to comment.