Skip to content

Commit

Permalink
Project graph and Nx implicit dependencies (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
DenysVuika authored Nov 16, 2024
1 parent d72b387 commit 1195847
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 52 deletions.
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ serde_json = "1.0.132"
serde_yaml = "0.9.34"
serde_with = "3.11.0"
glob = "0.3.1"
tokio = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["full"] }
petgraph = "0.6.5"
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ A tool to find affected files or projects in a git repository and run commands o
- determine affected files or projects for a git repository
- view affected files or projects
- run commands on affected files or projects
- redirects variables from `.env` files to the commands
- support `.env` files for the commands
- supports [Nx](https://nx.dev/) monorepos
- `implicitDependencies` via the `project.json` files

## Installation

Expand All @@ -27,7 +29,7 @@ Supported platforms:

- Linux (x86_64)
- Linux (aarch64)
- macOS (x86_64)
- macOS (x86_64, unsigned)

## Setup

Expand Down Expand Up @@ -70,6 +72,17 @@ Options:
-h, --help Print help
```

### Commands

- `init` - Initialize the configuration file
- `view` - View affected files or projects
- `files` - List affected files
- `projects` - List affected projects
- `tasks` - List defined tasks
- `run [task]` - Run a task on affected files or projects

For more information on a command, use the `help` command.

### Example

For the feature branch checked out, and the main branch is `develop`:
Expand Down
35 changes: 35 additions & 0 deletions src/graph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use crate::nx::NxProject;
use crate::projects::Project;
use anyhow::Result;
use petgraph::Graph;
use std::path::Path;

pub fn build_graph(workspace_root: &Path, project_paths: &[String]) -> Result<Graph<String, ()>> {
let mut graph = Graph::new();
let projects: Vec<NxProject> = project_paths
.iter()
.map(|path| NxProject::load(workspace_root, path))
.collect::<Result<Vec<NxProject>>>()?;

for project in projects {
let project_name = project.name().unwrap_or("Unnamed");
let project_node = graph.add_node(project_name.to_string());

if let Some(dependencies) = project.implicit_dependencies {
dependencies.iter().for_each(|dependency| {
let dependency_node = graph.add_node(dependency.to_string());
graph.add_edge(project_node, dependency_node, ());
});
}

// let project_dependencies = project.dependencies();
// for dependency in project_dependencies {
// let dependency_name = dependency.name().unwrap_or("Unnamed");
// let dependency_node = graph.add_node(dependency_name.to_string());
//
// graph.add_edge(project_node, dependency_node, 1);
// }
}

Ok(graph)
}
57 changes: 21 additions & 36 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
mod config;
pub mod graph;
pub mod logger;
mod node;
pub mod nx;
mod project;
mod projects;
pub mod tasks;
mod utils;

use crate::project::Project;
use crate::utils::parse_workspace;
use crate::projects::{is_project_dir, Project};
use crate::utils::inspect_workspace;
use anyhow::{bail, Context, Result};
pub use config::Config;
use git2::{BranchType, DiffOptions, Repository};
Expand Down Expand Up @@ -75,46 +76,30 @@ pub fn get_affected_files(repo: &Repository, config: &Config) -> Result<Vec<Stri
Ok(result)
}

fn is_project_dir(path: &Path) -> bool {
path.is_dir()
&& (
path.join("project.json").is_file() || path.join("package.json").is_file()
// || path.join("Cargo.toml").is_file()
)
}

// TODO: provide a way to specify the display options: name as folder, package.json, project.json, etc.
pub fn list_affected_projects(
pub fn get_affected_projects(
workspace_root: &PathBuf,
repo: &Repository,
config: &Config,
) -> Result<Vec<String>> {
let projects = parse_workspace(workspace_root, is_project_dir)?;
let mut affected_projects = HashSet::new();

if !projects.is_empty() {
let affected_files: HashSet<_> = get_affected_files(repo, config)?.into_iter().collect();
// Check if any of the affected files are in the projects
for project in projects {
if affected_files.iter().any(|file| file.starts_with(&project)) {
affected_projects.insert(project);
} else {
debug!("Skipping project '{}'", project);
}
}
let affected_files: HashSet<_> = get_affected_files(repo, config)?.into_iter().collect();
if affected_files.is_empty() {
return Ok(vec![]);
}

Ok(affected_projects.into_iter().collect())
}

pub fn list_all_projects(workspace_root: &PathBuf) -> Result<()> {
let filter_fn = |path: &Path| path.is_dir() && path.join("project.json").is_file();
let projects = parse_workspace(workspace_root, filter_fn)?;

for project in projects {
println!("{}", project);
let mut projects = inspect_workspace(workspace_root, is_project_dir)?;
if projects.is_empty() {
return Ok(vec![]);
}
Ok(())
projects.retain(|project| {
if affected_files.iter().any(|file| file.starts_with(project)) {
true
} else {
debug!("Skipping project '{}'", project);
false
}
});

Ok(projects)
}

pub fn get_project(workspace_root: &Path, project_path: &str) -> Result<Box<dyn Project>> {
Expand Down
45 changes: 35 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use affected::logger::init_logger;
use affected::tasks;
use affected::{get_affected_files, get_project, list_affected_projects, Config};
use anyhow::{bail, Result};
use affected::{get_affected_files, get_affected_projects, Config};
use anyhow::Result;
use clap::{Parser, Subcommand};
use dotenvy::dotenv;
use git2::Repository;
Expand Down Expand Up @@ -102,15 +102,40 @@ async fn main() -> Result<()> {
}
}
ViewCommands::Projects => {
let project_paths = list_affected_projects(&workspace_root, &repo, &config)?;
for project_path in project_paths {
let project = get_project(&workspace_root, &project_path)?;
let name = match project.name() {
Some(name) => name,
None => bail!("Project name is not defined"),
};
println!("{}", name);
let project_paths = get_affected_projects(&workspace_root, &repo, &config)?;
if project_paths.is_empty() {
println!("No projects affected");
return Ok(());
}

let graph = affected::graph::build_graph(&workspace_root, &project_paths)?;

if graph.node_count() == 0 {
println!("No projects affected");
return Ok(());
}

let mut printed_nodes = std::collections::HashSet::new();

for node_index in graph.node_indices() {
let project_name = &graph[node_index];
printed_nodes.insert(project_name);
debug!("{}", project_name);
}

for edge in graph.edge_indices() {
let (source, target) = graph.edge_endpoints(edge).unwrap();
let source_name = &graph[source];
let target_name = &graph[target];
debug!("{} -> (implicit) -> {}", source_name, target_name);
printed_nodes.insert(target_name);
}

for node in printed_nodes {
println!("{}", node);
}

// println!("{:?}", graph);
}
ViewCommands::Tasks => {
if let Some(tasks) = &config.tasks {
Expand Down
2 changes: 1 addition & 1 deletion src/node/node_project.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::project::Project;
use crate::projects::Project;
use anyhow::Result;
use serde::Deserialize;
use std::fs;
Expand Down
2 changes: 1 addition & 1 deletion src/nx/nx_project.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::project::Project;
use crate::projects::Project;
use anyhow::Result;
use serde::Deserialize;
use std::fs;
Expand Down
8 changes: 8 additions & 0 deletions src/project.rs → src/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ pub trait Project {
where
Self: Sized;
}

pub fn is_project_dir(path: &Path) -> bool {
path.is_dir()
&& (
path.join("project.json").is_file() || path.join("package.json").is_file()
// || path.join("Cargo.toml").is_file()
)
}
2 changes: 1 addition & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use ignore::WalkBuilder;
use std::path::{Path, PathBuf};

pub fn parse_workspace<F>(workspace_root: &PathBuf, filter_fn: F) -> Result<Vec<String>>
pub fn inspect_workspace<F>(workspace_root: &PathBuf, filter_fn: F) -> Result<Vec<String>>
where
F: Fn(&Path) -> bool,
{
Expand Down

0 comments on commit 1195847

Please sign in to comment.