workspacer_cli/
topo.rs

1// ---------------- [ File: workspacer-cli/src/topo.rs ]
2crate::ix!();
3
4#[derive(Getters,Debug, StructOpt)]
5#[getset(get="pub")]
6pub struct TopoSubcommand {
7    /// Path to the workspace directory (required unless you're in the workspace root).
8    #[structopt(long = "path")]
9    workspace_path: Option<PathBuf>,
10
11    /// Optional crate name for either a "focus" subgraph or "internal deps" mode
12    #[structopt(long = "crate")]
13    crate_name: Option<String>,
14
15    /// If true, produce a layered ordering rather than a flat toposort
16    #[structopt(long = "layered")]
17    layered: bool,
18
19    /// If true, reverse the final ordering (or layering vector)
20    #[structopt(long = "reverse")]
21    reverse: bool,
22
23    /// If true, remove crates that fail the filter from the graph entirely
24    #[structopt(long = "remove-unwanted")]
25    remove_unwanted: bool,
26
27    /// Optional substring to exclude crates whose names contain it
28    #[structopt(long = "exclude-substr", default_value="")]
29    exclude_substring: String,
30
31    /// If true, include external (third-party) crates in workspace/focus queries
32    #[structopt(long = "include-externals")]
33    include_externals: bool,
34
35    /// If true, interpret `--crate` as single-crate "internal dependencies" mode
36    #[structopt(long = "internal")]
37    internal_mode: bool,
38}
39
40impl TopoSubcommand {
41    pub async fn run(&self) -> Result<(), WorkspaceError> {
42        // Branching logic:
43
44        // 1) If user provided `--crate <NAME>`:
45        if let Some(ref c_name) = self.crate_name {
46            // A) If `--internal`, do single crate internal deps
47            if self.internal_mode {
48                self.run_single_crate_internal(c_name).await
49            }
50            // B) Else do partial subgraph "Focus" (ancestors up to crate)
51            else {
52                self.run_focus_workspace(c_name).await
53            }
54        }
55        // 2) Otherwise => entire workspace
56        else {
57            self.run_entire_workspace().await
58        }
59    }
60
61    // -----------------------------------------------------
62    // (A) Single Crate Internal Dependencies
63    // -----------------------------------------------------
64    async fn run_single_crate_internal(&self, crate_name: &str) -> Result<(), WorkspaceError> {
65        trace!(
66            "TopoSubcommand::run_single_crate_internal => crate='{}', layered={}, reverse={}",
67            crate_name, self.layered, self.reverse
68        );
69        // We'll need a workspace path to locate that crate, or we can attempt to find it
70        // in the current working directory. Let's require `--path`.
71        let ws_path = self.get_workspace_path("single-crate internal deps")?;
72
73        let layered:         bool = *self.layered();
74        let reverse:         bool = *self.reverse();
75        let remove_unwanted: bool = *self.remove_unwanted();
76        let exclude_substring = self.exclude_substring().to_string();
77
78        let c_name_cloned = crate_name.to_string();
79
80        // 1) Load the workspace
81        run_with_workspace(Some(ws_path), /*skip_git_check=*/true, move |ws| {
82            // clone relevant fields
83            let layered_flag = layered;
84            let reverse_flag = reverse;
85            let rm_unwanted = remove_unwanted;
86            let excl_substr = exclude_substring.clone();
87
88            Box::pin(async move {
89                // 2) Attempt to find that crate's handle
90                let maybe_crate_arc = ws.find_crate_by_name(&c_name_cloned).await;
91                let crate_arc = match maybe_crate_arc {
92                    Some(arc) => arc,
93                    None => {
94                        let msg = format!("No crate named '{}' found in workspace", c_name_cloned);
95                        error!("{}", msg);
96                        return Err(WorkspaceError::CrateError(CrateError::CrateNotFoundInWorkspace {
97                            crate_name: c_name_cloned
98                        }));
99                    }
100                };
101                let handle = crate_arc.lock().await;
102
103                // 3) Build the topological config
104                let final_filter = if excl_substr.is_empty() {
105                    None
106                } else {
107                    Some(Arc::new(move |nm: &str| !nm.contains(&excl_substr))
108                         as Arc<dyn Fn(&str)->bool + Send + Sync>)
109                };
110
111                let mut config_builder = TopologicalSortConfigBuilder::default();
112                config_builder
113                    .layering_enabled(layered_flag)
114                    .reverse_order(reverse_flag)
115                    .remove_unwanted_from_graph(rm_unwanted)
116                    .filter_fn(final_filter);
117                let config = config_builder.build().unwrap();
118
119                // 4) Single-crate internal: layered or flat
120                if layered_flag {
121                    let layers = handle.layered_topological_order_upto_self(&config).await?;
122                    info!("CrateDeps => layered => crate='{}' => {} layers", handle.name(), layers.len());
123                    for (i, layer) in layers.iter().enumerate() {
124                        println!("Layer {} => {:?}", i, layer);
125                    }
126                } else {
127                    let sorted = handle.topological_sort_internal_deps(&config).await?;
128                    info!("CrateDeps => flat => crate='{}' => {:?}", handle.name(), sorted);
129                    for c in sorted {
130                        println!("{}", c);
131                    }
132                }
133                Ok(())
134            })
135        })
136        .await
137    }
138
139    // -----------------------------------------------------
140    // (B) Focus => partial workspace
141    // -----------------------------------------------------
142    async fn run_focus_workspace(&self, crate_name: &str) -> Result<(), WorkspaceError> {
143        trace!(
144            "TopoSubcommand::run_focus_workspace => crate='{}', layered={}, reverse={}, externals={}",
145            crate_name, self.layered, self.reverse, self.include_externals
146        );
147        let ws_path = self.get_workspace_path("focus subgraph")?;
148
149        // clone flags
150        let layered_flag = self.layered;
151        let reverse_flag = self.reverse;
152        let rm_unwanted = self.remove_unwanted;
153        let excl_substr = self.exclude_substring.clone();
154        let show_3p = self.include_externals;
155        let focus_crate = crate_name.to_string();
156
157        run_with_workspace(Some(ws_path), /*skip_git_check=*/true, move |ws| {
158            Box::pin(async move {
159                // gather known crates
160                let crate_arcs = ws.crates();
161                let mut known_set = HashSet::new();
162                for c_arc in crate_arcs {
163                    let locked = c_arc.lock().await;
164                    known_set.insert(locked.name().to_string());
165                }
166
167                let final_filter = Arc::new(move |nm: &str| {
168                    // skip externals if !show_3p
169                    if !show_3p && !known_set.contains(nm) {
170                        return false;
171                    }
172                    if !excl_substr.is_empty() && nm.contains(&excl_substr) {
173                        return false;
174                    }
175                    true
176                }) as Arc<dyn Fn(&str)->bool + Send + Sync>;
177
178                let mut config_builder = TopologicalSortConfigBuilder::default();
179                config_builder
180                    .layering_enabled(layered_flag)
181                    .reverse_order(reverse_flag)
182                    .remove_unwanted_from_graph(rm_unwanted)
183                    .filter_fn(Some(final_filter));
184                let config = config_builder.build().unwrap();
185
186                if layered_flag {
187                    let layers = ws.layered_topological_order_upto_crate(&config, &focus_crate).await?;
188                    info!("Focus layered => total {} layers => crate='{}'", layers.len(), focus_crate);
189                    for (i, layer) in layers.iter().enumerate() {
190                        println!("Layer {} => {:?}", i, layer);
191                    }
192                } else {
193                    let partial = ws.topological_order_upto_crate(&config, &focus_crate).await?;
194                    info!("Focus flat => partial => crate='{}': {:?}", focus_crate, partial);
195                    for c in partial {
196                        println!("{}", c);
197                    }
198                }
199                Ok(())
200            })
201        })
202        .await
203    }
204
205    // -----------------------------------------------------
206    // (C) Entire workspace
207    // -----------------------------------------------------
208    async fn run_entire_workspace(&self) -> Result<(), WorkspaceError> {
209        trace!(
210            "TopoSubcommand::run_entire_workspace => layered={}, reverse={}, externals={}",
211            self.layered, self.reverse, self.include_externals
212        );
213        let ws_path = self.get_workspace_path("workspace-level topo")?;
214
215        let layered_flag = self.layered;
216        let reverse_flag = self.reverse;
217        let rm_unwanted = self.remove_unwanted;
218        let excl_substr = self.exclude_substring.clone();
219        let show_3p = self.include_externals;
220
221        run_with_workspace(Some(ws_path), /*skip_git_check=*/true, move |ws| {
222            Box::pin(async move {
223                let crate_arcs = ws.crates();
224                let mut known_set = HashSet::new();
225                for c_arc in crate_arcs {
226                    let locked = c_arc.lock().await;
227                    known_set.insert(locked.name().to_string());
228                }
229
230                let final_filter = Arc::new(move |nm: &str| {
231                    if !show_3p && !known_set.contains(nm) {
232                        return false;
233                    }
234                    if !excl_substr.is_empty() && nm.contains(&excl_substr) {
235                        return false;
236                    }
237                    true
238                }) as Arc<dyn Fn(&str)->bool + Send + Sync>;
239
240                let mut config_builder = TopologicalSortConfigBuilder::default();
241                config_builder
242                    .layering_enabled(layered_flag)
243                    .reverse_order(reverse_flag)
244                    .remove_unwanted_from_graph(rm_unwanted)
245                    .filter_fn(Some(final_filter));
246                let config = config_builder.build().unwrap();
247
248                if layered_flag {
249                    let layered = ws.layered_topological_order_crate_names(&config).await?;
250                    info!("Workspace layering => total {} layers", layered.len());
251                    for (i, layer) in layered.iter().enumerate() {
252                        println!("Layer {} => {:?}", i, layer);
253                    }
254                } else {
255                    let sorted = ws.topological_order_crate_names(&config).await?;
256                    info!("Workspace flat => sorted crates: {:?}", sorted);
257                    for c in sorted {
258                        println!("{}", c);
259                    }
260                }
261                Ok(())
262            })
263        })
264        .await
265    }
266
267    // Helper to ensure we have a workspace path
268    fn get_workspace_path(&self, context: &str) -> Result<PathBuf, WorkspaceError> {
269        if let Some(ref p) = self.workspace_path {
270            Ok(p.clone())
271        } else {
272            Ok(PathBuf::from("."))
273        }
274    }
275}