Implement selective and in-place freeze (#1063)#1102
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
WalkthroughRefactors manifest handling to use a StrictYAML AST and adds in-place, selective freezing: Changes
Sequence DiagramsequenceDiagram
actor User as User
participant CLI as "dfetch freeze"
participant SP as SuperProject
participant M as Manifest (AST)
participant VCS as VCS/Subproject
participant FS as Manifest File
User->>CLI: run "dfetch freeze [projects...]"
CLI->>SP: create_super_project(manifest_path)
SP->>M: Manifest.from_file(manifest_path)
M->>FS: read YAML -> build AST
CLI->>M: selected_projects(args.projects)
M-->>CLI: list of projects to process
loop per selected project
CLI->>VCS: freeze_project(project) -- obtain new_version
VCS-->>CLI: new_version or None
alt new_version obtained
CLI->>M: update_project_version(project)
M->>M: mutate AST (revision/tag/branch/integrity)
CLI->>CLI: manifest_updated = True
end
end
alt manifest_updated
CLI->>M: update_dump()
M->>FS: write YAML preserving layout/comments
CLI-->>User: log updated manifest path
else no changes
CLI-->>User: no updates
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dfetch/manifest/manifest.py`:
- Around line 681-748: The current logic only adds/updates version fields but
never removes stale ones; update _apply_block_updates (used by
_update_project_version_in_text) to remove any existing "revision", "tag", and
"branch" lines from the block when they are not present in fields_to_set, and to
remove the integrity.hash entry when integrity_hash is empty (adjust or extend
_set_integrity_hash_in_block or add a _remove_integrity_hash_in_block helper to
perform the deletion). Locate and change behavior around _find_field,
_update_value, and the integrity handling so that missing keys in the project
YAML result in the corresponding lines being deleted from the block before
inserting/updating remaining fields.
- Around line 517-569: The code currently matches the first bare "- name:
<project>" anywhere in the file and doesn't accept quoted YAML scalars; update
_find_project_block and _locate_project_name_line so they only search inside the
"projects:" list and accept name values quoted with single or double quotes.
Concretely: first scan for the "projects:" key and compute its block
range/indentation, then limit the search in _locate_project_name_line to lines
inside that block; change the regex in _locate_project_name_line to allow
optional surrounding single or double quotes around the name value (e.g.
\s*(?P<q>['"]?)(?P<name>...)(?P=q)\s*) while still using re.escape(project_name)
when building the pattern; ensure _find_project_block calls the updated locator
and still returns (start, end, item_indent).
- Around line 594-620: Both helpers currently rebuild lines as "field: value\n"
losing any inline comment/trailing whitespace and forcing LF; update
_update_value and _append_field (and the similar code around the other
occurrence at the same file) to preserve the original line suffix and line
ending: in _update_value keep the original line's trailing content after the
value (including inline comments/whitespace) and reuse its exact EOL when
replacing the line, and in _append_field choose the EOL style by inspecting
neighboring lines (or a provided line's EOL) so inserted lines use the same
CR/LF style and do not drop comments/whitespace semantics when yaml_value is
empty or present.
In `@features/freeze-inplace.feature`:
- Around line 35-68: The selective in-place freeze scenario needs the same "no
.backup" invariant as the all-projects path: after running the command in the
"Only selected project is frozen in-place" scenario, add an assertion that no
".backup" file exists (e.g., no dfetch.yaml.backup or any ".backup" artifact) in
the repo; this ensures the selected_projects(args.projects) code path performs
in-place edits without falling back to backup+rewrite.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c048f3f0-d3b6-4dc0-aa30-5db8c49ac01c
📒 Files selected for processing (7)
CHANGELOG.rstdfetch/commands/freeze.pydfetch/manifest/manifest.pyfeatures/freeze-inplace.featurefeatures/freeze-specific-projects.featurefeatures/steps/manifest_steps.pytests/test_manifest.py
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
dfetch/manifest/manifest.py (2)
655-676:⚠️ Potential issue | 🟠 MajorStale version fields are not removed during in-place freeze.
_apply_block_updatesonly adds/updates fields present infields_to_setbut never removes existingrevision/tag/branchlines that are no longer in the frozen project. If a user switches pinning mode (e.g., fromtagtorevision), the old field remains, causing contradictory version state that diverges from the backup+dump path.🔧 Suggested approach
Before applying updates, remove any existing version fields (
revision,tag,branch) that are not infields_to_set:def _apply_block_updates(...) -> list[str]: # Remove stale version fields first fields_to_keep = {f for f, _ in fields_to_set} for field in ("revision", "tag", "branch"): if field not in fields_to_keep: idx = _find_field(block, field, field_indent) if idx is not None: del block[idx] # Then apply updates as before...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dfetch/manifest/manifest.py` around lines 655 - 676, _in _apply_block_updates remove stale version fields before inserting/updating: compute fields_to_keep = {f for f,_ in fields_to_set} and for each of ("revision","tag","branch") if the field is not in fields_to_keep call _find_field(block, field, field_indent) and del block[idx] when found, then proceed with the existing insert/update logic and finally call _set_integrity_hash_in_block as before so stale version lines are not left behind after a pinning-mode change.
518-541:⚠️ Potential issue | 🟠 MajorProject name search scope and quoted scalars are not handled.
_locate_project_name_linesearches the entire file for- name: <value>and doesn't:
- Limit the search to the
projects:section — aremotes:entry with the same name would match first- Handle YAML-quoted names like
name: 'myproject'orname: "176"This can cause in-place freeze to target the wrong block or fail on valid manifests with quoted project names.
🔧 Suggested approach
Update the regex to optionally match single/double quotes around the name value:
pattern = re.compile( - r"^(?P<indent>\s*)-\s*name:\s*(?P<name>" + r"^(?P<indent>\s*)-\s*name:\s*(?P<quote>['\"]?)(?P<name>" + re.escape(project_name) - + r")\s*(?:#.*)?$" + + r")(?P=quote)\s*(?:#.*)?$" )For scope limitation, consider first locating the
projects:key and its indentation, then only searching within that range.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dfetch/manifest/manifest.py` around lines 518 - 541, _locate_project_name_line currently scans the whole file and uses a regex that only matches unquoted scalars; update it to first find the projects: mapping block (locate the "projects:" line and note its indentation, then limit subsequent search to lines indented under that block until a same-or-less-indented non-blank non-comment line ends the block) and change the regex (the pattern variable) to accept optional single or double quotes around the name value (allowing name: myproj, name: 'myproj', and name: "myproj") while preserving capture groups for indent and name so the function still returns (line_idx, item_indent, name_col_start, name_col_end).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dfetch/manifest/manifest.py`:
- Around line 670-671: The code under the "if inserted:" branch constructs lines
with hardcoded "\n" and inserts them directly into the block, bypassing the
existing formatting helper; replace that manual insertion by iterating over the
inserted list and calling the existing _append_field helper for each (f, v) so
formatting/line endings and duplication logic are centralized; ensure you call
_append_field with the current block and the same field_indent/context used
elsewhere (preserve insertion order or insert in reverse if _append_field
mutates the block at the insertion index) and remove the hardcoded string
construction.
In `@dfetch/util/yaml.py`:
- Around line 45-61: The code in update_value only preserves '\n' eol, so CRLF
lines become partially converted; change update_value to detect and preserve
"\r\n" or "\n" (e.g., set eol = "\r\n" if line.endswith("\r\n") elif
line.endswith("\n") else ""), and use that eol when writing back; also update
append_field to choose the EOL for appended lines consistently (for example,
inspect the last line of the block to detect its eol and use that instead of
always appending "\n"), referencing the functions update_value and append_field
so the fix is applied where line endings are constructed.
---
Duplicate comments:
In `@dfetch/manifest/manifest.py`:
- Around line 655-676: _in _apply_block_updates remove stale version fields
before inserting/updating: compute fields_to_keep = {f for f,_ in fields_to_set}
and for each of ("revision","tag","branch") if the field is not in
fields_to_keep call _find_field(block, field, field_indent) and del block[idx]
when found, then proceed with the existing insert/update logic and finally call
_set_integrity_hash_in_block as before so stale version lines are not left
behind after a pinning-mode change.
- Around line 518-541: _locate_project_name_line currently scans the whole file
and uses a regex that only matches unquoted scalars; update it to first find the
projects: mapping block (locate the "projects:" line and note its indentation,
then limit subsequent search to lines indented under that block until a
same-or-less-indented non-blank non-comment line ends the block) and change the
regex (the pattern variable) to accept optional single or double quotes around
the name value (allowing name: myproj, name: 'myproj', and name: "myproj") while
preserving capture groups for indent and name so the function still returns
(line_idx, item_indent, name_col_start, name_col_end).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d0fcc303-cd67-461e-bb8d-0f0a67627fa1
📒 Files selected for processing (4)
dfetch/manifest/manifest.pydfetch/util/yaml.pyfeatures/freeze-inplace.featuretests/test_manifest.py
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
dfetch/manifest/manifest.py (2)
518-556:⚠️ Potential issue | 🔴 CriticalRestrict project lookup to
projects:and accept quoted names.
_locate_project_name_line()still scans the whole document for the first bare- name:match. If a remote shares the same name,_find_project_block()can rewrite the remote block; if the manifest stores the project name as'176'or"176", lookup fails even though the YAML is valid.
657-691:⚠️ Potential issue | 🟠 MajorRemove version keys that disappeared from
project.as_yaml().This path only updates/adds keys. If a project switches from
branch/tagtorevision, or losesintegrity, the old entries stay in the manifest; when no version keys remain, Lines 690-691 return the original text unchanged. That makes the in-place path diverge from the backup+dump flow and can leave contradictory pins behind.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dfetch/manifest/manifest.py` around lines 657 - 691, The current in-place path only adds/updates version keys and never removes keys that disappeared from project.as_yaml(), so stale entries remain; update the logic so when _collect_version_fields returns no fields and no integrity_hash you remove any existing version-related lines from the project's block before returning. Concretely, modify _apply_block_updates (or call it from _update_project_version_in_text) to detect and delete lines whose field name is one of the version keys (e.g. "revision", "tag", "branch", "integrity.hash") at the given field_indent when fields_to_set is empty, and ensure _set_integrity_hash_in_block is not re-adding; then return the cleaned block so the in-place flow matches the backup+dump behavior.dfetch/util/yaml.py (1)
57-64:⚠️ Potential issue | 🟡 MinorPreserve the original EOL convention here.
update_value()still collapses\r\nto\n, andappend_field()always emits\n. A CRLF-formatted manifest will not round-trip verbatim through these helpers.Also applies to: 81-83
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dfetch/util/yaml.py` around lines 57 - 64, The code in update_value() (and similarly in append_field()) always uses "\n" for eol which collapses CRLF; detect and preserve the original EOL sequence by checking the source line(s) for "\r\n" vs "\n" (e.g., use line.endswith("\r\n") ? "\r\n" : line.endswith("\n") ? "\n" : "") or infer the file/ block EOL from existing lines, then use that eol variable when constructing the replacement string (replace the hard-coded "\n" in block[line_idx] assignment and in append_field() with the detected eol).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dfetch/manifest/manifest.py`:
- Around line 589-600: The loop that computes sub_end for the nested integrity
mapping incorrectly treats comment-only lines as ending the block, causing
_find_field to miss existing "hash:" entries; update the loop in the manifest
handling (the loop using variables sub_end, block, integrity_line, field_indent)
to ignore comment-only lines (e.g., skip any line where
block[i].strip().startswith("#")) when deciding the end boundary, so _find_field
and subsequent calls to _update_value/_append_field operate over the full nested
mapping.
---
Duplicate comments:
In `@dfetch/manifest/manifest.py`:
- Around line 657-691: The current in-place path only adds/updates version keys
and never removes keys that disappeared from project.as_yaml(), so stale entries
remain; update the logic so when _collect_version_fields returns no fields and
no integrity_hash you remove any existing version-related lines from the
project's block before returning. Concretely, modify _apply_block_updates (or
call it from _update_project_version_in_text) to detect and delete lines whose
field name is one of the version keys (e.g. "revision", "tag", "branch",
"integrity.hash") at the given field_indent when fields_to_set is empty, and
ensure _set_integrity_hash_in_block is not re-adding; then return the cleaned
block so the in-place flow matches the backup+dump behavior.
In `@dfetch/util/yaml.py`:
- Around line 57-64: The code in update_value() (and similarly in
append_field()) always uses "\n" for eol which collapses CRLF; detect and
preserve the original EOL sequence by checking the source line(s) for "\r\n" vs
"\n" (e.g., use line.endswith("\r\n") ? "\r\n" : line.endswith("\n") ? "\n" :
"") or infer the file/ block EOL from existing lines, then use that eol variable
when constructing the replacement string (replace the hard-coded "\n" in
block[line_idx] assignment and in append_field() with the detected eol).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c4d4cb95-596e-470b-a040-a219d0c2b6aa
📒 Files selected for processing (3)
dfetch/manifest/manifest.pydfetch/util/yaml.pytests/test_manifest.py
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dfetch/util/yaml.py`:
- Around line 68-85: The trailing-comment detection in update_value uses
re.search(r"(\s+#.*)$") which can treat hashes inside quoted YAML values as
comments; update update_value to detect an unquoted, whitespace-prefixed '#'
instead of using that regex—scan the stripped line character-by-character
tracking single/double-quote state and locate the first '#' that is not inside
quotes and is preceded by whitespace, then split comment = line[comment_start:]
(or empty if none); keep existing behavior for indent, eol and replacement.
Reference function: update_value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 197224af-fb0a-4f55-becb-5d5d3ee870a6
📒 Files selected for processing (2)
dfetch/manifest/manifest.pydfetch/util/yaml.py
- Add optional project name arguments to `dfetch freeze` so users can freeze individual projects rather than all at once. - When the manifest lives inside a git or SVN superproject, freeze now edits the manifest file in-place (preserving comments, blank lines and indentation) instead of creating a `.backup` copy and regenerating from scratch. - Add `update_project_in_manifest_file` and helper functions in `dfetch/manifest/manifest.py` for in-place YAML text editing. - Add BDD feature files: `freeze-specific-projects.feature` and `freeze-inplace.feature`, plus the required step definitions. - Add unit tests for all new in-place editing helpers. - Add CHANGELOG entry for v0.14.0 (unreleased). https://claude.ai/code/session_01Xd8EcAUkSoJo9YZimGzEuA
Split _set_integrity_hash_in_block (CC 11) and _update_project_version_in_text (CC 13) into smaller helpers so every new function stays below CC 9: - _update_hash_in_existing_integrity (CC 6) - _append_integrity_block (CC 3) - _set_integrity_hash_in_block (CC 4) - _collect_version_fields (CC 4) - _apply_block_updates (CC 7) - _update_project_version_in_text (CC 2) https://claude.ai/code/session_01Xd8EcAUkSoJo9YZimGzEuA
Both find_name_in_manifest (used by SARIF/Jenkins/CodeClimate reporters) and _find_project_block (used by in-place freeze) independently scanned for the '- name: <project>' line with slightly different regexes. Extract _locate_project_name_line(lines, project_name) -> tuple | None that performs the scan once and returns all four pieces of information both callers need: - line_idx (0-based) - item_indent (column of the '-') - name_col_start / name_col_end (for ManifestEntryLocation) find_name_in_manifest now calls _locate_project_name_line, preserving its public interface (same error message, same ManifestEntryLocation values including the existing "no spaces" edge case). _find_project_block delegates the name scan to the helper and only retains the block-boundary search loop. Side-effect: CC of find_name_in_manifest drops 5→3, _find_project_block drops 7→4. https://claude.ai/code/session_01Xd8EcAUkSoJo9YZimGzEuA
Replace specific-purpose helpers with three building-block functions: - `_find_field`: locate a named field at exact indent within a block - `_update_value`: replace the value on a known line index - `_append_field`: insert a new key-value line at a given position All higher-level helpers (_set_simple_field_in_block, _update_hash_in_existing_integrity, _append_integrity_block, _set_integrity_hash_in_block, _apply_block_updates) are refactored to compose these primitives instead of duplicating regex/index logic. https://claude.ai/code/session_01Xd8EcAUkSoJo9YZimGzEuA
- Extract yaml_scalar, find_field, update_value, append_field into the new dfetch.util.yaml module so they can be reused outside the manifest - Fix yaml_scalar: use splitlines()[0] instead of strip() to avoid the trailing '\n...' document-end marker that yaml.dump emits for plain strings - Fix update_value: preserve trailing inline comments (e.g. '# note') when replacing a field value, so freeze does not destroy annotations - manifest.py: replace the four duplicate implementations with aliases that delegate to dfetch.util.yaml - Add BDD scenario: inline comments on fields survive an in-place freeze - Add unit tests for comment preservation in update_value and _update_project_version_in_text https://claude.ai/code/session_01Xd8EcAUkSoJo9YZimGzEuA
find_field: add an explicit guard to skip lines where the first non-whitespace character is '#'. Previously the startswith-prefix check happened to reject these lines, but the intent was implicit. _find_project_block: skip commented lines when scanning for the end of a project block. A comment at item-indent level (e.g. ' # old pin') was incorrectly treated as a block boundary, causing fields that followed it to be excluded from the block and duplicated on freeze instead of updated. Add unit tests covering: - find_field skips '# field:' and '#field:' (no space) - find_field continues past a commented-out duplicate to find the live field - _find_project_block includes fields that follow a comment at item-indent - _update_project_version_in_text: commented-out field treated as absent - _update_project_version_in_text: comment at item-indent does not break block https://claude.ai/code/session_01Xd8EcAUkSoJo9YZimGzEuA
integrity sub_end scan skipped comment lines (manifest.py) The loop in _update_hash_in_existing_integrity that computes sub_end treated comment-only lines as block boundaries, so a '# note' line inside an integrity: mapping caused the subsequent hash: line to be missed. The loop now skips blank and comment-only lines before comparing indentation. Stale version keys not removed on in-place freeze (manifest.py) _apply_block_updates only added/updated fields; keys absent from project.as_yaml() (e.g. revision after unfreeze, or branch after switching to tag) were left in the manifest as stale entries. Added _remove_stale_version_fields and _remove_integrity_block helpers that delete version keys not present in the new project data before applying updates. Removed the early-return in _update_project_version_in_text so cleanup always runs. CRLF files collapsed to LF (yaml.py) update_value always wrote "\n"; append_field also hard-coded "\n". update_value now detects "\r\n" vs "\n" from the source line. append_field uses a new _detect_eol() helper that inspects the block's existing lines to infer the file's EOL convention. https://claude.ai/code/session_01Xd8EcAUkSoJo9YZimGzEuA
Sequence[str] for read-only inputs (manifest.py + yaml.py)
All functions that accept a block/lines parameter but never mutate it
in-place now declare Sequence[str] instead of list[str]. This resolves
8 pyright errors in tests where str.splitlines() on a LiteralString
produced list[LiteralString], which is invariant and could not be
passed as list[str].
Eliminate duplicated EOL detection (yaml.py)
_detect_eol and update_value both duplicated the endswith("\r\n") /
endswith("\n") pattern. Extracted into _line_eol(line) -> str and
wired both callers through it.
Cleaner copy pattern in _remove_* helpers (manifest.py)
_remove_integrity_block and _remove_stale_version_fields used
conditional list() copies. Both now copy unconditionally at the top
(result = list(block)) for clarity and consistent return type.
https://claude.ai/code/session_01Xd8EcAUkSoJo9YZimGzEuA
e1a96e1 to
0b30352
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
features/freeze-inplace.feature (1)
63-96:⚠️ Potential issue | 🟡 MinorAdd the no-backup invariant to the selective in-place scenario.
Line 63 onward covers a separate execution path (
dfetch freeze <project>in a VCS superproject), but it currently does not assert thatdfetch.yaml.backupis absent.Suggested assertion
Scenario: Only selected project is frozen in-place @@ Then the manifest 'dfetch.yaml' in superproject2 is replaced with """ @@ """ + And no file 'dfetch.yaml.backup' exists in superproject2🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@features/freeze-inplace.feature` around lines 63 - 96, The selective in-place freeze scenario ("Only selected project is frozen in-place") doesn't assert the no-backup invariant; after running "dfetch freeze ext/test-repo-tag" and verifying the manifest replacement (manifest 'dfetch.yaml'), add an assertion that no backup file named "dfetch.yaml.backup" exists in the superproject2 working tree to ensure no backup was created by the selective freeze path.dfetch/manifest/manifest.py (1)
518-558:⚠️ Potential issue | 🔴 CriticalScope the name lookup to
projects:and allow quoted scalars.This still matches the first bare
- name:anywhere in the file. If a remote shares the project's name,_find_project_block()will rewrite the remote entry; if a valid manifest spells the name as'foo'or"foo", the updater raises instead. Please bound the search to theprojects:block and accept optional matching quotes around the scalar.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dfetch/manifest/manifest.py` around lines 518 - 558, The current search in _find_project_block/_locate_project_name_line matches any "- name:" anywhere and fails on quoted scalars; update the logic to first locate the top-level "projects:" block (find the line matching a top-level ^\s*projects:\s*$ or with inline comment) and restrict subsequent name lookups to lines that are indented under that block (stop when you hit a line whose indent is <= the projects: indent or another top-level key). Then change the regex in _locate_project_name_line to accept optional single or double quotes around the scalar (e.g. allow name values like foo, 'foo', or "foo") when comparing to project_name, and return the same (line_idx, item_indent, name_col_start, name_col_end) coordinates for the unquoted characters; keep raising RuntimeError in _find_project_block when not found.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dfetch/commands/freeze.py`:
- Around line 108-111: The backup is using DEFAULT_MANIFEST_NAME instead of the
resolved manifest path, so change the no-VCS branch to use the manifest path
referenced by superproject.manifest.path (or the same resolved variable used
later) when copying the file and when writing updates; ensure the
shutil.copyfile call (when not use_inplace) and the subsequent edits that set
superproject.manifest.path both operate on the resolved manifest path rather
than DEFAULT_MANIFEST_NAME so non-default manifest filenames are backed up and
updated correctly.
In `@dfetch/manifest/manifest.py`:
- Around line 502-505: The current use of Path.read_text() and Path.write_text()
in manifest.py normalizes line endings and breaks the EOL-preservation logic in
_update_project_version_in_text; change the file I/O around the path variable so
you open the file with Path.open(newline='') (preserving exact EOLs) and read
the raw text (pass encoding='utf-8'), call _update_project_version_in_text(text,
project) as before, then write the updated content back using the same opened
file or a new open with newline='' and encoding='utf-8' to avoid any newline
normalization; update the code that currently uses path.read_text() and
path.write_text(updated, ...) to use open(newline='') instead so
_update_project_version_in_text receives and writes the original line endings
unchanged.
In `@features/steps/manifest_steps.py`:
- Around line 55-61: The step_impl currently calls check_file which uses a
lenient comparator that strips per-line whitespace; change it to perform a
strict byte/character comparison: build the manifest path with
os.path.join(directory, name), read the file contents (open(...).read()),
compute expected = apply_manifest_substitutions(context, context.text), and
assert file_contents == expected (or raise an assertion with both values) so
formatting/indentation regressions fail; update step_impl to use this direct
strict comparison instead of check_file.
---
Duplicate comments:
In `@dfetch/manifest/manifest.py`:
- Around line 518-558: The current search in
_find_project_block/_locate_project_name_line matches any "- name:" anywhere and
fails on quoted scalars; update the logic to first locate the top-level
"projects:" block (find the line matching a top-level ^\s*projects:\s*$ or with
inline comment) and restrict subsequent name lookups to lines that are indented
under that block (stop when you hit a line whose indent is <= the projects:
indent or another top-level key). Then change the regex in
_locate_project_name_line to accept optional single or double quotes around the
scalar (e.g. allow name values like foo, 'foo', or "foo") when comparing to
project_name, and return the same (line_idx, item_indent, name_col_start,
name_col_end) coordinates for the unquoted characters; keep raising RuntimeError
in _find_project_block when not found.
In `@features/freeze-inplace.feature`:
- Around line 63-96: The selective in-place freeze scenario ("Only selected
project is frozen in-place") doesn't assert the no-backup invariant; after
running "dfetch freeze ext/test-repo-tag" and verifying the manifest replacement
(manifest 'dfetch.yaml'), add an assertion that no backup file named
"dfetch.yaml.backup" exists in the superproject2 working tree to ensure no
backup was created by the selective freeze path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 65dc41e6-702a-4489-b9e1-e551673e824b
📒 Files selected for processing (12)
CHANGELOG.rstdfetch.yamldfetch/commands/freeze.pydfetch/manifest/manifest.pydfetch/reporting/sbom_reporter.pydfetch/util/yaml.pyfeatures/freeze-archive.featurefeatures/freeze-inplace.featurefeatures/freeze-projects.featurefeatures/freeze-specific-projects.featurefeatures/steps/manifest_steps.pytests/test_manifest.py
…ete test assertions - yaml.py: add delete(jsonpath, field) — mirrors set() but removes the field and its child block; _delete_by_parts stops at blank lines to preserve inter-item separators; 5 unit tests added in test_yaml.py - manifest.py update_project_version: call _doc.delete() for each version key that is now empty/absent (removes stale 'revision', 'tag', 'branch') and remove the entire 'integrity' block when no hash is present; regression test added in test_manifest.py - test_add.py: add append_project_entry.assert_called_once() + update_dump.assert_called_once() to the 5 tests that previously only accessed call_args without asserting call count: suffixes_duplicate_name, interactive_branch_by_number, svn_custom_branch, svn_tag, svn_branch_by_number https://claude.ai/code/session_01Xd8EcAUkSoJo9YZimGzEuA
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dfetch/manifest/manifest.py`:
- Around line 394-410: append_project_entry currently hardcodes a 2-space prefix
when appending YAML lines (the new_entry loop prepends " "), which will
misalign manifests that use a different indent width; update
append_project_entry to detect the existing indentation used under the projects:
section (e.g., scan self._doc.lines for the "projects:" line then inspect the
leading whitespace of the next non-empty line or an existing project entry) and
use that whitespace string when prefixing each line instead of the hardcoded "
", then keep calling update_dump to persist as before; reference
append_project_entry, new_entry, and self._doc.lines when making the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 81f2fd8c-3fe7-4792-aded-de1aa4f2a5b5
📒 Files selected for processing (5)
dfetch/manifest/manifest.pydfetch/util/yaml.pytests/test_add.pytests/test_manifest.pytests/test_yaml.py
3d41d05 to
7d4639c
Compare
7d4639c to
cad7f0a
Compare
81a4f7a to
8377944
Compare
|
@coderabbitai resume |
|
✅ Actions performedReviews resumed. Review triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
features/freeze-inplace.feature (1)
63-96:⚠️ Potential issue | 🟡 MinorMissing no-backup assertion for selective in-place freeze scenario.
Scenarios 1 and 2 both assert
no file 'dfetch.yaml.backup' exists, but this third scenario ("Only selected project is frozen in-place") is missing that assertion. A regression that creates backup files only for selective freezes would still pass this test.Suggested fix
- name: ext/test-repo-tag2 url: https://github.com/dfetch-org/test-repo branch: main """ + And no file 'dfetch.yaml.backup' exists in superproject2🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@features/freeze-inplace.feature` around lines 63 - 96, The scenario "Only selected project is frozen in-place" is missing the assertion that no backup file was created; add the same no-backup check used in the earlier scenarios by appending the step "And no file 'dfetch.yaml.backup' exists" (or the project's existing step definition for asserting absence of dfetch.yaml.backup) immediately after the "Then the manifest 'dfetch.yaml' in superproject2 is replaced with" block so selective in-place freeze doesn't silently create a backup file.features/steps/manifest_steps.py (1)
48-62:⚠️ Potential issue | 🟡 MinorMake the root-level manifest assertion strict too.
Lines 55-62 now do an exact compare, but the sibling step at Lines 48-52 still calls
check_file(...)withoutstrict=True. Root-level in-place freeze scenarios can still miss indentation/comment regressions.♻️ Suggested fix
`@then`("the manifest '{name}' is replaced with") `@then`("it should generate the manifest '{name}'") def step_impl(context, name): """Check a manifest.""" - check_file(name, apply_manifest_substitutions(context, context.text)) + check_file(name, apply_manifest_substitutions(context, context.text), strict=True)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@features/steps/manifest_steps.py` around lines 48 - 62, The root-level manifest assertion in the step_impl function currently calls check_file(name, apply_manifest_substitutions(...)) without strict=True, allowing whitespace/comment differences to slip; update that call in the first step_impl (the one decorated with `@then`("the manifest '{name}' is replaced with") and `@then`("it should generate the manifest '{name}'")) to pass strict=True to check_file (i.e., check_file(name, apply_manifest_substitutions(context, context.text), strict=True)) so it performs an exact compare like the subdirectory variant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dfetch.code-workspace`:
- Around line 62-69: Restore a separate debug configuration for the update
command instead of replacing it: add a new debug entry alongside the existing
"DFetch freeze" configuration named something like "DFetch update" that uses the
same "debugpy"/"dfetch.__main__"/"justMyCode" settings but sets "args" to
["update"] so developers can launch either "freeze" or "update" independently;
ensure the new entry mirrors the existing configuration fields except for the
"name" and "args".
In `@dfetch/manifest/manifest.py`:
- Around line 420-435: append_project_entry currently mutates self._doc but
doesn't update the in-memory cache self._projects, causing callers
(manifest.projects, check_name_uniqueness, _as_dict, dump) to see stale data;
after appending to projects_mu in append_project_entry, either append the
corresponding ProjectEntry (project_entry) to self._projects if it's already
populated, or invalidate the cache by setting self._projects = None so the next
access will reparse the document; reference append_project_entry, self._doc,
self._projects, and ProjectEntry when making this change.
- Around line 63-67: The _yaml_str function currently calls
yaml.safe_load(value) which can raise yaml.YAMLError for inputs like leading "!"
or unterminated quotes; update _yaml_str to call yaml.safe_load(value) inside a
try/except that catches yaml.YAMLError (and optionally Exception) and on
exception returns SingleQuotedScalarString(value), and otherwise keep the
existing behavior (return SingleQuotedScalarString(value) if parsed value is not
a str, else return value); refer to the function name _yaml_str and the use of
yaml.safe_load and SingleQuotedScalarString when making this change.
---
Duplicate comments:
In `@features/freeze-inplace.feature`:
- Around line 63-96: The scenario "Only selected project is frozen in-place" is
missing the assertion that no backup file was created; add the same no-backup
check used in the earlier scenarios by appending the step "And no file
'dfetch.yaml.backup' exists" (or the project's existing step definition for
asserting absence of dfetch.yaml.backup) immediately after the "Then the
manifest 'dfetch.yaml' in superproject2 is replaced with" block so selective
in-place freeze doesn't silently create a backup file.
In `@features/steps/manifest_steps.py`:
- Around line 48-62: The root-level manifest assertion in the step_impl function
currently calls check_file(name, apply_manifest_substitutions(...)) without
strict=True, allowing whitespace/comment differences to slip; update that call
in the first step_impl (the one decorated with `@then`("the manifest '{name}' is
replaced with") and `@then`("it should generate the manifest '{name}'")) to pass
strict=True to check_file (i.e., check_file(name,
apply_manifest_substitutions(context, context.text), strict=True)) so it
performs an exact compare like the subdirectory variant.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: aef22995-fb82-42f8-b8ea-17e4c18030a3
📒 Files selected for processing (19)
dfetch.code-workspacedfetch/commands/import_.pydfetch/commands/validate.pydfetch/manifest/manifest.pydfetch/manifest/parse.pydfetch/manifest/schema.pydfetch/project/__init__.pyfeatures/freeze-inplace.featurefeatures/freeze-projects.featurefeatures/freeze-specific-projects.featurefeatures/steps/add_steps.pyfeatures/steps/archive_steps.pyfeatures/steps/generic_steps.pyfeatures/steps/git_steps.pyfeatures/steps/manifest_steps.pytests/test_add.pytests/test_fuzzing.pytests/test_import.pytests/test_manifest.py
| def _yaml_str(value: str) -> str | SingleQuotedScalarString: | ||
| """Return SingleQuotedScalarString if value would be misread as non-string.""" | ||
| if not isinstance(yaml.safe_load(value), str): | ||
| return SingleQuotedScalarString(value) | ||
| return value |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python - <<'PY'
import yaml
for sample in ["'release", "!prod", "1.0", "true", "plain-text"]:
try:
loaded = yaml.safe_load(sample)
print(f"{sample!r} -> {type(loaded).__name__}: {loaded!r}")
except Exception as exc:
print(f"{sample!r} -> {type(exc).__name__}: {exc}")
PYRepository: dfetch-org/dfetch
Length of output: 499
🏁 Script executed:
cat -n dfetch/manifest/manifest.py | sed -n '60,70p'Repository: dfetch-org/dfetch
Length of output: 421
🏁 Script executed:
rg "_yaml_str" dfetch/manifest/manifest.py -A 3 -B 3Repository: dfetch-org/dfetch
Length of output: 791
🏁 Script executed:
rg "_yaml_str\(value\)" dfetch/manifest/manifest.py -B 10 -A 2Repository: dfetch-org/dfetch
Length of output: 820
🏁 Script executed:
# Let's also check imports to see if yaml.YAMLError exists
rg "^import|^from" dfetch/manifest/manifest.py | head -20Repository: dfetch-org/dfetch
Length of output: 827
🏁 Script executed:
# Check if yaml.YAMLError is available in the yaml module
python3 -c "import yaml; print(hasattr(yaml, 'YAMLError'))"Repository: dfetch-org/dfetch
Length of output: 65
🏁 Script executed:
# Look for tests or usages that show what kind of version strings might be passed
rg "version" dfetch/manifest/manifest.py -A 2 -B 2 | head -40Repository: dfetch-org/dfetch
Length of output: 1067
🏁 Script executed:
# Check the Project/ProjectEntry to understand what version field values look like
fd -e py | xargs rg "class ProjectEntry|class Project" | grep -E "^[^:]+:(class|@)"Repository: dfetch-org/dfetch
Length of output: 155
🏁 Script executed:
cat -n dfetch/manifest/project.py | sed -n '1,100p'Repository: dfetch-org/dfetch
Length of output: 4115
🏁 Script executed:
# Look for the version property in ProjectEntry
rg "version" dfetch/manifest/project.py -B 3 -A 3 | head -60Repository: dfetch-org/dfetch
Length of output: 1446
🏁 Script executed:
# Find the version property definition in ProjectEntry
rg "@property|def version" dfetch/manifest/project.py -A 5 | head -40Repository: dfetch-org/dfetch
Length of output: 1131
🏁 Script executed:
# Check the dataclass definition of ProjectEntry more thoroughly
cat -n dfetch/manifest/project.py | sed -n '150,250p'Repository: dfetch-org/dfetch
Length of output: 3988
🏁 Script executed:
# Find the Version class/namedtuple definition
rg "^class Version|^Version = " dfetch/manifest/project.py -A 5Repository: dfetch-org/dfetch
Length of output: 43
🏁 Script executed:
# Look at the context where _yaml_str is called with version fields
cat -n dfetch/manifest/manifest.py | sed -n '440,475p'Repository: dfetch-org/dfetch
Length of output: 2114
🏁 Script executed:
# Find Version namedtuple or class
rg "Version" dfetch/manifest/project.py | head -20Repository: dfetch-org/dfetch
Length of output: 298
🏁 Script executed:
# Get the actual Version import/definition
rg "from|import" dfetch/manifest/project.py | grep -i versionRepository: dfetch-org/dfetch
Length of output: 206
🏁 Script executed:
cat -n dfetch/manifest/version.pyRepository: dfetch-org/dfetch
Length of output: 1498
Handle parse errors in _yaml_str() by quoting instead of crashing.
Line 65 calls yaml.safe_load(value) to probe whether a string needs quoting. Arbitrary version strings—such as those starting with ! (YAML tags) or containing unterminated quotes—will raise yaml.YAMLError instead of returning a parsed value, causing freeze() to fail. Wrap the call in a try-except block and fall back to SingleQuotedScalarString(value) when parsing fails.
🩹 Suggested fix
def _yaml_str(value: str) -> str | SingleQuotedScalarString:
"""Return SingleQuotedScalarString if value would be misread as non-string."""
- if not isinstance(yaml.safe_load(value), str):
+ try:
+ parsed = yaml.safe_load(value)
+ except yaml.YAMLError:
+ return SingleQuotedScalarString(value)
+ if not isinstance(parsed, str):
return SingleQuotedScalarString(value)
return value🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@dfetch/manifest/manifest.py` around lines 63 - 67, The _yaml_str function
currently calls yaml.safe_load(value) which can raise yaml.YAMLError for inputs
like leading "!" or unterminated quotes; update _yaml_str to call
yaml.safe_load(value) inside a try/except that catches yaml.YAMLError (and
optionally Exception) and on exception returns SingleQuotedScalarString(value),
and otherwise keep the existing behavior (return SingleQuotedScalarString(value)
if parsed value is not a str, else return value); refer to the function name
_yaml_str and the use of yaml.safe_load and SingleQuotedScalarString when making
this change.
dfetch freezeso users canfreeze individual projects rather than all at once.
edits the manifest file in-place (preserving comments, blank lines and
indentation) instead of creating a
.backupcopy and regenerating fromscratch.
update_project_in_manifest_fileand helper functions indfetch/manifest/manifest.pyfor in-place YAML text editing.freeze-specific-projects.featureandfreeze-inplace.feature, plus the required step definitions.https://claude.ai/code/session_01Xd8EcAUkSoJo9YZimGzEuA
Summary by CodeRabbit
New Features
dfetch freezeaccepts optional project name arguments to freeze only selected projects.dfetch freezeupdates the manifest in-place while preserving comments and layout.Testing