Skip to content

Commit

Permalink
it builds...
Browse files Browse the repository at this point in the history
  • Loading branch information
jneem committed Nov 5, 2024
1 parent e67a1bd commit 143d570
Show file tree
Hide file tree
Showing 16 changed files with 786 additions and 535 deletions.
187 changes: 79 additions & 108 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"core",
"cli",
"git",
"vector",
"lsp/nls",
"lsp/lsp-harness",
Expand All @@ -24,6 +25,7 @@ readme = "README.md"

[workspace.dependencies]
nickel-lang-core = { version = "0.9.0", path = "./core", default-features = false }
nickel-lang-git = { version = "0.1.0", path = "./git" }
nickel-lang-package = { version = "0.1.0", path = "./package" }
nickel-lang-vector = { version = "0.1.0", path = "./vector" }
nickel-lang-utils = { version = "0.1.0", path = "./utils" }
Expand Down Expand Up @@ -57,6 +59,7 @@ derive_more = "0.99"
directories = "4.0.1"
env_logger = "0.10"
git-version = "0.3.5"
gix = "0.66.0"
indexmap = "1.9.3"
indoc = "2"
insta = "1.29.0"
Expand Down
1 change: 0 additions & 1 deletion core/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
//! [codespan](https://crates.io/crates/codespan-reporting) diagnostic from them.
use codespan::ByteIndex;

pub use codespan::{FileId, Files};
pub use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle};

use codespan_reporting::files::Files as _;
Expand Down
19 changes: 10 additions & 9 deletions flake.lock

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

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix";
Expand Down
16 changes: 16 additions & 0 deletions git/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "nickel-lang-git"
description = "Git utility functions for internal use in nickel"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow.workspace = true
gix = { workspace = true, features = ["blocking-network-client"] }
tempfile.workspace = true
thiserror.workspace = true

[dev-dependencies]
anyhow = { version = "1.0.86", features = ["backtrace"] }
clap = { version = "4.5.16", features = ["derive"] }
gix = { version = "0.66.0", features = ["blocking-http-transport-reqwest-rust-tls"] }
43 changes: 43 additions & 0 deletions git/examples/fetch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::path::PathBuf;

use clap::{ArgGroup, Parser};
use nickel_lang_git::{Spec, Target};

#[derive(Parser)]
#[command(group = ArgGroup::new("target").args(["branch", "tag", "commit"]))]
struct Args {
repository: String,

directory: PathBuf,

#[arg(long)]
branch: Option<String>,

#[arg(long)]
tag: Option<String>,

#[arg(long)]
commit: Option<String>,
}

pub fn main() -> anyhow::Result<()> {
let args = Args::parse();

let target = if let Some(branch) = args.branch {
Target::Branch(branch)
} else if let Some(tag) = args.tag {
Target::Tag(tag)
} else if let Some(commit) = args.commit {
Target::Commit(commit)
} else {
Target::Default
};

let spec = Spec {
url: args.repository.try_into()?,
target,
};

nickel_lang_git::fetch(&spec, args.directory)?;
Ok(())
}
187 changes: 187 additions & 0 deletions git/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use anyhow::anyhow;
use gix::{objs::Kind, ObjectId};
use std::{num::NonZero, path::Path};

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("io error {error} at {path}")]
Io {
error: std::io::Error,
path: std::path::PathBuf,
},

#[error("target `{0}` not found")]
TargetNotFound(Target),

#[error(transparent)]
Internal(#[from] anyhow::Error),
}

pub type Result<T, E = Error> = std::result::Result<T, E>;

trait IoResultExt<T> {
fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T>;
}

impl<T> IoResultExt<T> for Result<T, std::io::Error> {
fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T> {
self.map_err(|error| Error::Io {
error,
path: path.as_ref().to_owned(),
})
}
}

trait InternalResultExt<T> {
fn wrap_err(self) -> Result<T>;
}

impl<T, E: Into<anyhow::Error>> InternalResultExt<T> for Result<T, E> {
fn wrap_err(self) -> Result<T> {
self.map_err(|e| Error::Internal(e.into()))
}
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Spec {
pub url: gix::Url,
pub target: Target,
}

/// The different kinds of git "thing" that we can target.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
pub enum Target {
/// By default, we target the remote HEAD.
#[default]
Default,
/// Target the tip of a specific branch.
Branch(String),
/// Target a specific tag.
Tag(String),
/// Target a specific commit.
///
/// Currently, we only support a full commit: this needs to be a full hex-encoded
/// sha hash. We could try to support prefixes also, but it requires some work
/// and it *appears* to be inherently less efficient because there doesn't seem
/// to be a way to fetch it in one shot. At least, when `cargo` needs to fetch an
/// abbreviated hash it fetches everything and then looks for the hash among the
/// things that it finds.
Commit(String),
}

impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Target::Default => write!(f, "HEAD"),
Target::Branch(branch) => write!(f, "refs/heads/{branch}"),
Target::Tag(tag) => write!(f, "refs/tags/{tag}"),
Target::Commit(c) => write!(f, "{}", c),
}
}
}

impl Target {
fn refspec(&self) -> String {
self.to_string()
}
}

fn source_object_id(source: &gix::remote::fetch::Source) -> Result<ObjectId> {
match source {
gix::remote::fetch::Source::ObjectId(id) => Ok(*id),
gix::remote::fetch::Source::Ref(r) => {
let (_name, id, peeled) = r.unpack();

Ok(peeled
.or(id)
.ok_or_else(|| anyhow!("unborn reference"))?
.to_owned())
}
}
}

/// Fetches the contents of a git repository into a target directory.
///
/// The directory will be created if it doesn't exist yet. The data will be
/// fetched from scratch, even if it has already been fetched before. However,
/// we will try to minimize the amount of data to fetch (for example, by doing a
/// shallow fetch).
///
/// Only the contents of the git repository will be written to the given
/// directory; the git directory itself will be discarded.
pub fn fetch(spec: &Spec, dir: impl AsRef<Path>) -> Result<ObjectId> {
let dir = dir.as_ref();
let parent = dir.parent().ok_or(anyhow!("no parent"))?;

// Fetch the git directory somewhere temporary.
let git_tempdir = tempfile::tempdir().wrap_err()?;
let repo = gix::init(git_tempdir.path()).wrap_err()?;
let refspec = spec.target.refspec();

let remote = repo
.remote_at(spec.url.clone())
.wrap_err()?
.with_fetch_tags(gix::remote::fetch::Tags::None)
.with_refspecs(Some(refspec.as_str()), gix::remote::Direction::Fetch)
.wrap_err()?;

// This does similar credentials stuff to the git CLI (e.g. it looks for ssh
// keys if it's a fetch over ssh, or it tries to run `askpass` if it needs
// credentials for https). Maybe we want to have explicit credentials
// configuration instead of or in addition to the default?
let connection = remote.connect(gix::remote::Direction::Fetch).wrap_err()?;
let outcome = connection
.prepare_fetch(
&mut gix::progress::Discard,
gix::remote::ref_map::Options::default(),
)
.wrap_err()?
// For now, we always fetch shallow. Maybe for the index it's more efficient to
// keep a single repo around and update it? But that might be in another method.
.with_shallow(gix::remote::fetch::Shallow::DepthAtRemote(
NonZero::new(1).unwrap(),
))
.receive(&mut gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
.map_err(|e| match e {
gix::remote::fetch::Error::NoMapping { .. } => {
Error::TargetNotFound(spec.target.clone())
}
_ => Error::Internal(e.into()),
})?;

if outcome.ref_map.mappings.len() > 1 {
return Err(anyhow!("we only asked for 1 ref; why did we get more?")).wrap_err();
}
if outcome.ref_map.mappings.is_empty() {
return Err(Error::TargetNotFound(spec.target.clone()));
}
let object_id = source_object_id(&outcome.ref_map.mappings[0].remote)?;

let object = repo.find_object(object_id).wrap_err()?;
let commit = object.clone().peel_to_kind(Kind::Commit).wrap_err()?;
let tree_id = object.peel_to_tree().wrap_err()?.id();
let mut index = repo.index_from_tree(&tree_id).wrap_err()?;

let tree_tempdir = tempfile::tempdir_in(parent).wrap_err()?;
std::fs::create_dir_all(&tree_tempdir).with_path(&tree_tempdir)?;

gix::worktree::state::checkout(
&mut index,
tree_tempdir.path(),
repo.objects.clone(),
&gix::progress::Discard,
&gix::progress::Discard,
&gix::interrupt::IS_INTERRUPTED,
gix::worktree::state::checkout::Options {
overwrite_existing: true,
..Default::default()
},
)
.wrap_err()?;
index.write(Default::default()).wrap_err()?;

// Move the checked-out repo to the right place.
std::fs::rename(tree_tempdir.path(), dir).with_path(dir)?;

Ok(commit.id)
}
Loading

0 comments on commit 143d570

Please sign in to comment.