Skip to content

Commit

Permalink
feat(turborepo): implement --affected flag (#8884)
Browse files Browse the repository at this point in the history
### Description

Implements the `--affected` flag. Does some light refactoring to SCM to
ensure that we're getting the unstaged changes in `--affected`.

### Testing Instructions

<!--
  Give a quick description of steps to test your changes.
-->
  • Loading branch information
NicholasLYang authored Aug 6, 2024
1 parent 50a7aea commit 672e2f2
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 73 deletions.
5 changes: 5 additions & 0 deletions crates/turborepo-lib/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,11 @@ pub struct ExecutionArgs {
#[clap(short = 'F', long, group = "scope-filter-group")]
pub filter: Vec<String>,

/// Run only tasks that are affected by changes between
/// the current branch and `main`
#[clap(long, group = "scope-filter-group")]
pub affected: bool,

/// Set type of process output logging. Use "full" to show
/// all output. Use "hash-only" to show only turbo-computed
/// task hashes. Use "new-only" to show only new output with
Expand Down
4 changes: 4 additions & 0 deletions crates/turborepo-lib/src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ struct ConfigOutput<'a> {
package_manager: PackageManager,
daemon: Option<bool>,
env_mode: EnvMode,
scm_base: &'a str,
scm_head: &'a str,
}

pub async fn run(base: CommandBase) -> Result<(), cli::Error> {
Expand Down Expand Up @@ -51,6 +53,8 @@ pub async fn run(base: CommandBase) -> Result<(), cli::Error> {
package_manager: *package_manager,
daemon: config.daemon,
env_mode: config.env_mode(),
scm_base: config.scm_base(),
scm_head: config.scm_head(),
})?
);
Ok(())
Expand Down
22 changes: 22 additions & 0 deletions crates/turborepo-lib/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ pub struct ConfigurationOptions {
pub(crate) daemon: Option<bool>,
#[serde(rename = "envMode")]
pub(crate) env_mode: Option<EnvMode>,
pub(crate) scm_base: Option<String>,
pub(crate) scm_head: Option<String>,
}

#[derive(Default)]
Expand Down Expand Up @@ -285,6 +287,14 @@ impl ConfigurationOptions {
self.ui.unwrap_or(UIMode::Stream)
}

pub fn scm_base(&self) -> &str {
self.scm_base.as_deref().unwrap_or("main")
}

pub fn scm_head(&self) -> &str {
self.scm_head.as_deref().unwrap_or("HEAD")
}

pub fn allow_no_package_manager(&self) -> bool {
self.allow_no_package_manager.unwrap_or_default()
}
Expand Down Expand Up @@ -371,6 +381,8 @@ fn get_env_var_config(
turbo_mapping.insert(OsString::from("turbo_daemon"), "daemon");
turbo_mapping.insert(OsString::from("turbo_env_mode"), "env_mode");
turbo_mapping.insert(OsString::from("turbo_preflight"), "preflight");
turbo_mapping.insert(OsString::from("turbo_scm_base"), "scm_base");
turbo_mapping.insert(OsString::from("turbo_scm_head"), "scm_head");

// We do not enable new config sources:
// turbo_mapping.insert(String::from("turbo_signature"), "signature"); // new
Expand Down Expand Up @@ -489,6 +501,8 @@ fn get_env_var_config(
team_slug: output_map.get("team_slug").cloned(),
team_id: output_map.get("team_id").cloned(),
token: output_map.get("token").cloned(),
scm_base: output_map.get("scm_base").cloned(),
scm_head: output_map.get("scm_head").cloned(),

// Processed booleans
signature,
Expand Down Expand Up @@ -553,6 +567,8 @@ fn get_override_env_var_config(
team_slug: None,
team_id: output_map.get("team_id").cloned(),
token: output_map.get("token").cloned(),
scm_base: None,
scm_head: None,

signature: None,
preflight: None,
Expand Down Expand Up @@ -794,6 +810,12 @@ impl TurborepoConfigBuilder {
if let Some(env_mode) = current_source_config.env_mode {
acc.env_mode = Some(env_mode);
}
if let Some(scm_base) = current_source_config.scm_base {
acc.scm_base = Some(scm_base);
}
if let Some(scm_head) = current_source_config.scm_head {
acc.scm_head = Some(scm_head);
}

acc
})
Expand Down
31 changes: 31 additions & 0 deletions crates/turborepo-lib/src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ impl Opts {
cmd.push_str(pattern);
}

if self.scope_opts.affected_range.is_some() {
cmd.push_str(" --affected");
}

if self.run_opts.parallel {
cmd.push_str(" --parallel");
}
Expand Down Expand Up @@ -300,6 +304,7 @@ pub struct ScopeOpts {
pub pkg_inference_root: Option<AnchoredSystemPathBuf>,
pub global_deps: Vec<String>,
pub filter_patterns: Vec<String>,
pub affected_range: Option<(String, String)>,
}

impl<'a> TryFrom<OptsInputs<'a>> for ScopeOpts {
Expand All @@ -313,9 +318,16 @@ impl<'a> TryFrom<OptsInputs<'a>> for ScopeOpts {
.map(AnchoredSystemPathBuf::from_raw)
.transpose()?;

let affected_range = inputs.execution_args.affected.then(|| {
let scm_base = inputs.config.scm_base();
let scm_head = inputs.config.scm_head();
(scm_base.to_string(), scm_head.to_string())
});

Ok(Self {
global_deps: inputs.execution_args.global_deps.clone(),
pkg_inference_root,
affected_range,
filter_patterns: inputs.execution_args.filter.clone(),
})
}
Expand Down Expand Up @@ -390,6 +402,7 @@ mod test {
parallel: bool,
continue_on_error: bool,
dry_run: Option<DryRunMode>,
affected: Option<(String, String)>,
}

#[test_case(TestCaseOpts {
Expand Down Expand Up @@ -452,6 +465,23 @@ mod test {
},
"turbo run build --filter=my-app --dry=json"
)]
#[test_case (
TestCaseOpts {
filter_patterns: vec!["my-app".to_string()],
tasks: vec!["build".to_string()],
affected: Some(("HEAD".to_string(), "my-branch".to_string())),
..Default::default()
},
"turbo run build --filter=my-app --affected"
)]
#[test_case (
TestCaseOpts {
tasks: vec!["build".to_string()],
affected: Some(("HEAD".to_string(), "my-branch".to_string())),
..Default::default()
},
"turbo run build --affected"
)]
fn test_synthesize_command(opts_input: TestCaseOpts, expected: &str) {
let run_opts = RunOpts {
tasks: opts_input.tasks,
Expand Down Expand Up @@ -479,6 +509,7 @@ mod test {
pkg_inference_root: None,
global_deps: vec![],
filter_patterns: opts_input.filter_patterns,
affected_range: opts_input.affected,
};
let opts = Opts {
run_opts,
Expand Down
24 changes: 22 additions & 2 deletions crates/turborepo-lib/src/run/scope/change_detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use turborepo_repository::{
change_mapper::{ChangeMapper, DefaultPackageChangeMapper, LockfileChange, PackageChanges},
package_graph::{PackageGraph, PackageName},
};
use turborepo_scm::SCM;
use turborepo_scm::{git::ChangedFiles, SCM};

use crate::{
global_deps_package_change_mapper::{Error, GlobalDepsPackageChangeMapper},
Expand All @@ -19,6 +19,8 @@ pub trait GitChangeDetector {
&self,
from_ref: &str,
to_ref: Option<&str>,
include_uncommitted: bool,
allow_unknown_objects: bool,
) -> Result<HashSet<PackageName>, ResolutionError>;
}

Expand Down Expand Up @@ -88,10 +90,28 @@ impl<'a> GitChangeDetector for ScopeChangeDetector<'a> {
&self,
from_ref: &str,
to_ref: Option<&str>,
include_uncommitted: bool,
allow_unknown_objects: bool,
) -> Result<HashSet<PackageName>, ResolutionError> {
let mut changed_files = HashSet::new();
if !from_ref.is_empty() {
changed_files = self.scm.changed_files(self.turbo_root, from_ref, to_ref)?;
changed_files = match self.scm.changed_files(
self.turbo_root,
from_ref,
to_ref,
include_uncommitted,
allow_unknown_objects,
)? {
ChangedFiles::All => {
debug!("all packages changed");
return Ok(self
.pkg_graph
.packages()
.map(|(name, _)| name.to_owned())
.collect());
}
ChangedFiles::Some(changed_files) => changed_files,
}
}

let lockfile_contents = self.get_lockfile_contents(from_ref, &changed_files);
Expand Down
46 changes: 33 additions & 13 deletions crates/turborepo-lib/src/run/scope/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,11 @@ impl<'a, T: GitChangeDetector> FilterResolver<'a, T> {
/// It applies the following rules:
pub(crate) fn resolve(
&self,
affected: &Option<(String, String)>,
patterns: &[String],
) -> Result<(HashSet<PackageName>, bool), ResolutionError> {
// inference is None only if we are in the root
let is_all_packages = patterns.is_empty() && self.inference.is_none();
let is_all_packages = patterns.is_empty() && self.inference.is_none() && affected.is_none();

let filter_patterns = if is_all_packages {
// return all packages in the workspace
Expand All @@ -176,21 +177,34 @@ impl<'a, T: GitChangeDetector> FilterResolver<'a, T> {
.map(|(name, _)| name.to_owned())
.collect()
} else {
self.get_packages_from_patterns(patterns)?
self.get_packages_from_patterns(affected, patterns)?
};

Ok((filter_patterns, is_all_packages))
}

fn get_packages_from_patterns(
&self,
affected: &Option<(String, String)>,
patterns: &[String],
) -> Result<HashSet<PackageName>, ResolutionError> {
let selectors = patterns
let mut selectors = patterns
.iter()
.map(|pattern| TargetSelector::from_str(pattern))
.collect::<Result<Vec<_>, _>>()?;

if let Some((from_ref, to_ref)) = affected {
selectors.push(TargetSelector {
git_range: Some(GitRange {
from_ref: from_ref.to_string(),
to_ref: Some(to_ref.to_string()),
include_uncommitted: true,
allow_unknown_objects: true,
}),
..Default::default()
});
}

self.get_filtered_packages(selectors)
}

Expand Down Expand Up @@ -534,8 +548,12 @@ impl<'a, T: GitChangeDetector> FilterResolver<'a, T> {
&self,
git_range: &GitRange,
) -> Result<HashSet<PackageName>, ResolutionError> {
self.change_detector
.changed_packages(&git_range.from_ref, git_range.to_ref.as_deref())
self.change_detector.changed_packages(
&git_range.from_ref,
git_range.to_ref.as_deref(),
git_range.include_uncommitted,
git_range.allow_unknown_objects,
)
}

fn match_package_names_to_vertices(
Expand Down Expand Up @@ -1105,7 +1123,7 @@ mod test {
#[test_case(
vec![
TargetSelector {
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None }),
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None, ..Default::default() }),
..Default::default()
}
],
Expand All @@ -1115,7 +1133,7 @@ mod test {
#[test_case(
vec![
TargetSelector {
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None }),
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None, ..Default::default() }),
parent_dir: Some(AnchoredSystemPathBuf::try_from(".").unwrap()),
..Default::default()
}
Expand All @@ -1126,7 +1144,7 @@ mod test {
#[test_case(
vec![
TargetSelector {
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None }),
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None, ..Default::default() }),
parent_dir: Some(AnchoredSystemPathBuf::try_from("package-2").unwrap()),
..Default::default()
}
Expand All @@ -1137,7 +1155,7 @@ mod test {
#[test_case(
vec![
TargetSelector {
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None }),
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None, ..Default::default() }),
name_pattern: "package-2*".to_string(),
..Default::default()
}
Expand All @@ -1148,7 +1166,7 @@ mod test {
#[test_case(
vec![
TargetSelector {
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None }),
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None, ..Default::default() }),
name_pattern: "package-1".to_string(),
match_dependencies: true,
..Default::default()
Expand All @@ -1160,7 +1178,7 @@ mod test {
#[test_case(
vec![
TargetSelector {
git_range: Some(GitRange { from_ref: "HEAD~2".to_string(), to_ref: None }),
git_range: Some(GitRange { from_ref: "HEAD~2".to_string(), to_ref: None, ..Default::default() }),
..Default::default()
}
],
Expand All @@ -1170,7 +1188,7 @@ mod test {
#[test_case(
vec![
TargetSelector {
git_range: Some(GitRange { from_ref: "HEAD~2".to_string(), to_ref: Some("HEAD~1".to_string()) }),
git_range: Some(GitRange { from_ref: "HEAD~2".to_string(), to_ref: Some("HEAD~1".to_string()), ..Default::default() }),
..Default::default()
}
],
Expand All @@ -1180,7 +1198,7 @@ mod test {
#[test_case(
vec![
TargetSelector {
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None }),
git_range: Some(GitRange { from_ref: "HEAD~1".to_string(), to_ref: None, ..Default::default() }),
parent_dir: Some(AnchoredSystemPathBuf::try_from("package-*").unwrap()),
match_dependencies: true, ..Default::default()
}
Expand Down Expand Up @@ -1234,6 +1252,8 @@ mod test {
&self,
from: &str,
to: Option<&str>,
_include_uncommitted: bool,
_allow_unknown_objects: bool,
) -> Result<HashSet<PackageName>, ResolutionError> {
Ok(self
.0
Expand Down
2 changes: 1 addition & 1 deletion crates/turborepo-lib/src/run/scope/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ pub fn resolve_packages(
scm,
root_turbo_json,
)?
.resolve(&opts.get_filters())
.resolve(&opts.affected_range, &opts.get_filters())
}
Loading

0 comments on commit 672e2f2

Please sign in to comment.