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),
|
Licenses(tasks::licenses::LicensesArgs),
|
||||||
/// Checks that packages conform to a set of standards.
|
/// Checks that packages conform to a set of standards.
|
||||||
PackageConformity(tasks::package_conformity::PackageConformityArgs),
|
PackageConformity(tasks::package_conformity::PackageConformityArgs),
|
||||||
/// Prints the GPUI crate graph in dependency-first order.
|
/// Inspect the GPUI publish plan and its dependencies.
|
||||||
Gpui,
|
#[command(subcommand)]
|
||||||
|
Gpui(tasks::gpui::GpuiCommand),
|
||||||
/// Runs the Linux Bubblewrap sandboxing NixOS VM tests.
|
/// Runs the Linux Bubblewrap sandboxing NixOS VM tests.
|
||||||
SandboxTests(tasks::sandbox_tests::SandboxTestsArgs),
|
SandboxTests(tasks::sandbox_tests::SandboxTestsArgs),
|
||||||
/// Runs the Windows WSL Bubblewrap sandbox behavior tests.
|
/// Runs the Windows WSL Bubblewrap sandbox behavior tests.
|
||||||
@@ -43,7 +44,7 @@ fn main() -> Result<()> {
|
|||||||
CliCommand::PackageConformity(args) => {
|
CliCommand::PackageConformity(args) => {
|
||||||
tasks::package_conformity::run_package_conformity(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::SandboxTests(args) => tasks::sandbox_tests::run_sandbox_tests(args),
|
||||||
CliCommand::WslSandboxTests(args) => tasks::wsl_sandbox_tests::run_wsl_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),
|
CliCommand::SetupWebrtc(args) => tasks::setup_webrtc::run_setup_webrtc(args),
|
||||||
|
|||||||
@@ -1,16 +1,38 @@
|
|||||||
#![allow(clippy::disallowed_methods, reason = "tooling is exempt")]
|
#![allow(clippy::disallowed_methods, reason = "tooling is exempt")]
|
||||||
|
|
||||||
pub mod crate_graph;
|
pub mod crate_graph;
|
||||||
|
pub mod publish_plan;
|
||||||
|
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::process::{Command, Output, Stdio};
|
use std::process::{Command, Output, Stdio};
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use clap::Parser;
|
use clap::{Parser, Subcommand};
|
||||||
use guppy::graph::DependencyDirection;
|
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)]
|
#[derive(Parser)]
|
||||||
pub struct GpuiArgs {
|
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
|
/// Perform a dry-run and wait for user confirmation before each publish
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
@@ -20,18 +42,40 @@ pub struct GpuiArgs {
|
|||||||
skip_to: Option<String>,
|
skip_to: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_gpui() -> Result<()> {
|
pub fn run_gpui(command: GpuiCommand) -> Result<()> {
|
||||||
let graph = crate_graph::load_workspace_graph()?;
|
let graph = crate_graph::load_workspace_graph()?;
|
||||||
let crates = crate_graph::gpui_crates(&graph)?;
|
match command {
|
||||||
|
GpuiCommand::List(args) => {
|
||||||
println!("GPUI crate graph ({} crates):", crates.len());
|
let plan = publish_plan::build_publish_plan(&graph)?;
|
||||||
for package in crates.packages(DependencyDirection::Reverse) {
|
println!(
|
||||||
println!("{} ({})", package.name(), package.source());
|
"GPUI publish plan ({} crates, dependencies first):",
|
||||||
for link in package.direct_links().filter(|link| !link.dev_only()) {
|
plan.len()
|
||||||
match crates.contains(link.to().id()) {
|
);
|
||||||
Ok(true) => println!(" -> {}", link.to().name()),
|
for entry in &plan {
|
||||||
Ok(false) => {}
|
let package = entry.repository.publish_info(entry.package.name());
|
||||||
Err(e) => eprintln!(" error checking dependency {}: {}", link.to().name(), e),
|
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")]
|
#[allow(unused, reason = "retained while GPUI releases are reworked")]
|
||||||
fn run_publish_gpui(args: GpuiArgs) -> Result<()> {
|
fn run_publish_gpui(args: PublishGpuiArgs) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"Starting GPUI publish process{}...",
|
"Starting GPUI publish process{}...",
|
||||||
if args.dry_run { " (with dry-run)" } else { "" }
|
if args.dry_run { " (with dry-run)" } else { "" }
|
||||||
@@ -363,6 +407,29 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_update_dependency_version_in_toml() {
|
fn test_update_dependency_version_in_toml() {
|
||||||
let input = indoc! {r#"
|
let input = indoc! {r#"
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use anyhow::{Context as _, Result, ensure};
|
use anyhow::{Context as _, Result, ensure};
|
||||||
use guppy::{
|
use guppy::{
|
||||||
MetadataCommand,
|
MetadataCommand,
|
||||||
graph::{DependencyDirection, ExternalSource, PackageGraph, PackageSet, PackageSource},
|
graph::{
|
||||||
|
DependencyDirection, ExternalSource, PackageGraph, PackageMetadata, PackageSet,
|
||||||
|
PackageSource,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
@@ -11,12 +14,16 @@ pub fn load_workspace_graph() -> Result<PackageGraph> {
|
|||||||
.context("failed to load the workspace crate graph")
|
.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
|
/// Returns all GPUI crates in the current workspace
|
||||||
pub fn gpui_crates(graph: &PackageGraph) -> Result<PackageSet<'_>> {
|
pub fn gpui_crates(graph: &PackageGraph) -> Result<PackageSet<'_>> {
|
||||||
let roots = graph
|
let roots = graph
|
||||||
.workspace()
|
.workspace()
|
||||||
.iter_by_name()
|
.iter_by_name()
|
||||||
.filter(|(name, _)| name.starts_with("gpui"))
|
.filter(|(_, package)| is_gpui_root(*package))
|
||||||
.map(|(_, package)| package.id())
|
.map(|(_, package)| package.id())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
ensure!(!roots.is_empty(), "no GPUI crates found in the workspace");
|
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 {
|
fn is_zed_fork(source: PackageSource<'_>) -> bool {
|
||||||
let Some(ExternalSource::Git { repository, .. }) = source.parse_external() else {
|
let Some(ExternalSource::Git { repository, .. }) = source.parse_external() else {
|
||||||
return false;
|
return false;
|
||||||
@@ -219,7 +249,9 @@ mod tests {
|
|||||||
},
|
},
|
||||||
FixturePackage {
|
FixturePackage {
|
||||||
name: "fork",
|
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 {
|
dependencies: &[FixtureDependency {
|
||||||
to: "collections",
|
to: "collections",
|
||||||
kind: Normal(Required),
|
kind: Normal(Required),
|
||||||
@@ -283,6 +315,83 @@ mod tests {
|
|||||||
assert!(crates.links(DependencyDirection::Reverse).all(|link| {
|
assert!(crates.links(DependencyDirection::Reverse).all(|link| {
|
||||||
names.contains(&link.from().name()) && names.contains(&link.to().name())
|
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(())
|
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(
|
#[allow(
|
||||||
clippy::disallowed_methods,
|
clippy::disallowed_methods,
|
||||||
reason = "This runs only in a CLI environment"
|
reason = "This runs only in a CLI environment"
|
||||||
)]
|
)]
|
||||||
fn parse_ref(value: &str) -> Result<GitSha, String> {
|
fn parse_ref(value: &str) -> Result<GitSha, String> {
|
||||||
const GIT_SHA_LENGTH: usize = 40;
|
let sha = value.parse::<GitSha>()?;
|
||||||
(value.len() == GIT_SHA_LENGTH)
|
let output = std::process::Command::new("git")
|
||||||
.then_some(value)
|
.args([
|
||||||
.ok_or_else(|| {
|
"rev-parse",
|
||||||
format!(
|
"--quiet",
|
||||||
"Git SHA has wrong length! \
|
"--verify",
|
||||||
Only SHAs with a full length of {GIT_SHA_LENGTH} are supported, found {len} characters.",
|
&format!("{}^{{commit}}", sha.as_ref()),
|
||||||
len = value.len()
|
])
|
||||||
)
|
.output()
|
||||||
})
|
.map_err(|_| "Failed to spawn Git command to verify SHA".to_owned())?;
|
||||||
.and_then(|value| {
|
if !output.status.success() {
|
||||||
let mut tmp = [0; 4];
|
return Err(format!(
|
||||||
value
|
"SHA {} is not a valid Git SHA within this repository!",
|
||||||
.chars()
|
sha.as_ref()
|
||||||
.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())
|
}
|
||||||
})
|
Ok(sha)
|
||||||
.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()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
|||||||
Reference in new issue
Block a user