Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(query): map file to package #9174

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
120 changes: 110 additions & 10 deletions crates/turborepo-lib/src/query/file.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use std::sync::Arc;

use async_graphql::{Object, SimpleObject};
use async_graphql::{Object, SimpleObject, Union};
use camino::Utf8PathBuf;
use itertools::Itertools;
use swc_ecma_ast::EsVersion;
use swc_ecma_parser::{EsSyntax, Syntax, TsSyntax};
use turbo_trace::Tracer;
use turbopath::AbsoluteSystemPathBuf;
use turborepo_repository::{
change_mapper::{ChangeMapper, GlobalDepsPackageChangeMapper},
package_graph::PackageNode,
};

use crate::{
query::{Array, Error},
query::{package::Package, Array, Error, PackageChangeReason},
run::Run,
};

Expand Down Expand Up @@ -135,23 +139,119 @@ impl TraceResult {
}
}

#[derive(SimpleObject)]
struct All {
reason: PackageChangeReason,
count: usize,
}

#[derive(Union)]
enum PackageMapping {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Union types in GraphQL are pretty annoying. Maybe I should change this to just return all the packages? I'm a little worried that could be too much data.

All(All),
Package(Package),
}

impl File {
fn get_package(&self) -> Result<Option<PackageMapping>, Error> {
let change_mapper = ChangeMapper::new(
self.run.pkg_dep_graph(),
vec![],
GlobalDepsPackageChangeMapper::new(
self.run.pkg_dep_graph(),
self.run
.root_turbo_json()
.global_deps
.iter()
.map(|dep| dep.as_str()),
)?,
);

// If the file is not in the repo, we can't get the package
let Ok(anchored_path) = self.run.repo_root().anchor(&self.path) else {
return Ok(None);
};

let package = change_mapper
.package_detector()
.detect_package(&anchored_path);

match package {
turborepo_repository::change_mapper::PackageMapping::All(reason) => {
Ok(Some(PackageMapping::All(All {
reason: reason.into(),
count: self.run.pkg_dep_graph().len(),
})))
}
turborepo_repository::change_mapper::PackageMapping::Package((package, _)) => {
Ok(Some(PackageMapping::Package(Package {
run: self.run.clone(),
name: package.name.clone(),
})))
}
turborepo_repository::change_mapper::PackageMapping::None => Ok(None),
}
}
}

#[Object]
impl File {
async fn contents(&self) -> Result<String, Error> {
let contents = self.path.read_to_string()?;
Ok(contents)
Ok(self.path.read_to_string()?)
}

async fn path(&self) -> Result<String, Error> {
Ok(self
.run
// This is `Option` because the file may not be in the repo
async fn path(&self) -> Option<String> {
self.run
.repo_root()
.anchor(&self.path)
.map(|path| path.to_string())?)
.ok()
.map(|path| path.to_string())
}

async fn absolute_path(&self) -> Result<String, Error> {
Ok(self.path.to_string())
async fn absolute_path(&self) -> String {
self.path.to_string()
}

async fn package(&self) -> Result<Option<PackageMapping>, Error> {
self.get_package()
}

/// Gets the affected packages for the file, i.e. all packages that depend
/// on the file.
async fn affected_packages(&self) -> Result<Array<Package>, Error> {
match self.get_package() {
Ok(Some(PackageMapping::All(_))) => Ok(self
.run
.pkg_dep_graph()
.packages()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be sorted?

.map(|(name, _)| Package {
run: self.run.clone(),
name: name.clone(),
})
.sorted_by(|a, b| a.name.cmp(&b.name))
.collect()),
Ok(Some(PackageMapping::Package(package))) => {
let node: PackageNode = PackageNode::Workspace(package.name.clone());
Ok(self
.run
.pkg_dep_graph()
.ancestors(&node)
.iter()
.map(|package| Package {
run: self.run.clone(),
name: package.as_package_name().clone(),
})
// Add the package itself to the list
.chain(std::iter::once(Package {
run: self.run.clone(),
name: package.name.clone(),
}))
.sorted_by(|a, b| a.name.cmp(&b.name))
.collect())
}
Ok(None) => Ok(Array::new()),
Err(e) => Err(e),
}
}

async fn dependencies(&self, depth: Option<usize>, ts_config: Option<String>) -> TraceResult {
Expand Down
130 changes: 101 additions & 29 deletions crates/turborepo-lib/src/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
mod server;
mod task;

use std::{io, sync::Arc};
use std::{borrow::Cow, io, sync::Arc};

use async_graphql::{http::GraphiQLSource, *};
use axum::{response, response::IntoResponse};
Expand Down Expand Up @@ -51,6 +51,10 @@
Resolution(#[from] crate::run::scope::filter::ResolutionError),
#[error("failed to parse file: {0:?}")]
Parse(swc_ecma_parser::error::Error),
#[error(transparent)]
ChangeMapper(#[from] turborepo_repository::change_mapper::Error),
#[error(transparent)]
Scm(#[from] turborepo_scm::Error),
}

pub struct RepositoryQuery {
Expand All @@ -63,7 +67,7 @@
}
}

#[derive(Debug, SimpleObject)]
#[derive(Debug, SimpleObject, Default)]
#[graphql(concrete(name = "RepositoryTasks", params(RepositoryTask)))]
#[graphql(concrete(name = "Packages", params(Package)))]
#[graphql(concrete(name = "ChangedPackages", params(ChangedPackage)))]
Expand All @@ -74,13 +78,29 @@
length: usize,
}

impl<T: OutputType> Array<T> {
pub fn new() -> Self {
Self {
items: Vec::new(),
length: 0,
}
}
}

impl<T: OutputType> FromIterator<T> for Array<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let items: Vec<_> = iter.into_iter().collect();
let length = items.len();
Self { items, length }
}
}

impl<T: OutputType> TypeName for Array<T> {
fn type_name() -> Cow<'static, str> {
Cow::Owned(format!("Array<{}>", T::type_name()))
}
}

#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)]
enum PackageFields {
Name,
Expand Down Expand Up @@ -400,41 +420,50 @@
InFilteredDirectory(InFilteredDirectory),
}

impl From<turborepo_repository::change_mapper::PackageInclusionReason> for PackageChangeReason {
fn from(value: turborepo_repository::change_mapper::PackageInclusionReason) -> Self {
match value {
turborepo_repository::change_mapper::PackageInclusionReason::All(
AllPackageChangeReason::GlobalDepsChanged { file },
) => PackageChangeReason::GlobalDepsChanged(GlobalDepsChanged {
file_path: file.to_string(),
}),
turborepo_repository::change_mapper::PackageInclusionReason::All(
AllPackageChangeReason::DefaultGlobalFileChanged { file },
) => PackageChangeReason::DefaultGlobalFileChanged(DefaultGlobalFileChanged {
file_path: file.to_string(),
}),
turborepo_repository::change_mapper::PackageInclusionReason::All(
AllPackageChangeReason::LockfileChangeDetectionFailed,
) => {
impl From<AllPackageChangeReason> for PackageChangeReason {
fn from(reason: AllPackageChangeReason) -> Self {
match reason {
AllPackageChangeReason::GlobalDepsChanged { file } => {
PackageChangeReason::GlobalDepsChanged(GlobalDepsChanged {
file_path: file.to_string(),
})
}
AllPackageChangeReason::DefaultGlobalFileChanged { file } => {
PackageChangeReason::DefaultGlobalFileChanged(DefaultGlobalFileChanged {
file_path: file.to_string(),
})
}

AllPackageChangeReason::LockfileChangeDetectionFailed => {
PackageChangeReason::LockfileChangeDetectionFailed(LockfileChangeDetectionFailed {
empty: false,
})
}
turborepo_repository::change_mapper::PackageInclusionReason::All(
AllPackageChangeReason::GitRefNotFound { from_ref, to_ref },
) => PackageChangeReason::GitRefNotFound(GitRefNotFound { from_ref, to_ref }),
turborepo_repository::change_mapper::PackageInclusionReason::All(
AllPackageChangeReason::LockfileChangedWithoutDetails,
) => {

AllPackageChangeReason::GitRefNotFound { from_ref, to_ref } => {
PackageChangeReason::GitRefNotFound(GitRefNotFound { from_ref, to_ref })
}

AllPackageChangeReason::LockfileChangedWithoutDetails => {
PackageChangeReason::LockfileChangedWithoutDetails(LockfileChangedWithoutDetails {
empty: false,
})
}
turborepo_repository::change_mapper::PackageInclusionReason::All(
AllPackageChangeReason::RootInternalDepChanged { root_internal_dep },
) => PackageChangeReason::RootInternalDepChanged(RootInternalDepChanged {
root_internal_dep: root_internal_dep.to_string(),
}),
AllPackageChangeReason::RootInternalDepChanged { root_internal_dep } => {
PackageChangeReason::RootInternalDepChanged(RootInternalDepChanged {
root_internal_dep: root_internal_dep.to_string(),
})
}
}
}
}

impl From<turborepo_repository::change_mapper::PackageInclusionReason> for PackageChangeReason {
fn from(value: turborepo_repository::change_mapper::PackageInclusionReason) -> Self {
match value {
turborepo_repository::change_mapper::PackageInclusionReason::All(reason) => {
PackageChangeReason::from(reason)
}
turborepo_repository::change_mapper::PackageInclusionReason::RootTask { task } => {
PackageChangeReason::RootTask(RootTask {
task_name: task.to_string(),
Expand Down Expand Up @@ -533,6 +562,49 @@
Ok(File::new(self.run.clone(), abs_path))
}

/// Get the files that have changed between the `base` and `head` commits.
///
/// # Arguments
///
/// * `base`: Defaults to `main` or `master`
/// * `head`: Defaults to `HEAD`
/// * `include_uncommitted`: Defaults to `true` if `head` is not provided
/// * `allow_unknown_objects`: Defaults to `false`
/// * `merge_base`: Defaults to `true`
///
/// returns: Result<Array<File>, Error>
async fn affected_files(
&self,
base: Option<String>,
head: Option<String>,
include_uncommitted: Option<bool>,
merge_base: Option<bool>,
) -> Result<Array<File>, Error> {
let base = base.as_deref();
let head = head.as_deref();
let include_uncommitted = include_uncommitted.unwrap_or_else(|| head.is_none());
let merge_base = merge_base.unwrap_or(true);

let repo_root = self.run.repo_root();
let change_result = self
.run
.scm()
.changed_files(
repo_root,
base,
head,
include_uncommitted,
false,
merge_base,
)?
.expect("set allow unknown objects to false");

Ok(change_result
.into_iter()
.map(|file| File::new(self.run.clone(), self.run.repo_root().resolve(&file)))
.collect())

Check failure on line 605 in crates/turborepo-lib/src/query/mod.rs

View workflow job for this annotation

GitHub Actions / Turborepo rust check

a value of type `query::Array<query::file::File>` cannot be built from an iterator over elements of type `std::result::Result<query::file::File, query::Error>`

Check failure on line 605 in crates/turborepo-lib/src/query/mod.rs

View workflow job for this annotation

GitHub Actions / Rust lints

a value of type `query::Array<query::file::File>` cannot be built from an iterator over elements of type `std::result::Result<query::file::File, query::Error>`

Check failure on line 605 in crates/turborepo-lib/src/query/mod.rs

View workflow job for this annotation

GitHub Actions / Turborepo Integration (ubuntu-latest)

a value of type `query::Array<query::file::File>` cannot be built from an iterator over elements of type `std::result::Result<query::file::File, query::Error>`

Check failure on line 605 in crates/turborepo-lib/src/query/mod.rs

View workflow job for this annotation

GitHub Actions / Turborepo Integration (macos-12)

a value of type `query::Array<query::file::File>` cannot be built from an iterator over elements of type `std::result::Result<query::file::File, query::Error>`

Check failure on line 605 in crates/turborepo-lib/src/query/mod.rs

View workflow job for this annotation

GitHub Actions / Turborepo Integration (windows-latest)

a value of type `query::Array<query::file::File>` cannot be built from an iterator over elements of type `std::result::Result<query::file::File, query::Error>`
}

/// Gets a list of packages that match the given filter
async fn packages(&self, filter: Option<PackagePredicate>) -> Result<Array<Package>, Error> {
let Some(filter) = filter else {
Expand Down
4 changes: 4 additions & 0 deletions crates/turborepo-paths/src/anchored_system_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ impl AnchoredSystemPath {
buf.unwrap_or_else(|_| panic!("anchored system path is relative: {}", self.0.as_str()))
}

pub fn extension(&self) -> Option<&str> {
self.0.extension()
}

pub fn join_component(&self, segment: &str) -> AnchoredSystemPathBuf {
debug_assert!(!segment.contains(std::path::MAIN_SEPARATOR));
AnchoredSystemPathBuf(self.0.join(segment))
Expand Down
5 changes: 4 additions & 1 deletion crates/turborepo-repository/src/change_mapper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ pub enum PackageChanges {

pub struct ChangeMapper<'a, PD> {
pkg_graph: &'a PackageGraph,

ignore_patterns: Vec<String>,
package_detector: PD,
}
Expand All @@ -114,6 +113,10 @@ impl<'a, PD: PackageChangeMapper> ChangeMapper<'a, PD> {
.find(|f| DEFAULT_GLOBAL_DEPS.iter().any(|dep| *dep == f.as_str()))
}

pub fn package_detector(&self) -> &dyn PackageChangeMapper {
&self.package_detector
}

pub fn changed_packages(
&self,
changed_files: HashSet<AnchoredSystemPathBuf>,
Expand Down
Loading
Loading