Skip to content
Merged
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
73 changes: 55 additions & 18 deletions src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,16 @@ fn local_commands(config: &LoadedConfig, prefix: &str) -> Vec<CompletionSuggesti
if file.source != SourceKind::Local {
continue;
}
for (name, entry) in &file.commands {
if name.starts_with(prefix) {
map.insert(name.clone(), command_suggestion(name, entry));
match &file.scope {
FileScope::Root | FileScope::Namespace { .. } => {
for (name, entry) in &file.commands {
if name.starts_with(prefix) {
map.insert(name.clone(), command_suggestion(name, entry));
}
}
}
FileScope::Group { .. } | FileScope::NamespaceGroup { .. } => {
// Hidden from root commands
}
}
}
Expand Down Expand Up @@ -372,10 +379,18 @@ fn local_groups(config: &LoadedConfig, prefix: &str) -> Vec<CompletionSuggestion
if file.source != SourceKind::Local {
continue;
}
if let FileScope::Group { group } = &file.scope {
if group.starts_with(prefix) {
set.insert(group.clone());
match &file.scope {
FileScope::Group { group } => {
if group.starts_with(prefix) {
set.insert(group.clone());
}
}
FileScope::NamespaceGroup { group, .. } => {
if group.starts_with(prefix) {
set.insert(group.clone());
}
}
_ => {}
}
}
set.into_iter()
Expand Down Expand Up @@ -450,14 +465,26 @@ fn namespace_groups(
fn group_children(config: &LoadedConfig, group: &str, prefix: &str) -> Vec<CompletionSuggestion> {
let mut map = BTreeMap::new();
for file in &config.files {
if let FileScope::Group { group: file_alias } = &file.scope {
if file_alias == group {
for (name, entry) in &file.commands {
if name.starts_with(prefix) {
map.insert(name.clone(), command_suggestion(name, entry));
match &file.scope {
FileScope::Group { group: file_alias } => {
if file_alias == group {
for (name, entry) in &file.commands {
if name.starts_with(prefix) {
map.insert(name.clone(), command_suggestion(name, entry));
}
}
}
}
FileScope::NamespaceGroup { group: file_alias, .. } if file.source == SourceKind::Local => {
if file_alias == group {
for (name, entry) in &file.commands {
if name.starts_with(prefix) {
map.insert(name.clone(), command_suggestion(name, entry));
}
}
}
}
_ => {}
}
}
map.into_values().collect()
Expand Down Expand Up @@ -526,13 +553,21 @@ fn nested_from_group_scope(
let group = &path[0];
let command_name = &path[1];
let mut command = None;

// We reverse to prioritize overrides (though not fully applicable here, but consistent style)
for file in config.files.iter().rev() {
if let FileScope::Group { group: file_alias } = &file.scope {
if file_alias == group {
if let Some(candidate) = file.commands.get(command_name) {
command = Some(candidate);
break;
}
let matches = match &file.scope {
FileScope::Group { group: file_alias } => file_alias == group,
FileScope::NamespaceGroup { group: file_alias, .. } if file.source == SourceKind::Local => {
file_alias == group
}
_ => false,
};

if matches {
if let Some(candidate) = file.commands.get(command_name) {
command = Some(candidate);
break;
}
}
}
Expand Down Expand Up @@ -845,7 +880,9 @@ commands:
}],
};

let values = completion_suggestions(&config, &["run".to_string()]);
// Updated loop to respect mandatory group enforcement.
// Command "run" in group "ops" must be accessed via "ops run".
let values = completion_suggestions(&config, &["ops".to_string(), "run".to_string()]);
let names: Vec<String> = values.into_iter().map(|it| it.value).collect();
assert_eq!(names, vec!["start", "test"]);
}
Expand Down
44 changes: 42 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::{Component, Path, PathBuf},
process::Command,
};

use serde::Deserialize;
Expand Down Expand Up @@ -196,16 +197,19 @@ pub(crate) fn load_config() -> LoadedConfig {
let mut loaded = LoadedConfig::default();
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));

// Attempt git root detection FIRST
let project_root = detect_git_root(&cwd).unwrap_or(cwd.clone());

// Local project commands.
loaded
.files
.extend(load_local_file_configs(&cwd, SourceKind::Local));
.extend(load_local_file_configs(&project_root, SourceKind::Local));

// Global installed directories. No implicit namespace here:
// files without namespace/group stay as direct global commands.
let installed_dirs = load_installed_directories();
for directory in installed_dirs {
if directory == cwd {
if directory == project_root {
continue;
}
loaded
Expand All @@ -216,6 +220,42 @@ pub(crate) fn load_config() -> LoadedConfig {
loaded
}

fn detect_git_root(cwd: &Path) -> Option<PathBuf> {
if !cwd.exists() {
return None;
}

let output = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.current_dir(cwd)
.stderr(std::process::Stdio::null())
.output()
.ok()?;

if !output.status.success() {
return None;
}

let path_str = String::from_utf8(output.stdout).ok()?;
let path = PathBuf::from(path_str.trim());

// Check if git root has ANY fire config.
let config_files = discover_config_files(&path);
if config_files.is_empty() {
return None;
}

// Check if any of the root config files defines a namespace.
let raw = parse_raw_files(&config_files);
let has_explicit_namespace = select_directory_explicit_namespace(&raw).is_some();

if has_explicit_namespace {
return Some(path);
}

None
}

fn load_local_file_configs(cwd: &Path, source: SourceKind) -> Vec<FileConfig> {
load_directory_file_configs(cwd, source, DefaultNamespaceMode::DirectoryFallback)
}
Expand Down
47 changes: 35 additions & 12 deletions src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,15 @@ fn local_commands(config: &LoadedConfig) -> Vec<(String, Option<String>)> {
if file.source != crate::config::SourceKind::Local {
continue;
}
for (name, entry) in &file.commands {
map.insert(name.clone(), optional_description(entry));
match &file.scope {
FileScope::Root | FileScope::Namespace { .. } => {
for (name, entry) in &file.commands {
map.insert(name.clone(), optional_description(entry));
}
}
FileScope::Group { .. } | FileScope::NamespaceGroup { .. } => {
// Commands inside groups are NOT displayed at root level.
}
}
}
map.into_iter().collect()
Expand Down Expand Up @@ -146,8 +153,14 @@ fn namespaces(config: &LoadedConfig) -> Vec<(String, Option<String>)> {
fn groups(config: &LoadedConfig) -> Vec<(String, Option<String>)> {
let mut map = BTreeMap::new();
for file in &config.files {
if let FileScope::Group { group } = &file.scope {
map.insert(group.clone(), None);
match &file.scope {
FileScope::Group { group } => {
map.insert(group.clone(), None);
}
FileScope::NamespaceGroup { group, .. } if file.source == crate::config::SourceKind::Local => {
map.insert(group.clone(), None);
}
_ => {}
}
}
map.into_iter().collect()
Expand Down Expand Up @@ -219,11 +232,17 @@ fn namespace_groups(config: &LoadedConfig, namespace: &str) -> Vec<(String, Opti
fn group_commands(config: &LoadedConfig, group: &str) -> Vec<(String, Option<String>)> {
let mut map = BTreeMap::new();
for file in &config.files {
if let FileScope::Group { group: file_alias } = &file.scope {
if file_alias == group {
for (name, entry) in &file.commands {
map.insert(name.clone(), optional_description(entry));
}
let is_match = match &file.scope {
FileScope::Group { group: file_alias } => file_alias == group,
FileScope::NamespaceGroup { group: file_alias, .. } if file.source == crate::config::SourceKind::Local => {
file_alias == group
}
_ => false,
};

if is_match {
for (name, entry) in &file.commands {
map.insert(name.clone(), optional_description(entry));
}
}
}
Expand Down Expand Up @@ -272,9 +291,13 @@ fn has_namespace(config: &LoadedConfig, namespace: &str) -> bool {
}

fn has_root_group(config: &LoadedConfig, group: &str) -> bool {
config.files.iter().any(
|file| matches!(&file.scope, FileScope::Group { group: file_alias } if file_alias == group),
)
config.files.iter().any(|file| match &file.scope {
FileScope::Group { group: file_alias } => file_alias == group,
FileScope::NamespaceGroup { group: file_alias, .. } if file.source == crate::config::SourceKind::Local => {
file_alias == group
}
_ => false,
})
}

fn has_namespace_prefix(config: &LoadedConfig, namespace: &str, group: &str) -> bool {
Expand Down
Loading