Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
12 changes: 6 additions & 6 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ jobs:
name: Build Base Image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
with:
buildkitd-flags: --debug

- name: Build and (locally) load base image
uses: docker/build-push-action@v7
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
context: .
file: tests/Dockerfile.builder
Expand All @@ -52,15 +52,15 @@ jobs:
fail-fast: false

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
with:
buildkitd-flags: --debug

- name: Load cached builder image
uses: docker/build-push-action@v7
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
context: .
file: tests/Dockerfile.builder
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,25 @@ jobs:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master
with:
components: rustfmt
toolchain: 1.91.1
components: rustfmt, clippy

- name: Cache dependencies
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
Comment on lines +26 to 32
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Cache key should also key on the Rust toolchain version.

key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} does not include the rustc version. When you bump rust-toolchain.toml from 1.91.1 to a newer version in a future PR, the cached target/ will be reused even though it was built by an older rustc, which can cause confusing rebuild diagnostics or, worse, stale artifacts. Consider hashing rust-toolchain.toml (or the Cargo.toml rust-version) into the cache key.

Proposed change
-          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+          key: ${{ runner.os }}-cargo-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/rust.yml around lines 26 - 32, The cache key in the GitHub
Actions step (the `key` field currently set to `${{ runner.os }}-cargo-${{
hashFiles('**/Cargo.lock') }}`) does not include the Rust toolchain version and
can reuse artifacts built with a different rustc; update the `key` to also hash
the rust-toolchain file (or Cargo.toml rust-version) so toolchain bumps
invalidate the cache—specifically modify the `key` expression to include
`hashFiles('rust-toolchain.toml')` (or `hashFiles('**/Cargo.toml')` targeting
rust-version) alongside the existing Cargo.lock hash to ensure `target/` and
registry caches are version-aware.


- name: Check formatting
run: cargo fmt --all -- --check
- name: Lint
run: make lint

- name: Run tests
run: cargo test --verbose
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[package]
name = "dela"
version = "0.0.6"
rust-version = "1.91.1"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🌐 Web query:

Cargo rust-version field convention patch version MSRV

💡 Result:

The rust-version field in Cargo.toml specifies the Minimum Supported Rust Version (MSRV), the minimum Rust toolchain version supported by the package. It is an optional [package] key that Cargo uses to error on compilation if the toolchain is too old (unless --ignore-rust-version is used). Format: A bare semantic version number with at least one component (major, major.minor, or major.minor.patch), no semver operators or pre-release identifiers. Official docs use "1.56" (major.minor). Full patch like "1.56.1" is allowed but not shown in examples. Convention for patch version: Official Cargo documentation does not mandate including the patch level; major.minor is the standard and sufficient. Cargo treats rust-version with caret-like compatibility internally for resolution (e.g., "^package.rust-version" for deps), but the declared value is exact for checking the user's toolchain (>= declared version). Projects commonly use major.minor (e.g., stable-2 like 1.75 for current stable minus 2), updating minor when raising MSRV, often as a minor SemVer change in the crate (not breaking). Use tools like cargo-msrv to verify the actual MSRV matches the declared value. Support expectations: Dependencies should have at least one compatible version; Cargo resolver prefers compatible ones (with resolver.incompatible-rust-versions = "fallback" default in edition 2024). Policy recommendation: Define a support window (e.g., latest stable, or N-2 minors), test with cargo-msrv verify.

Citations:


Use rust-version = "1.91" (major.minor only, no patch version).

Including the patch version (1.91.1) causes cargo to reject users on Rust 1.91.0, even though the crate compiles fine on that version. The rust-version field checks toolchain >= declared version, so 1.91.0 < 1.91.1 incorrectly flags compliant users. The Cargo documentation convention and standard practice is major.minor only; the rust-toolchain.toml already pins the exact build version for contributors and CI.

Proposed change
-rust-version = "1.91.1"
+rust-version = "1.91"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rust-version = "1.91.1"
rust-version = "1.91"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 4, Update the Cargo.toml rust-version field from the
patch-pinned value to a major.minor only value: change the rust-version
declaration (the "rust-version" entry in Cargo.toml) from "1.91.1" to "1.91" so
Cargo's version check accepts Rust 1.91.0 while your rust-toolchain.toml
continues to pin the exact build for CI/contributors.

edition = "2024"
license = "MIT"
authors = ["Alexander Yankov"]
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ build:
@echo "Building dela..."
cargo build

lint:
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings

tests:
@echo "Running unit tests."
cargo test
Expand Down
3 changes: 3 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[toolchain]
channel = "1.91.1"
components = ["rustfmt", "clippy"]
Comment on lines +1 to +3
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Toolchain pin is fine; consider whether you want exact-patch or minor pinning.

channel = "1.91.1" will cause rustup to install/use exactly that patch version for every contributor, which is great for CI reproducibility but means contributors locally won't pick up patch fixes (e.g., a hypothetical 1.91.2) without a manifest bump. If you'd prefer to allow patch updates while still pinning the minor toolchain, channel = "1.91" is the common alternative. As-is is acceptable — just flagging the trade-off.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust-toolchain.toml` around lines 1 - 3, The toolchain is pinned to an exact
patch via channel = "1.91.1"; decide whether you want exact-patch or minor
pinning and update the channel string accordingly (e.g., change channel =
"1.91.1" to channel = "1.91" to allow patch updates), and ensure components =
["rustfmt","clippy"] remains unchanged; update the channel value in the rust
toolchain manifest where the [toolchain] section and channel key are defined.

19 changes: 9 additions & 10 deletions src/allowlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ pub fn is_task_allowed(task: &Task) -> Result<(bool, bool), String> {

// First pass: Check for deny entries (highest precedence)
for entry in &allowlist.entries {
if let AllowScope::Deny = entry.scope {
if path_matches(&task.file_path, &entry.path, true) {
return Ok((false, true));
}
if let AllowScope::Deny = entry.scope
&& path_matches(&task.file_path, &entry.path, true)
{
return Ok((false, true));
}
}

Expand All @@ -89,12 +89,11 @@ pub fn is_task_allowed(task: &Task) -> Result<(bool, bool), String> {
}
}
AllowScope::Task => {
if path_matches(&task.file_path, &entry.path, false) {
if let Some(ref tasks) = entry.tasks {
if tasks.contains(&task.name) {
return Ok((true, false));
}
}
if path_matches(&task.file_path, &entry.path, false)
&& let Some(ref tasks) = entry.tasks
&& tasks.contains(&task.name)
{
return Ok((true, false));
}
}
Comment on lines 91 to 98
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Triple let-chain is hard to follow; flatten it.

Beyond the fmt failure (pipeline error at line 90) and MSRV-1.88 requirement, the binding let Some(ref tasks) = entry.tasks chained inside && with two other booleans is not particularly readable. A small refactor avoids the let-chain altogether:

♻️ Suggested simplification
             AllowScope::Task => {
-                if path_matches(&task.file_path, &entry.path, false)
-                    && let Some(ref tasks) = entry.tasks
-                        && tasks.contains(&task.name) {
-                            return Ok((true, false));
-                        }
+                let task_listed = entry
+                    .tasks
+                    .as_ref()
+                    .is_some_and(|tasks| tasks.contains(&task.name));
+                if task_listed && path_matches(&task.file_path, &entry.path, false) {
+                    return Ok((true, false));
+                }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AllowScope::Task => {
if path_matches(&task.file_path, &entry.path, false) {
if let Some(ref tasks) = entry.tasks {
if tasks.contains(&task.name) {
if path_matches(&task.file_path, &entry.path, false)
&& let Some(ref tasks) = entry.tasks
&& tasks.contains(&task.name) {
return Ok((true, false));
}
}
}
}
AllowScope::Task => {
let task_listed = entry
.tasks
.as_ref()
.is_some_and(|tasks| tasks.contains(&task.name));
if task_listed && path_matches(&task.file_path, &entry.path, false) {
return Ok((true, false));
}
}
🧰 Tools
🪛 GitHub Actions: Rust CI

[error] 90-90: cargo fmt --all -- --check failed. Formatting diff detected in allowlist.rs for AllowScope::Task branch (indentation/brace placement). Run 'cargo fmt --all' to apply rustfmt formatting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/allowlist.rs` around lines 90 - 96, The AllowScope::Task match arm uses a
chained let-binding inside an && expression which is hard to read and breaks
fmt/MSRV; replace the chained condition with a flattened nested check: first
test path_matches(&task.file_path, &entry.path, false), then use an explicit if
let Some(ref tasks) = entry.tasks (or if let Some(tasks) = &entry.tasks) and
inside that check tasks.contains(&task.name) and return Ok((true, false)) when
all conditions hold; update the AllowScope::Task arm accordingly referencing
AllowScope::Task, path_matches, entry.tasks, task.file_path, and task.name.

AllowScope::Deny | AllowScope::Once => {
Expand Down
12 changes: 6 additions & 6 deletions src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ fn add_shell_integration(config_path: &PathBuf) -> Result<(), String> {
}

// Create parent directory if it doesn't exist (needed for PowerShell)
if let Some(parent) = config_path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
}
if let Some(parent) = config_path.parent()
&& !parent.exists()
{
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
}

// Open file in append mode
Expand Down Expand Up @@ -174,7 +174,7 @@ mod tests {
use serial_test::serial;
use tempfile::TempDir;

fn setup_test_env(shell: &str, home: &PathBuf) -> Result<(), std::io::Error> {
fn setup_test_env(shell: &str, home: &std::path::Path) -> Result<(), std::io::Error> {
let test_env = TestEnvironment::new()
.with_shell(shell)
.with_home(home.to_string_lossy());
Expand Down
24 changes: 13 additions & 11 deletions src/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ pub fn execute(verbose: bool) -> Result<(), String> {

// Ensure all task names will be padded to this width
// Round up to nearest multiple of 5 for better alignment
let display_width = (max_task_name_width + 4) / 5 * 5;
let display_width = max_task_name_width.div_ceil(5) * 5;

// Get a sorted list of runners for deterministic output
let mut runners: Vec<String> = tasks_by_runner.keys().cloned().collect();
Expand Down Expand Up @@ -390,7 +390,7 @@ fn format_task_entry(task: &Task, is_ambiguous: bool, name_width: usize) -> Stri
};

// Create the task description part
let description_part = if let Some(_) = &task.disambiguated_name {
let description_part = if task.disambiguated_name.is_some() {
// For disambiguated tasks, show the original name with footnotes
let orig_with_footnotes = if !footnotes.is_empty() {
format!("{} {}", task.name.dimmed().red(), footnotes.yellow())
Expand Down Expand Up @@ -422,9 +422,7 @@ fn format_task_entry(task: &Task, is_ambiguous: bool, name_width: usize) -> Stri
} else if task.disambiguated_name.is_some() {
// For disambiguated tasks, the display name (disambiguated) should be green
display_name.green()
} else if is_ambiguous {
display_name.dimmed().red()
} else if task.shadowed_by.is_some() {
} else if is_ambiguous || task.shadowed_by.is_some() {
display_name.dimmed().red()
} else {
display_name.green()
Expand Down Expand Up @@ -644,8 +642,10 @@ mod tests {
runners.sort();

// Mock the task discovery
let mut discovered_tasks = task_discovery::DiscoveredTasks::default();
discovered_tasks.tasks = tasks;
let discovered_tasks = task_discovery::DiscoveredTasks {
tasks,
..Default::default()
};

// Calculate max task name width across all runners
let max_task_name_width = discovered_tasks
Expand All @@ -658,7 +658,7 @@ mod tests {

// Ensure all task names will be padded to this width
// Round up to nearest multiple of 5 for better alignment
let display_width = (max_task_name_width + 4) / 5 * 5;
let display_width = max_task_name_width.div_ceil(5) * 5;

// Process each runner
for runner in runners {
Expand Down Expand Up @@ -900,8 +900,10 @@ mod tests {
let mut writer = TestWriter::new();

// Mock the task discovery with our GitHub Actions task
let mut discovered_tasks = task_discovery::DiscoveredTasks::default();
discovered_tasks.tasks = vec![task];
let discovered_tasks = task_discovery::DiscoveredTasks {
tasks: vec![task],
..Default::default()
};

// Group tasks by runner
let mut tasks_by_runner: HashMap<String, Vec<&Task>> = HashMap::new();
Expand All @@ -922,7 +924,7 @@ mod tests {
write!(writer, "{} — {}", runner.cyan(), display_path.dimmed()).unwrap();

// Write the task
let formatted_task = format_task_entry(&act_tasks[0], false, 20);
let formatted_task = format_task_entry(act_tasks[0], false, 20);
writeln!(writer, "\n {}", formatted_task).unwrap();

// Get the output and verify it shows the full path
Expand Down
10 changes: 5 additions & 5 deletions src/commands/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,11 @@ fn merge_dela_into_toml(existing: &str) -> Result<String, String> {
/// Generate MCP config file for an editor at a specific path
fn generate_config_at(editor: Editor, config_path: &PathBuf) -> Result<(), String> {
// Create parent directory if it doesn't exist
if let Some(parent) = config_path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create {} directory: {}", editor.name(), e))?;
}
if let Some(parent) = config_path.parent()
&& !parent.exists()
{
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create {} directory: {}", editor.name(), e))?;
}

// Check if config already exists
Expand Down
39 changes: 19 additions & 20 deletions src/mcp/allowlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ impl McpAllowlistEvaluator {
pub fn is_task_allowed(&self, task: &Task) -> Result<bool, String> {
// First pass: Check for deny entries (highest precedence)
for entry in &self.allowlist.entries {
if let AllowScope::Deny = entry.scope {
if self.path_matches(&task.file_path, &entry.path, true) {
return Ok(false);
}
if let AllowScope::Deny = entry.scope
&& self.path_matches(&task.file_path, &entry.path, true)
{
return Ok(false);
}
}

Expand All @@ -56,12 +56,11 @@ impl McpAllowlistEvaluator {
}
}
AllowScope::Task => {
if self.path_matches(&task.file_path, &entry.path, false) {
if let Some(ref tasks) = entry.tasks {
if tasks.contains(&task.name) {
return Ok(true);
}
}
if self.path_matches(&task.file_path, &entry.path, false)
&& let Some(ref tasks) = entry.tasks
&& tasks.contains(&task.name)
{
return Ok(true);
}
}
AllowScope::Deny | AllowScope::Once => {
Expand Down Expand Up @@ -157,7 +156,7 @@ mod tests {
let evaluator = McpAllowlistEvaluator::new().unwrap();

// With empty allowlist, task should be denied by default for MCP
assert_eq!(evaluator.is_task_allowed(&task).unwrap(), false);
assert!(!evaluator.is_task_allowed(&task).unwrap());

reset_to_real_environment();
}
Expand All @@ -180,7 +179,7 @@ mod tests {
let evaluator = McpAllowlistEvaluator::new().unwrap();

// Task should be allowed
assert_eq!(evaluator.is_task_allowed(&task).unwrap(), true);
assert!(evaluator.is_task_allowed(&task).unwrap());

drop(temp_dir);
reset_to_real_environment();
Expand All @@ -204,11 +203,11 @@ mod tests {
let evaluator = McpAllowlistEvaluator::new().unwrap();

// Task should be allowed
assert_eq!(evaluator.is_task_allowed(&task).unwrap(), true);
assert!(evaluator.is_task_allowed(&task).unwrap());

// Create a different task that should be denied
let other_task = create_test_task("other-task", std::path::PathBuf::from("Makefile"));
assert_eq!(evaluator.is_task_allowed(&other_task).unwrap(), false);
assert!(!evaluator.is_task_allowed(&other_task).unwrap());

drop(temp_dir);
reset_to_real_environment();
Expand Down Expand Up @@ -236,11 +235,11 @@ mod tests {
"build",
std::path::PathBuf::from("/project/subdir/Makefile"),
);
assert_eq!(evaluator.is_task_allowed(&subdir_task).unwrap(), true);
assert!(evaluator.is_task_allowed(&subdir_task).unwrap());

// Task outside directory should be denied
let outside_task = create_test_task("build", std::path::PathBuf::from("/other/Makefile"));
assert_eq!(evaluator.is_task_allowed(&outside_task).unwrap(), false);
assert!(!evaluator.is_task_allowed(&outside_task).unwrap());

drop(temp_dir);
reset_to_real_environment();
Expand All @@ -264,7 +263,7 @@ mod tests {
let evaluator = McpAllowlistEvaluator::new().unwrap();

// Task should be denied
assert_eq!(evaluator.is_task_allowed(&task).unwrap(), false);
assert!(!evaluator.is_task_allowed(&task).unwrap());

drop(temp_dir);
reset_to_real_environment();
Expand Down Expand Up @@ -300,7 +299,7 @@ mod tests {
let evaluator = McpAllowlistEvaluator::new().unwrap();

// Task should be denied (deny takes precedence)
assert_eq!(evaluator.is_task_allowed(&task).unwrap(), false);
assert!(!evaluator.is_task_allowed(&task).unwrap());

drop(temp_dir);
reset_to_real_environment();
Expand Down Expand Up @@ -350,12 +349,12 @@ mod tests {
let evaluator = McpAllowlistEvaluator::new().unwrap();

// Task should be denied due to file deny entry (higher precedence than directory allow)
assert_eq!(evaluator.is_task_allowed(&task).unwrap(), false);
assert!(!evaluator.is_task_allowed(&task).unwrap());

// But a task in a different file in the same directory should be allowed
let other_file_task =
create_test_task("build", std::path::PathBuf::from("/project/other.mk"));
assert_eq!(evaluator.is_task_allowed(&other_file_task).unwrap(), true);
assert!(evaluator.is_task_allowed(&other_file_task).unwrap());

drop(temp_dir);
reset_to_real_environment();
Expand Down
Loading