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 3 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: 109 additions & 11 deletions crates/turborepo-lib/src/query/file.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use std::sync::Arc;

use async_graphql::Object;
use async_graphql::{Object, SimpleObject, Union};
use itertools::Itertools;
use turbo_trace::Tracer;
use turbopath::AbsoluteSystemPathBuf;
use turborepo_repository::{change_mapper::ChangeMapper, package_graph::PackageNode};

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

pub struct File {
run: Arc<Run>,
Expand All @@ -18,26 +23,119 @@ impl File {
}
}

#[derive(Clone, Debug, SimpleObject)]
struct All {
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 => {
Ok(Some(PackageMapping::All(All {
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) -> String {
self.path.to_string()
}

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

async fn absolute_path(&self) -> Result<String, Error> {
Ok(self.path.to_string())
/// 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()
.map(|(name, _)| Package {
run: self.run.clone(),
name: name.clone(),
})
.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) -> Result<Vec<File>, Error> {
async fn file_dependencies(&self) -> Result<Array<File>, Error> {
let tracer = Tracer::new(
self.run.repo_root().to_owned(),
vec![self.path.clone()],
Expand Down
75 changes: 73 additions & 2 deletions crates/turborepo-lib/src/query/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
mod file;
mod package;

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

use async_graphql::{http::GraphiQLSource, *};
use async_graphql_axum::GraphQL;
Expand All @@ -14,8 +14,10 @@ use tokio::{net::TcpListener, select};
use turbo_trace::TraceError;
use turbopath::AbsoluteSystemPathBuf;
use turborepo_repository::package_graph::PackageName;
use turborepo_scm::git::ChangedFiles;

use crate::{
global_deps_package_change_mapper,
query::file::File,
run::{builder::RunBuilder, Run},
signal::SignalHandler,
Expand All @@ -39,6 +41,10 @@ pub enum Error {
Run(#[from] crate::run::Error),
#[error(transparent)]
Path(#[from] turbopath::PathError),
#[error(transparent)]
ChangeMapper(#[from] global_deps_package_change_mapper::Error),
#[error(transparent)]
Scm(#[from] turborepo_scm::Error),
}

pub struct Query {
Expand All @@ -51,19 +57,37 @@ impl Query {
}
}

#[derive(Debug, SimpleObject)]
#[derive(Debug, SimpleObject, Default)]
#[graphql(concrete(name = "Files", params(File)))]
#[graphql(concrete(name = "Packages", params(Package)))]
pub struct Array<T: OutputType> {
items: Vec<T>,
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 @@ -310,6 +334,53 @@ impl Query {
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>,
allow_unknown_objects: 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 allow_unknown_objects = allow_unknown_objects.unwrap_or(false);

let repo_root = self.run.repo_root();
let change_result = self.run.scm().changed_files(
repo_root,
base,
head,
include_uncommitted,
allow_unknown_objects,
merge_base,
)?;

let files = match change_result {
// Shouldn't happen since we set `allow_unknown_objects` to false
ChangedFiles::All => unreachable!(),
ChangedFiles::Some(files) => files,
};

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

/// 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 @@ -40,7 +40,6 @@ pub enum PackageChanges {

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

ignore_patterns: Vec<String>,
package_detector: PD,
}
Expand All @@ -64,6 +63,10 @@ impl<'a, PD: PackageChangeMapper> ChangeMapper<'a, PD> {
.any(|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