Skip to content

Support folder arguments in mm cat and mm bench - #131

Open
spillai wants to merge 3 commits into
mainfrom
devin/1778548323-cat-bench-folder
Open

Support folder arguments in mm cat and mm bench#131
spillai wants to merge 3 commits into
mainfrom
devin/1778548323-cat-bench-folder

Conversation

@spillai

@spillai spillai commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add folder support to mm cat and the mm bench suite.

mm cat <folder> — recursively expands the folder into its files (gitignore-aware by default; pass --no-ignore to include ignored entries). Folders can be mixed with individual file arguments; duplicates are removed while preserving first-seen order. The existing ≥9-paths batch-confirmation gate applies to the post-expansion file count.

mm bench — gains two new rows in FAST_COMMANDS so the suite covers the new folder-expansion path:

  • mm cat <folder> (default, gitignore-aware)
  • mm cat <folder> --no-ignore

Both run with --no-cache --no-generate -y so timings stay focused on the folder-walk + encoder-dispatch cost (no per-file LLM calls during bench rounds) and the ≥9-paths confirmation prompt never blocks non-interactive runs.

mm bench <file> — single-file target also works: benches just that file with its parent directory used for {dir} substitutions. The legacy mm bench <folder> behaviour is unchanged.

Implementation notes:

  • Added mm.utils.expand_path_arg / expand_path_args helpers backed by the Rust Scanner so folder expansion stays consistent with mm find (gitignore semantics, deterministic ordering, performance).
  • mm cat walks the raw path list (stdin + argv), expanding directory entries via expand_path_arg, deduplicating, and then handing the resulting file list to the existing multi-file pipeline.
  • mm bench introduces _resolve_bench_target which normalizes the CLI target arg into (directory, files_filter). _run_benchmarks and _run_stdout_snapshot accept an optional files_filter: frozenset[str] | None and apply it after scanning so per-file dispatch keeps working without re-scanning.
  • README, docs/spec/cat.md, and CLAUDE.md / AGENTS.md updated with the new usage. New unit + CLI tests for the helpers, mm cat <folder>, mm bench <file> / _resolve_bench_target, and the new bench rows.

Local status:

  • pytest tests/python/ — 1092 passed, 55 skipped, 65 deselected
  • make lint, make typecheck, pre-commit run on the touched files — pass

Review & Testing Checklist for Human

  • mm cat ./some-folder prints every file in the folder once with the usual <filename> banners. Try a folder with a subdirectory to confirm recursion.
  • mm cat ./folder file.md (mixing a folder with a file already inside it) processes each file exactly once.
  • mm bench ./mm-samples --mode fast --command folder shows the two new mm cat <folder> rows with sensible timings.
  • mm bench ./folder/image.jpg --dry-run --format json reports files: 1 and the parent folder as directory, with per-kind commands pointing at the targeted file only.

Notes

  • For mm cat, the batch-confirmation prompt counts post-expansion files: cat'ing a 50-file folder still hits the ≥9 threshold and needs -y in non-interactive use. The new bench rows pass -y for exactly that reason.
  • The new bench rows pass --no-generate so they don't spend API tokens during bench rounds. If you want a folder benchmark that includes LLM calls, copy the row and drop --no-generate.
  • mm cat's internal threadpool can hit database is locked errors when several text/code files are cat'd concurrently against the same SQLite DB. That's a pre-existing limitation; the folder-CLI tests in this PR are written to avoid hitting it. Worth fixing separately (e.g. lower max_workers or a single shared write transaction).

Link to Devin session: https://app.devin.ai/sessions/0f554f89676d4c438512fd343d9baf32
Requested by: @spillai


Open in Devin Review

- `mm cat <folder>` recursively expands a folder into its files (gitignore-aware
  by default; pass `--no-ignore` to include ignored entries). Folders mix with
  individual file arguments; duplicates are removed while preserving order.
- `mm bench <file>` benches just that file, using its parent directory for
  `{dir}` substitutions. The legacy `mm bench <folder>` behaviour is unchanged.
- Add `mm.utils.expand_path_arg` / `expand_path_args` helpers (Scanner-backed)
  for use by both commands.
- Update README, docs/spec/cat.md, and CLAUDE.md/AGENTS.md with the new usage.
- Add unit and CLI tests for both commands and the helpers.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request adds support for directory arguments to the mm cat and mm bench commands, enabling recursive file expansion that respects .gitignore rules by default. A new --no-ignore flag has been added to include ignored files, and the mm bench command now supports targeting individual files. Review feedback identifies a bug in path deduplication caused by inconsistent path resolution between files and directories in the expand_path_arg utility, as well as opportunities to simplify the expansion logic in the cat command.

Comment thread python/mm/utils.py Outdated
if not p.exists():
raise FileNotFoundError(str(p))
if p.is_file():
return [p]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The expand_path_arg function returns relative paths for files but absolute paths for directory contents. This inconsistency causes deduplication to fail in expand_path_args (and in cat_cmd) when a file is provided both as a direct argument and as part of a directory expansion. Returning resolved paths for files ensures consistent behavior and reliable deduplication.

Suggested change
return [p]
return [p.resolve()]

Comment thread python/mm/commands/cat.py Outdated
Comment on lines +352 to +367
if p.is_dir():
try:
expanded = expand_path_arg(p, no_ignore=no_ignore)
except FileNotFoundError:
paths.append(entry)
continue
for f in expanded:
key = str(f)
if key not in seen:
seen.add(key)
paths.append(key)
else:
key = str(p)
if key not in seen:
seen.add(key)
paths.append(entry)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current path expansion and deduplication logic is unnecessarily complex and contains a bug where deduplication fails when mixing relative and absolute paths (e.g., mm cat file.txt .). Since expand_path_arg returns absolute paths for directory contents, but the file branch uses the raw input string, the seen set fails to catch duplicates. This can be simplified by using expand_path_arg for both files and directories, ensuring consistent absolute paths for all existing entries.

Suggested change
if p.is_dir():
try:
expanded = expand_path_arg(p, no_ignore=no_ignore)
except FileNotFoundError:
paths.append(entry)
continue
for f in expanded:
key = str(f)
if key not in seen:
seen.add(key)
paths.append(key)
else:
key = str(p)
if key not in seen:
seen.add(key)
paths.append(entry)
# expand_path_arg handles both files and directories consistently.
# It returns resolved absolute paths to ensure reliable deduplication.
try:
for f in expand_path_arg(p, no_ignore=no_ignore):
key = str(f)
if key not in seen:
seen.add(key)
paths.append(key)
except FileNotFoundError:
# Fallback for race conditions where path is deleted after exists() check
paths.append(entry)

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment thread python/mm/commands/cat.py Outdated
Comment on lines +358 to +367
for f in expanded:
key = str(f)
if key not in seen:
seen.add(key)
paths.append(key)
else:
key = str(p)
if key not in seen:
seen.add(key)
paths.append(entry)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Deduplication fails when mixing relative file paths with directory expansion (which produces absolute paths)

In cat_cmd, directory expansion via expand_path_arg returns absolute paths (because it resolves: root = p.resolve() at python/mm/utils.py:128), but individual file arguments use str(p) as the dedup key, which preserves the user's original (potentially relative) form. When a user runs mm cat ./my-folder ./my-folder/a.md, the directory expansion adds /absolute/my-folder/a.md to seen, but the file argument adds my-folder/a.md — these keys don't match, so the file is processed twice.

The test test_mixed_file_and_directory_dedupe doesn't catch this because tmp_path is already absolute, so both the directory-expanded and file-argument keys happen to be identical absolute paths.

Suggested change
for f in expanded:
key = str(f)
if key not in seen:
seen.add(key)
paths.append(key)
else:
key = str(p)
if key not in seen:
seen.add(key)
paths.append(entry)
for f in expanded:
key = str(f)
if key not in seen:
seen.add(key)
paths.append(key)
else:
key = str(p.resolve())
if key not in seen:
seen.add(key)
paths.append(entry)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

devin-ai-integration Bot and others added 2 commits May 12, 2026 01:32
Adds two new entries to FAST_COMMANDS so the bench harness exercises the
folder-expansion path of `mm cat`:

- `mm cat <folder>` — gitignore-aware (default)
- `mm cat <folder> --no-ignore` — bypasses .gitignore

Both use `--no-cache --no-generate -y` so timings stay focused on the
folder-walk + encoder-dispatch cost (no per-file LLM calls during bench
rounds) and so the >=9-paths confirmation prompt doesn't block
non-interactive runs.

Also stabilises the cat folder-CLI tests against a pre-existing
threadpool/SQLite race that only manifests when multiple text/code
files are cat'd concurrently. Folder walking and de-duplication are
still fully covered at the unit level.
expand_path_arg previously returned the raw input Path for files but
absolute paths for directory contents. Combined with cat_cmd keying
the de-dup set on the user's raw input string, this meant
`mm cat ./folder ./folder/a.md` processed a.md twice (the directory
walk's absolute key didn't match the file branch's relative key).

- expand_path_arg now always returns resolved absolute paths so its
  contract is uniform across file and directory inputs.
- cat_cmd drops the explicit is_dir() branch and uses expand_path_arg
  for both file and directory args, de-duplicating on the resolved
  string form.
- Added unit + CLI regression tests that mix a relative file with an
  absolute directory (and vice versa) to catch the original bug.

Co-Authored-By: Sudeep Pillai <sudeep.pillai@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Addressed review feedback in 8d1e855:

  • expand_path_arg contract fixed — files now also return resolved absolute paths (gemini comment 3). The helper has a single, uniform contract regardless of input kind, which is what makes string-keyed de-dup reliable.
  • cat_cmd simplified — dropped the explicit is_dir() branch and route both file and directory args through expand_path_arg. Resolves the relative-vs-absolute key mismatch flagged by gemini comment 4 and Devin Review.
  • Regression tests addedtest_expand_path_args_dedupes_mixed_relative_and_absolute (unit) and test_mixed_relative_file_and_absolute_directory_dedupes (CLI) exercise the exact failure mode (relative file + absolute directory). Both fail on the pre-fix code, pass after.

Full local pass: 1095 tests, lint + typecheck clean.

@nwaughachukwuma

Copy link
Copy Markdown
Collaborator

@spillai, is this ready for merging?

@devin-ai-integration

Copy link
Copy Markdown
Contributor

@nwaughachukwuma from the Devin side it's ready: all review comments from gemini-code-assist and Devin Review are addressed in 8d1e855 (path-dedup bug fix + regression tests), CI is green, and 1095 tests pass locally. Waiting on @spillai for final sign-off and merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants