Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,497 changes: 1,398 additions & 99 deletions Cargo.lock

Large diffs are not rendered by default.

29 changes: 25 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ name = "qpm"
path = "src/main.rs"

[features]
default = ["templatr", "network_test"]
default = ["templatr", "network_test", "gitoxide"]
templatr = ["dep:templatr"]
network_test = []
gitoxide = ["dep:gix", "dep:prodash"]
libgit2 = ["dep:git2"]

[build-dependencies]
vergen = { version = "8", features = ["build", "git", "gitcl"] }
Expand All @@ -26,7 +28,7 @@ trycmd = "0.14"

[dependencies]
#qpm
qpm_package = { git = "https://github.com/QuestPackageManager/QPM.Package.git"}
qpm_package = { git = "https://github.com/QuestPackageManager/QPM.Package.git" }
qpm_qmod = { git = "https://github.com/QuestPackageManager/QPM.Qmod.git" }
qpm_arg_tokenizer = { git = "https://github.com/QuestPackageManager/QPM.arg_tokenizer.git" }
templatr = { git = "https://github.com/QuestPackageManager/templatr.git", optional = true }
Expand All @@ -36,10 +38,29 @@ color-eyre = { version = "0.6", default-features = false }
# progress bar
pbr = "*" #{ git = "https://github.com/a8m/pb.git" }

git2 = "0.19"
git2 = { version = "0.19", optional = true }

#gitoxide
# gix = {git = "https://github.com/Byron/gitoxide.git", features = ["command", "attributes", "progress-tree", "worktree-mutation", "blocking-network-client", "blocking-http-transport-reqwest-rust-tls"], optional = true }
gix = { git = "https://github.com/Fernthedev/gitoxide.git", branch = "feat/checkout-with-ref-name", features = [
"command",
"attributes",
"progress-tree",
"worktree-mutation",
"blocking-network-client",
"blocking-http-transport-reqwest-rust-tls",
], optional = true }
prodash = { version = "*", features = ["progress-log"], optional = true }
pretty_env_logger = "0.5"
log = "0.4"

#tokio fix
tokio = { version = ">=1.33", features = ["io-util", "io-std", "net", "macros"] }
tokio = { version = ">=1.33", features = [
"io-util",
"io-std",
"net",
"macros",
] }

bytes = "*"
reqwest = { version = "0.12", features = [
Expand Down
18 changes: 17 additions & 1 deletion src/commands/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,30 @@ pub struct RestoreCommand {
offline: bool,
}

fn is_ignored() -> bool {
#[cfg(feature = "libgit2")]
pub(crate) fn is_ignored() -> bool {
git2::Repository::open(".").is_ok_and(|r| {
r.is_path_ignored(SHARED_PACKAGE_FILE_NAME) == Ok(true)
|| r.status_file(Path::new(SHARED_PACKAGE_FILE_NAME))
.is_ok_and(|s| s.is_ignored())
})
}

#[cfg(feature = "gitoxide")]
pub(crate) fn is_ignored() -> bool {
gix::open(".").is_ok_and(|r| {
let Ok(index) = r.index() else {return false};

let excludes = r.excludes(&index, None, Default::default());

excludes.is_ok_and(|mut attribute| {
attribute
.at_path(SHARED_PACKAGE_FILE_NAME, Some(gix::index::entry::Mode::FILE))
.is_ok_and(|e| e.is_excluded())
})
})
}

impl Command for RestoreCommand {
fn execute(self) -> color_eyre::Result<()> {
let package = PackageConfig::read(".")?;
Expand Down
8 changes: 8 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ mod benchmark;
#[cfg(test)]
mod tests;


#[cfg(all(feature = "gitoxide", feature = "libgit2"))]
compile_error!("feature \"gitoxide\" and feature \"libgit2\" cannot be enabled at the same time");

fn main() -> Result<()> {
pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Info)
.parse_default_env()
.init();
color_eyre::config::HookBuilder::default()
.panic_section(concat!(
"version ",
Expand Down
2 changes: 1 addition & 1 deletion src/repository/qpackages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ impl QPMRepository {
// github url!
git::clone(
url.clone(),
config.info.additional_data.branch_name.as_ref(),
config.info.additional_data.branch_name.as_deref(),
&tmp_path,
)
.context("Clone")?;
Expand Down
174 changes: 166 additions & 8 deletions src/utils/git.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
use std::{
fs::File,
io::{BufReader, BufWriter},
path::Path,
process::{Command, Stdio},
};
use std::{fs::File, io::BufWriter, path::Path};

use color_eyre::{
eyre::{bail, Context},
Result, Section,
Result,
};
use owo_colors::OwoColorize;
//use duct::cmd;
Expand All @@ -19,6 +14,7 @@ use crate::{
terminal::colors::QPMColor,
};

#[cfg(feature = "libgit2")]
pub fn check_git() -> Result<()> {
let mut git = std::process::Command::new("git");
git.arg("--version");
Expand Down Expand Up @@ -49,6 +45,11 @@ pub fn check_git() -> Result<()> {
}
}

#[cfg(feature = "gitoxide")]
pub fn check_git() -> Result<()> {
Ok(())
}

pub fn get_release(url: &str, out: &std::path::Path) -> Result<bool> {
check_git()?;
if let Ok(token_unwrapped) = get_keyring().get_password() {
Expand Down Expand Up @@ -125,7 +126,8 @@ pub fn get_release_with_token(url: &str, out: &std::path::Path, token: &str) ->
Ok(out.exists())
}

pub fn clone(mut url: String, branch: Option<&String>, out: &Path) -> Result<bool> {
#[cfg(feature = "libgit2")]
pub fn clone(mut url: String, branch: Option<&str>, out: &Path) -> Result<bool> {
check_git()?;
if let Ok(token_unwrapped) = get_keyring().get_password() {
if let Some(gitidx) = url.find("github.com") {
Expand Down Expand Up @@ -189,6 +191,162 @@ pub fn clone(mut url: String, branch: Option<&String>, out: &Path) -> Result<boo
Ok(out.try_exists()?)
}

// https://github.com/rust-lang/cargo/blob/7da3c360bc82021e0daf93dba092628165375456/src/cargo/sources/git/utils.rs#L346
#[cfg(feature = "gitoxide")]
fn recursive_update(repo: &gix::Repository) -> color_eyre::Result<()> {
use std::num::NonZero;

use prodash::progress::Log;

use color_eyre::eyre::ContextCompat;
use gix::{
bstr::BStr,
clone::{PrepareCheckout, PrepareFetch},
submodule::config::Update,
};

use crate::utils::progress::PbrProgress;

let Some(mut submodules) = repo.submodules().context("Submodules listing")? else {
return Ok(());
};

println!(
"Recursive cloning submodules for {}",
repo.path().display().file_path_color()
);

submodules.try_for_each(|submodule| -> color_eyre::Result<()> {
println!(
"Cloning submodule {} {}",
submodule.name().download_file_name_color(),
submodule.path()?.file_path_color()
);

let update = submodule.update()?.unwrap_or(Default::default());

if update == Update::None {
return Ok(());
}

let sub_url = submodule.url()?;
let sub_path = submodule.work_dir()?;

let mut prepare_clone = gix::prepare_clone(sub_url, &sub_path)
.with_context(|| format!("Preparing submodule clone {sub_path:?}"))?
.with_shallow(gix::remote::fetch::Shallow::DepthAtRemote(
NonZero::new(1).unwrap(),
));

let (mut prepare_checkout, _) = prepare_clone
.fetch_then_checkout(
// prodash::progress::Log::new("Fetch", None),
PbrProgress::default(),
&gix::interrupt::IS_INTERRUPTED,
)
.with_context(|| format!("Fetching and checking out submodule {sub_path:?}"))?;

println!(
"Checking out submodule {} into {:?} ...",
submodule.name(),
prepare_checkout
.repo()
.work_dir()
.context("repo work dir")?
);

if let Some(index_id) = submodule.index_id()? {
prepare_checkout = prepare_checkout.with_object_id(Some(index_id));
}

let (sub_repo, _) = prepare_checkout
.main_worktree(PbrProgress::default(), &gix::interrupt::IS_INTERRUPTED)
.with_context(|| format!("Submodule worktree {}", submodule.name()))?;

recursive_update(&sub_repo)
.with_context(|| format!("Cloning submodules of {sub_path:?}"))
})?;

Ok(())
}

#[cfg(feature = "gitoxide")]
pub fn clone(url: String, branch: Option<&str>, out: &Path) -> Result<bool> {
use std::num::NonZero;

use color_eyre::eyre::{ContextCompat, OptionExt};
use gix::{bstr::BStr, head, refs::PartialName, submodule::config::Update, trace::warn};

use crate::{terminal::colors::QPMColor, utils::progress::PbrProgress};

check_git()?;
// TODO: Figure out tokens\
// TODO: Clone submodules

let branch_ref: Option<PartialName> = branch.map(PartialName::try_from).transpose()?;

let mut prepare_clone = gix::prepare_clone(gix::url::parse(url.as_str().into())?, out)?
.with_shallow(gix::remote::fetch::Shallow::DepthAtRemote(
NonZero::new(1).unwrap(),
))
.with_ref_name(branch_ref.as_ref())?;

let (mut prepare_checkout, _) = prepare_clone.fetch_then_checkout(
// prodash::progress::Log::new("Fetch", None),
PbrProgress::default(),
&gix::interrupt::IS_INTERRUPTED,
)?;

println!(
"Checking out into {:?} ...",
prepare_checkout
.repo()
.work_dir()
.context("repo work dir")?
);

// let branch_ref = branch
// .map(|b| prepare_checkout.repo().find_reference(b))
// .transpose()?;

// let branch_fn = branch.map(|b| {
// let str = b.to_string();
// move |r: &'a Repository| -> Reference<'a> {
// r.find_reference(str.as_str())
// .expect("remote branch not found")
// }
// });

let (repo, _) = prepare_checkout
.main_worktree(PbrProgress::default(), &gix::interrupt::IS_INTERRUPTED)
.with_context(|| format!("Checkout {branch:?}"))?;

recursive_update(&repo).with_context(|| format!("Cloning submodules of root repo"))?;

println!(
"Repo cloned into {:?}",
repo.work_dir().expect("directory pre-created")
);

let remote = repo
.find_default_remote(gix::remote::Direction::Fetch)
.expect("always present after clone")?;

println!(
"Default remote: {} -> {}",
remote
.name()
.expect("default remote is always named")
.as_bstr(),
remote
.url(gix::remote::Direction::Fetch)
.expect("should be the remote URL")
.to_bstring(),
);

Ok(out.try_exists()?)
}

#[derive(Serialize, Deserialize, Debug)]
pub struct GithubReleaseAsset {
pub url: String,
Expand Down
3 changes: 3 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ pub mod fs;
pub mod git;
pub mod json;
pub mod toggle;

#[cfg(feature = "gitoxide")]
pub mod progress;
Loading