xtask: Add more helpers for querying information on GPUI crates (#64333)
Primarily, add a - helper to easily get information on what pulls in a crate (hey Mr Piotr we can consider moving this up in the namespace to also use this elsewhere) - helper to extract a list of GPUI crates in order which need publishing Release Notes: - N/A
This commit is contained in:
1 parent
c96197a34d
commit
1df382558f
5 files changed
+518
-52
No files matched your search
@@ -19,8 +19,9 @@ enum CliCommand {
|
||||
Licenses(tasks::licenses::LicensesArgs),
|
||||
/// Checks that packages conform to a set of standards.
|
||||
PackageConformity(tasks::package_conformity::PackageConformityArgs),
|
||||
/// Prints the GPUI crate graph in dependency-first order.
|
||||
Gpui,
|
||||
/// Inspect the GPUI publish plan and its dependencies.
|
||||
#[command(subcommand)]
|
||||
Gpui(tasks::gpui::GpuiCommand),
|
||||
/// Runs the Linux Bubblewrap sandboxing NixOS VM tests.
|
||||
SandboxTests(tasks::sandbox_tests::SandboxTestsArgs),
|
||||
/// Runs the Windows WSL Bubblewrap sandbox behavior tests.
|
||||
@@ -43,7 +44,7 @@ fn main() -> Result<()> {
|
||||
CliCommand::PackageConformity(args) => {
|
||||
tasks::package_conformity::run_package_conformity(args)
|
||||
}
|
||||
CliCommand::Gpui => tasks::gpui::run_gpui(),
|
||||
CliCommand::Gpui(args) => tasks::gpui::run_gpui(args),
|
||||
CliCommand::SandboxTests(args) => tasks::sandbox_tests::run_sandbox_tests(args),
|
||||
CliCommand::WslSandboxTests(args) => tasks::wsl_sandbox_tests::run_wsl_sandbox_tests(args),
|
||||
CliCommand::SetupWebrtc(args) => tasks::setup_webrtc::run_setup_webrtc(args),
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
#![allow(clippy::disallowed_methods, reason = "tooling is exempt")]
|
||||
|
||||
pub mod crate_graph;
|
||||
pub mod publish_plan;
|
||||
|
||||
use std::io::{self, Write};
|
||||
use std::process::{Command, Output, Stdio};
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use clap::Parser;
|
||||
use clap::{Parser, Subcommand};
|
||||
use guppy::graph::DependencyDirection;
|
||||
|
||||
use crate::tasks::workflows::GitSha;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum GpuiCommand {
|
||||
/// Print the publish plan in dependency-first order
|
||||
List(GpuiArgs),
|
||||
/// Show the dependency paths that put a crate in the publish list
|
||||
Why {
|
||||
/// Original Cargo package name
|
||||
crate_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct GpuiArgs {
|
||||
/// Full commit SHA to use when checking out workspace crates
|
||||
#[arg(long)]
|
||||
sha: GitSha,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[allow(unused)]
|
||||
struct PublishGpuiArgs {
|
||||
/// Perform a dry-run and wait for user confirmation before each publish
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
@@ -20,18 +42,40 @@ pub struct GpuiArgs {
|
||||
skip_to: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run_gpui() -> Result<()> {
|
||||
pub fn run_gpui(command: GpuiCommand) -> Result<()> {
|
||||
let graph = crate_graph::load_workspace_graph()?;
|
||||
let crates = crate_graph::gpui_crates(&graph)?;
|
||||
|
||||
println!("GPUI crate graph ({} crates):", crates.len());
|
||||
for package in crates.packages(DependencyDirection::Reverse) {
|
||||
println!("{} ({})", package.name(), package.source());
|
||||
for link in package.direct_links().filter(|link| !link.dev_only()) {
|
||||
match crates.contains(link.to().id()) {
|
||||
Ok(true) => println!(" -> {}", link.to().name()),
|
||||
Ok(false) => {}
|
||||
Err(e) => eprintln!(" error checking dependency {}: {}", link.to().name(), e),
|
||||
match command {
|
||||
GpuiCommand::List(args) => {
|
||||
let plan = publish_plan::build_publish_plan(&graph)?;
|
||||
println!(
|
||||
"GPUI publish plan ({} crates, dependencies first):",
|
||||
plan.len()
|
||||
);
|
||||
for entry in &plan {
|
||||
let package = entry.repository.publish_info(entry.package.name());
|
||||
println!(
|
||||
"{} -> {} ({})",
|
||||
package.original_name,
|
||||
package.repository.target_name(package.original_name),
|
||||
package.repository.display(&args)
|
||||
);
|
||||
}
|
||||
}
|
||||
GpuiCommand::Why { crate_name } => {
|
||||
let explanation = crate_graph::explain_crate(&graph, &crate_name)?;
|
||||
println!(
|
||||
"Why `{crate_name}` is in the GPUI publish list (A -> B means A depends on B):"
|
||||
);
|
||||
for package in explanation.packages(DependencyDirection::Forward) {
|
||||
if crate_graph::is_gpui_root(package) {
|
||||
println!(" {} is a GPUI workspace root", package.name());
|
||||
}
|
||||
}
|
||||
for link in explanation
|
||||
.links(DependencyDirection::Forward)
|
||||
.filter(|link| !link.dev_only())
|
||||
{
|
||||
println!(" {} -> {}", link.from().name(), link.to().name());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +83,7 @@ pub fn run_gpui() -> Result<()> {
|
||||
}
|
||||
|
||||
#[allow(unused, reason = "retained while GPUI releases are reworked")]
|
||||
fn run_publish_gpui(args: GpuiArgs) -> Result<()> {
|
||||
fn run_publish_gpui(args: PublishGpuiArgs) -> Result<()> {
|
||||
println!(
|
||||
"Starting GPUI publish process{}...",
|
||||
if args.dry_run { " (with dry-run)" } else { "" }
|
||||
@@ -363,6 +407,29 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn gpui_list_accepts_an_explicit_workspace_sha() -> Result<()> {
|
||||
let sha = "0123456789abcdef0123456789abcdef01234567";
|
||||
let args = crate::Args::try_parse_from(["xtask", "gpui", "list", "--sha", sha])?;
|
||||
let crate::CliCommand::Gpui(GpuiCommand::List(args)) = args.command else {
|
||||
bail!("expected the GPUI list command");
|
||||
};
|
||||
assert_eq!(args.sha.as_ref(), sha);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpui_list_requires_a_full_workspace_sha() {
|
||||
assert!(crate::Args::try_parse_from(["xtask", "gpui"]).is_err());
|
||||
assert!(crate::Args::try_parse_from(["xtask", "gpui", "list"]).is_err());
|
||||
for sha in ["HEAD", "abc", "g123456789abcdef0123456789abcdef01234567"] {
|
||||
assert!(
|
||||
crate::Args::try_parse_from(["xtask", "gpui", "list", "--sha", sha]).is_err(),
|
||||
"{sha}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_dependency_version_in_toml() {
|
||||
let input = indoc! {r#"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use guppy::{
|
||||
MetadataCommand,
|
||||
graph::{DependencyDirection, ExternalSource, PackageGraph, PackageSet, PackageSource},
|
||||
graph::{
|
||||
DependencyDirection, ExternalSource, PackageGraph, PackageMetadata, PackageSet,
|
||||
PackageSource,
|
||||
},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
@@ -11,12 +14,16 @@ pub fn load_workspace_graph() -> Result<PackageGraph> {
|
||||
.context("failed to load the workspace crate graph")
|
||||
}
|
||||
|
||||
pub fn is_gpui_root(package: PackageMetadata<'_>) -> bool {
|
||||
package.in_workspace() && package.name().starts_with("gpui")
|
||||
}
|
||||
|
||||
/// Returns all GPUI crates in the current workspace
|
||||
pub fn gpui_crates(graph: &PackageGraph) -> Result<PackageSet<'_>> {
|
||||
let roots = graph
|
||||
.workspace()
|
||||
.iter_by_name()
|
||||
.filter(|(name, _)| name.starts_with("gpui"))
|
||||
.filter(|(_, package)| is_gpui_root(*package))
|
||||
.map(|(_, package)| package.id())
|
||||
.collect::<Vec<_>>();
|
||||
ensure!(!roots.is_empty(), "no GPUI crates found in the workspace");
|
||||
@@ -34,6 +41,29 @@ pub fn gpui_crates(graph: &PackageGraph) -> Result<PackageSet<'_>> {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn explain_crate<'graph>(
|
||||
graph: &'graph PackageGraph,
|
||||
name: &str,
|
||||
) -> Result<PackageSet<'graph>> {
|
||||
let crates = gpui_crates(graph)?;
|
||||
let selected = crates.filter(DependencyDirection::Forward, |package| {
|
||||
package.name() == name
|
||||
});
|
||||
ensure!(
|
||||
!selected.is_empty(),
|
||||
"crate `{name}` is not in the GPUI publish list"
|
||||
);
|
||||
|
||||
let dependents = selected
|
||||
.to_package_query(DependencyDirection::Reverse)
|
||||
.resolve_with_fn(|_, link| !link.dev_only());
|
||||
|
||||
let dependencies = crates
|
||||
.to_package_query(DependencyDirection::Forward)
|
||||
.resolve_with_fn(|_, link| !link.dev_only());
|
||||
Ok(dependents.intersection(&dependencies))
|
||||
}
|
||||
|
||||
fn is_zed_fork(source: PackageSource<'_>) -> bool {
|
||||
let Some(ExternalSource::Git { repository, .. }) = source.parse_external() else {
|
||||
return false;
|
||||
@@ -219,7 +249,9 @@ mod tests {
|
||||
},
|
||||
FixturePackage {
|
||||
name: "fork",
|
||||
origin: Git("git+https://github.com/zed-industries/fork?rev=abc#abc"),
|
||||
origin: Git(
|
||||
"git+https://github.com/zed-industries/fork?rev=abc#0123456789abcdef0123456789abcdef01234567",
|
||||
),
|
||||
dependencies: &[FixtureDependency {
|
||||
to: "collections",
|
||||
kind: Normal(Required),
|
||||
@@ -283,6 +315,83 @@ mod tests {
|
||||
assert!(crates.links(DependencyDirection::Reverse).all(|link| {
|
||||
names.contains(&link.from().name()) && names.contains(&link.to().name())
|
||||
}));
|
||||
|
||||
let explanation = explain_crate(&graph, "fork")?;
|
||||
assert_eq!(
|
||||
explanation
|
||||
.packages(DependencyDirection::Forward)
|
||||
.map(|package| package.name())
|
||||
.collect::<Vec<_>>(),
|
||||
["gpui_platform", "gpui", "registry", "fork"],
|
||||
);
|
||||
assert_eq!(
|
||||
explanation
|
||||
.links(DependencyDirection::Forward)
|
||||
.filter(|link| !link.dev_only())
|
||||
.map(|link| (link.from().name(), link.to().name()))
|
||||
.collect::<std::collections::BTreeSet<_>>(),
|
||||
[
|
||||
("gpui_platform", "gpui"),
|
||||
("gpui", "registry"),
|
||||
("registry", "fork")
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
let root = explain_crate(&graph, "gpui_platform")?;
|
||||
assert_eq!(root.len(), 1);
|
||||
assert!(
|
||||
root.packages(DependencyDirection::Forward)
|
||||
.all(is_gpui_root)
|
||||
);
|
||||
for excluded in ["zed", "test_helper", "registry", "missing"] {
|
||||
assert!(explain_crate(&graph, excluded).is_err(), "{excluded}");
|
||||
}
|
||||
|
||||
use super::super::publish_plan::{Repository, build_publish_plan};
|
||||
|
||||
let plan = build_publish_plan(&graph)?;
|
||||
assert_eq!(
|
||||
plan.iter()
|
||||
.map(|entry| entry.package.name())
|
||||
.collect::<Vec<_>>(),
|
||||
names,
|
||||
);
|
||||
assert_eq!(
|
||||
plan.iter()
|
||||
.map(|entry| entry.package.id())
|
||||
.collect::<Vec<_>>(),
|
||||
crates
|
||||
.package_ids(DependencyDirection::Reverse)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
for entry in &plan {
|
||||
let package = entry.repository.publish_info(entry.package.name());
|
||||
match package.repository {
|
||||
Repository::Workspace => {
|
||||
assert_ne!(package.original_name, "fork");
|
||||
if package.original_name == "gpui_macros" {
|
||||
assert_eq!(
|
||||
package.repository.target_name(package.original_name),
|
||||
"gpui-macros"
|
||||
);
|
||||
}
|
||||
}
|
||||
Repository::Git(repository) => {
|
||||
assert_eq!(package.original_name, "fork");
|
||||
assert_eq!(
|
||||
package.repository.target_name(package.original_name),
|
||||
"zed-fork"
|
||||
);
|
||||
assert_eq!(repository.owner, "zed-industries");
|
||||
assert_eq!(repository.repo, "fork");
|
||||
assert_eq!(
|
||||
repository.revision.as_ref(),
|
||||
"0123456789abcdef0123456789abcdef01234567"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use guppy::graph::{DependencyDirection, ExternalSource, PackageGraph, PackageMetadata};
|
||||
use url::Url;
|
||||
|
||||
use crate::tasks::{gpui::GpuiArgs, workflows::GitSha};
|
||||
|
||||
pub enum Repository {
|
||||
Workspace,
|
||||
Git(GitRepository),
|
||||
}
|
||||
|
||||
pub struct GitRepository {
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
pub revision: GitSha,
|
||||
}
|
||||
|
||||
impl GitRepository {
|
||||
fn new(repository: &str, revision: &str) -> Result<Self> {
|
||||
let url = Url::parse(repository).context("invalid Git repository URL")?;
|
||||
ensure!(
|
||||
url.host_str()
|
||||
.is_some_and(|host| host.eq_ignore_ascii_case("github.com")),
|
||||
"expected a GitHub repository, got {repository}"
|
||||
);
|
||||
let (owner, repo) = url
|
||||
.path()
|
||||
.trim_matches('/')
|
||||
.split_once('/')
|
||||
.context("repository URL must contain an owner and repository name")?;
|
||||
let repo = repo.strip_suffix(".git").unwrap_or(repo);
|
||||
ensure!(
|
||||
!owner.is_empty() && !repo.is_empty() && !repo.contains('/'),
|
||||
"expected an owner/repo path, got {repository}"
|
||||
);
|
||||
let revision = revision
|
||||
.parse::<GitSha>()
|
||||
.map_err(anyhow::Error::msg)
|
||||
.with_context(|| format!("invalid resolved revision for {repository}"))?;
|
||||
|
||||
Ok(Self {
|
||||
owner: owner.to_owned(),
|
||||
repo: repo.to_owned(),
|
||||
revision,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Repository {
|
||||
pub fn fork(repository: &str, revision: &str) -> Result<Self> {
|
||||
Ok(Self::Git(GitRepository::new(repository, revision)?))
|
||||
}
|
||||
|
||||
pub fn publish_info<'a>(&'a self, name: &'a str) -> CratePublishInfo<'a> {
|
||||
CratePublishInfo {
|
||||
repository: self,
|
||||
original_name: name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
Self::Workspace => "zed-industries/zed".to_owned(),
|
||||
Self::Git(repository) => format!(
|
||||
"{owner}/{repo}",
|
||||
owner = repository.owner,
|
||||
repo = repository.repo
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_name(&self, name: &str) -> String {
|
||||
match self {
|
||||
Self::Workspace => {
|
||||
if name == "gpui_macros" {
|
||||
"gpui-macros".to_owned()
|
||||
} else if name == "gpui" || name.starts_with("gpui_") {
|
||||
name.to_owned()
|
||||
} else {
|
||||
format!("gpui_{name}")
|
||||
}
|
||||
}
|
||||
Self::Git(_) => {
|
||||
if name.starts_with("zed-") {
|
||||
name.to_owned()
|
||||
} else {
|
||||
format!("zed-{name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display<'a>(&'a self, args: &'a GpuiArgs) -> impl fmt::Display + 'a {
|
||||
let revision = match self {
|
||||
Self::Workspace => &args.sha,
|
||||
Self::Git(repository) => &repository.revision,
|
||||
};
|
||||
fmt::from_fn(move |formatter| {
|
||||
write!(
|
||||
formatter,
|
||||
"{name}@{revision}",
|
||||
name = self.name(),
|
||||
revision = revision.as_ref()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CratePublishInfo<'a> {
|
||||
pub repository: &'a Repository,
|
||||
pub original_name: &'a str,
|
||||
}
|
||||
|
||||
pub struct PublishPlanEntry<'graph> {
|
||||
pub package: PackageMetadata<'graph>,
|
||||
pub repository: Repository,
|
||||
}
|
||||
|
||||
pub fn build_publish_plan(graph: &PackageGraph) -> Result<Vec<PublishPlanEntry<'_>>> {
|
||||
let packages = super::crate_graph::gpui_crates(graph)?;
|
||||
packages
|
||||
.packages(DependencyDirection::Reverse)
|
||||
.map(|package| {
|
||||
let repository = if package.in_workspace() {
|
||||
Repository::Workspace
|
||||
} else if let Some(ExternalSource::Git {
|
||||
repository,
|
||||
resolved,
|
||||
..
|
||||
}) = package.source().parse_external()
|
||||
{
|
||||
Repository::fork(repository, resolved)?
|
||||
} else {
|
||||
bail!(
|
||||
"unexpected source for GPUI crate {}: {}",
|
||||
package.name(),
|
||||
package.source()
|
||||
);
|
||||
};
|
||||
Ok(PublishPlanEntry {
|
||||
package,
|
||||
repository,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const REVISION: &str = "0123456789abcdef0123456789abcdef01234567";
|
||||
const WORKSPACE_REVISION: &str = "fedcba9876543210fedcba9876543210fedcba98";
|
||||
|
||||
#[test]
|
||||
fn publishing_info_borrows_its_repository_and_crate_name() {
|
||||
let repository = Repository::Workspace;
|
||||
let name = String::from("collections");
|
||||
let package = repository.publish_info(&name);
|
||||
|
||||
assert!(std::ptr::eq(package.repository, &repository));
|
||||
assert!(std::ptr::eq(package.original_name, name.as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_names_follow_existing_publish_names() {
|
||||
let repository = Repository::Workspace;
|
||||
for (original, target) in [
|
||||
("collections", "gpui_collections"),
|
||||
("perf", "gpui_perf"),
|
||||
("util_macros", "gpui_util_macros"),
|
||||
("util", "gpui_util"),
|
||||
("gpui_macros", "gpui-macros"),
|
||||
("http_client", "gpui_http_client"),
|
||||
("derive_refineable", "gpui_derive_refineable"),
|
||||
("refineable", "gpui_refineable"),
|
||||
("semantic_version", "gpui_semantic_version"),
|
||||
("sum_tree", "gpui_sum_tree"),
|
||||
("media", "gpui_media"),
|
||||
("gpui", "gpui"),
|
||||
("gpui_platform", "gpui_platform"),
|
||||
("gpui-macros", "gpui_gpui-macros"),
|
||||
("scheduler", "gpui_scheduler"),
|
||||
] {
|
||||
let package = repository.publish_info(original);
|
||||
assert_eq!(package.original_name, original);
|
||||
assert_eq!(
|
||||
package.repository.target_name(package.original_name),
|
||||
target
|
||||
);
|
||||
assert!(matches!(package.repository, Repository::Workspace));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fork_names_have_a_single_zed_prefix() -> Result<()> {
|
||||
let repository = Repository::fork("https://github.com/zed-industries/fork", REVISION)?;
|
||||
for (original, target) in [
|
||||
("calloop", "zed-calloop"),
|
||||
("wasm_thread", "zed-wasm_thread"),
|
||||
("zed-font-kit", "zed-font-kit"),
|
||||
("zed-scap", "zed-scap"),
|
||||
] {
|
||||
let package = repository.publish_info(original);
|
||||
assert_eq!(package.original_name, original);
|
||||
assert_eq!(
|
||||
package.repository.target_name(package.original_name),
|
||||
target
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_repository_uses_the_argument_sha() -> Result<()> {
|
||||
let repository = Repository::Workspace;
|
||||
let package = repository.publish_info("gpui");
|
||||
assert_eq!(package.repository.name(), "zed-industries/zed");
|
||||
for revision in [REVISION, WORKSPACE_REVISION] {
|
||||
let args = GpuiArgs {
|
||||
sha: revision.parse().map_err(anyhow::Error::msg)?,
|
||||
};
|
||||
assert_eq!(
|
||||
package.repository.display(&args).to_string(),
|
||||
format!("zed-industries/zed@{revision}")
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_repositories_have_checkout_coordinates() -> Result<()> {
|
||||
let args = GpuiArgs {
|
||||
sha: WORKSPACE_REVISION.parse().map_err(anyhow::Error::msg)?,
|
||||
};
|
||||
for url in [
|
||||
"https://github.com/zed-industries/fork",
|
||||
"https://github.com/zed-industries/fork.git",
|
||||
"https://github.com/zed-industries/fork.git/",
|
||||
"ssh://git@github.com/zed-industries/fork.git",
|
||||
] {
|
||||
let repository = Repository::fork(url, REVISION)?;
|
||||
let package = repository.publish_info("fork");
|
||||
assert_eq!(package.repository.name(), "zed-industries/fork");
|
||||
assert_eq!(
|
||||
package.repository.display(&args).to_string(),
|
||||
format!("zed-industries/fork@{REVISION}")
|
||||
);
|
||||
let Repository::Git(repository) = package.repository else {
|
||||
bail!("expected a Git repository");
|
||||
};
|
||||
assert_eq!(repository.owner, "zed-industries");
|
||||
assert_eq!(repository.repo, "fork");
|
||||
assert_eq!(repository.revision.as_ref(), REVISION);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_repositories_require_an_owner_repo_and_full_sha() {
|
||||
for url in [
|
||||
"not a URL",
|
||||
"https://example.com/zed-industries/fork",
|
||||
"https://github.com/zed-industries",
|
||||
"https://github.com/zed-industries/",
|
||||
"https://github.com/zed-industries/.git",
|
||||
"https://github.com/zed-industries/fork/tree/main",
|
||||
] {
|
||||
assert!(Repository::fork(url, REVISION).is_err(), "{url}");
|
||||
}
|
||||
for revision in [
|
||||
"",
|
||||
"main",
|
||||
"v1.0",
|
||||
"abc",
|
||||
"g123456789abcdef0123456789abcdef01234567",
|
||||
] {
|
||||
assert!(
|
||||
Repository::fork("https://github.com/zed-industries/fork", revision).is_err(),
|
||||
"{revision}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,44 +41,47 @@ impl AsRef<str> for GitSha {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for GitSha {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
const GIT_SHA_LENGTH: usize = 40;
|
||||
if value.len() != GIT_SHA_LENGTH {
|
||||
return Err(format!(
|
||||
"Git SHA has wrong length! \
|
||||
Only SHAs with a full length of {GIT_SHA_LENGTH} are supported, found {len} characters.",
|
||||
len = value.len()
|
||||
));
|
||||
}
|
||||
if !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
|
||||
return Err("Not a valid Git SHA".to_owned());
|
||||
}
|
||||
Ok(Self(value.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This runs only in a CLI environment"
|
||||
)]
|
||||
fn parse_ref(value: &str) -> Result<GitSha, String> {
|
||||
const GIT_SHA_LENGTH: usize = 40;
|
||||
(value.len() == GIT_SHA_LENGTH)
|
||||
.then_some(value)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"Git SHA has wrong length! \
|
||||
Only SHAs with a full length of {GIT_SHA_LENGTH} are supported, found {len} characters.",
|
||||
len = value.len()
|
||||
)
|
||||
})
|
||||
.and_then(|value| {
|
||||
let mut tmp = [0; 4];
|
||||
value
|
||||
.chars()
|
||||
.all(|char| u16::from_str_radix(char.encode_utf8(&mut tmp), 16).is_ok()).then_some(value)
|
||||
.ok_or_else(|| "Not a valid Git SHA".to_owned())
|
||||
})
|
||||
.and_then(|sha| {
|
||||
std::process::Command::new("git")
|
||||
.args([
|
||||
"rev-parse",
|
||||
"--quiet",
|
||||
"--verify",
|
||||
&format!("{sha}^{{commit}}")
|
||||
])
|
||||
.output()
|
||||
.map_err(|_| "Failed to spawn Git command to verify SHA".to_owned())
|
||||
.and_then(|output|
|
||||
output
|
||||
.status.success()
|
||||
.then_some(sha)
|
||||
.ok_or_else(|| format!("SHA {sha} is not a valid Git SHA within this repository!")))
|
||||
}).map(|sha| GitSha(sha.to_owned()))
|
||||
let sha = value.parse::<GitSha>()?;
|
||||
let output = std::process::Command::new("git")
|
||||
.args([
|
||||
"rev-parse",
|
||||
"--quiet",
|
||||
"--verify",
|
||||
&format!("{}^{{commit}}", sha.as_ref()),
|
||||
])
|
||||
.output()
|
||||
.map_err(|_| "Failed to spawn Git command to verify SHA".to_owned())?;
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"SHA {} is not a valid Git SHA within this repository!",
|
||||
sha.as_ref()
|
||||
));
|
||||
}
|
||||
Ok(sha)
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
|
||||
Reference in new issue
Block a user