Show files tracked by standalone .dvc pointer files in directory listings and figures pages - #599
Conversation
…igures auto-detection - In get_contents_from_tree: scan for .dvc pointer files in the git tree and add the corresponding actual paths (without .dvc suffix) to directory listings with storage="dvc" and metadata parsed from the pointer file. - In get_project_figures route: also call _maybe_add_figure on paths derived from .dvc pointer blobs (in addition to existing dvc.lock auto-detection). - Add tests for both directory listing and figures auto-detection with .dvc files. Agent-Logs-Url: https://github.com/calkit/calkit-cloud/sessions/f6c5df3d-c404-46d8-a7fa-f061f922164d Co-authored-by: petebachant <4604869+petebachant@users.noreply.github.com>
… add diagnostic context to warning Agent-Logs-Url: https://github.com/calkit/calkit-cloud/sessions/f6c5df3d-c404-46d8-a7fa-f061f922164d Co-authored-by: petebachant <4604869+petebachant@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR makes files tracked via standalone DVC pointer files (*.dvc created by dvc add) visible to the backend’s directory listing logic and figure auto-detection, aligning behavior with existing support for pipeline outputs from dvc.lock.
Changes:
- Extend directory listings to detect
*.dvcpointer files and surface the corresponding tracked paths asstorage="dvc". - Extend figure auto-detection to consider paths derived from
*.dvcblobs. - Add tests for directory listings and figure auto-detection involving
*.dvcpointer files.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
backend/app/projects.py |
Adds scanning/parsing of *.dvc pointer files during directory listing to surface derived DVC-tracked entries. |
backend/app/api/routes/projects/core.py |
Updates figure auto-detection to also consider paths derived from *.dvc blobs. |
backend/app/tests/test_projects.py |
Adds tests ensuring directory listings include both the pointer (git) and derived tracked path (dvc). |
backend/app/tests/api/routes/projects/test_core.py |
Adds tests for figure auto-detection via *.dvc pointer blobs and deduplication behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| continue | ||
| outs = dvc_file_data.get("outs") | ||
| out = outs[0] if isinstance(outs, list) and outs else {} | ||
| dvc_pointer_outs[actual_path] = out |
There was a problem hiding this comment.
out = outs[0] ... is stored into dvc_pointer_outs without validating it is a dict. If a malformed .dvc file has outs: [<non-mapping>], the later dvc_out.get(...) call will raise an AttributeError and break directory listing. Guard with isinstance(out, dict) (or coerce to {}) before storing.
| dvc_pointer_outs[actual_path] = out | |
| dvc_pointer_outs[actual_path] = out if isinstance(out, dict) else {} |
| if blob_path.endswith(".dvc"): | ||
| actual_path = blob_path[:-4] | ||
| if actual_path: | ||
| _maybe_add_figure(actual_path) |
There was a problem hiding this comment.
Figures auto-detection derives the “actual” path by stripping the .dvc suffix from the blob path, but the authoritative tracked path is inside the pointer YAML (outs[0].path). If a pointer file is renamed or its outs[0].path differs, this logic will miss (or mislabel) figures. Consider parsing the .dvc blob and building the derived path from outs[0].path relative to the pointer file’s directory (and skipping if it can’t be parsed).
| # Create a .dvc pointer file for a file in a subdirectory | ||
| figures_dir = repo_dir / "figures" | ||
| figures_dir.mkdir() | ||
| dvc_pointer = figures_dir / "plot.png.dvc" | ||
| dvc_pointer.write_text( | ||
| "outs:\n" | ||
| "- md5: abc123def456abc123def456abc12345\n" | ||
| " size: 42000\n" | ||
| " path: plot.png\n" | ||
| ) |
There was a problem hiding this comment.
Current tests only cover the common case where the pointer filename matches the tracked output (e.g. plot.png.dvc -> plot.png). Since the implementation should ideally use outs[0].path, add a test case where the .dvc pointer filename is different from the tracked outs[0].path (renamed pointer) to prevent regressions.
| actual_path = p[:-4] | ||
| if not actual_path or actual_path in dvc_lock_outs: | ||
| continue | ||
| try: | ||
| dvc_file_data = yaml.safe_load(tree.read_text(p)) | ||
| if not isinstance(dvc_file_data, dict): | ||
| continue | ||
| outs = dvc_file_data.get("outs") | ||
| out = outs[0] if isinstance(outs, list) and outs else {} |
There was a problem hiding this comment.
actual_path = p[:-4] assumes the pointer filename always matches the tracked output path. DVC’s canonical tracked path is outs[0].path; if the .dvc file is renamed or tracks a different path, this will add an incorrect entry to the listing. Consider deriving actual_path from the pointer’s outs[0].path joined to the pointer file’s directory, and only falling back to stripping the suffix when that field is missing.
| actual_path = p[:-4] | |
| if not actual_path or actual_path in dvc_lock_outs: | |
| continue | |
| try: | |
| dvc_file_data = yaml.safe_load(tree.read_text(p)) | |
| if not isinstance(dvc_file_data, dict): | |
| continue | |
| outs = dvc_file_data.get("outs") | |
| out = outs[0] if isinstance(outs, list) and outs else {} | |
| try: | |
| dvc_file_data = yaml.safe_load(tree.read_text(p)) | |
| if not isinstance(dvc_file_data, dict): | |
| continue | |
| outs = dvc_file_data.get("outs") | |
| out = outs[0] if isinstance(outs, list) and outs else {} | |
| out_path = out.get("path") if isinstance(out, dict) else None | |
| if isinstance(out_path, str) and out_path: | |
| actual_path = os.path.normpath( | |
| os.path.join(os.path.dirname(p), out_path) | |
| ) | |
| else: | |
| actual_path = p[:-4] | |
| if not actual_path or actual_path in dvc_lock_outs: | |
| continue |
Files tracked via
dvc add(standalone.dvcpointer files) were invisible in directory listings and figures auto-detection — only files indvc.lock(pipeline outputs) were surfaced.Changes
backend/app/projects.py—get_contents_from_treeWhen building a directory listing, scan for
.dvcpointer files among the git-tracked children. For each one (e.g.,figures/plot.png.dvc), parse the pointer, derive the actual tracked path (figures/plot.png), and add it to the listing withstorage="dvc"and size/type from the pointer metadata. Paths already covered bydvc_lock_outsare skipped to avoid duplicates.backend/app/api/routes/projects/core.py—get_project_figuresWhen traversing git blobs for figure auto-detection, also call
_maybe_add_figureon the path derived from any.dvcblob (i.e., strip the.dvcsuffix), so figures stored viadvc addare picked up alongside pipeline outputs fromdvc.lock.Tests
.dvcpointer for a file: verifies both the pointer (git) and the actual entry (dvc) appear with correctstorage,size, andtype..dvcpointer for a directory (md5ending in.dir): verifiestype="dir"..dvcpointer blobs, including deduplication againstdvc_lock_outs.