Skip to content

feat: add GCP labels and tags support to CLI commands#30

Merged
tardigrde merged 4 commits intomainfrom
claude/add-cli-tag-options-3WTAx
Mar 29, 2026
Merged

feat: add GCP labels and tags support to CLI commands#30
tardigrde merged 4 commits intomainfrom
claude/add-cli-tag-options-3WTAx

Conversation

@tardigrde
Copy link
Copy Markdown
Owner

@tardigrde tardigrde commented Mar 26, 2026

Add opt-in support for GCP resource labels (key-value pairs) and resource
tags (Tag Manager bindings) across CLI commands. Labels are fetched via an
additional SQL column in Asset API queries (cheap). Tags require a separate
Asset API query against TagBinding resources (expensive). Both are only
fetched when explicitly requested via CLI flags.

New CLI options on ls, tree, find, ancestors:

  • --show-labels: display labels in output
  • --show-tags: display tags in output
  • --label key=value: filter by label (repeatable, ANDed)
  • --tag key=value: filter by tag (repeatable, ANDed)

Changes across the stack:

  • core.py: labels/tags fields on Folder and Project dataclasses
  • parsers.py: extract_labels() and has_labels param on parse functions
  • loaders.py: include_labels in SQL builders, load_tags_asset/apply_tags
  • cache.py: bump CACHE_VERSION to 2, serialize/deserialize labels+tags
  • serializers.py: include labels/tags in JSON/YAML output when non-empty
  • formatters.py: show labels/tags in tree view labels

https://claude.ai/code/session_01HugtU9fbaL97tbb7zaqNPL

Summary by CodeRabbit

  • New Features
    • Projects and folders now surface labels and tags; CLI can display and filter by them (show-labels/show-tags, repeatable label/tag filters)
    • Tree, ls and find outputs (long/JSON/tree) include metadata when requested
  • Other
    • Cache format version bumped; older caches are rejected
    • Hierarchy loading can include label/tag data
  • Tests
    • Expanded tests covering labels/tags, CLI behavior, serialization and cache handling

claude added 2 commits March 26, 2026 21:10
Add opt-in support for GCP resource labels (key-value pairs) and resource
tags (Tag Manager bindings) across CLI commands. Labels are fetched via an
additional SQL column in Asset API queries (cheap). Tags require a separate
Asset API query against TagBinding resources (expensive). Both are only
fetched when explicitly requested via CLI flags.

New CLI options on ls, tree, find, ancestors:
- --show-labels: display labels in output
- --show-tags: display tags in output
- --label key=value: filter by label (repeatable, ANDed)
- --tag key=value: filter by tag (repeatable, ANDed)

Changes across the stack:
- core.py: labels/tags fields on Folder and Project dataclasses
- parsers.py: extract_labels() and has_labels param on parse functions
- loaders.py: include_labels in SQL builders, load_tags_asset/apply_tags
- cache.py: bump CACHE_VERSION to 2, serialize/deserialize labels+tags
- serializers.py: include labels/tags in JSON/YAML output when non-empty
- formatters.py: show labels/tags in tree view labels

https://claude.ai/code/session_01HugtU9fbaL97tbb7zaqNPL
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 26, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds labels and tags end-to-end: Folder/Project dataclasses gain labels/tags; cache format bumped to v2; Asset API loaders can request labels and tag bindings; CLI gains show/filter flags for labels/tags; formatters, serializers, and tests updated accordingly.

Changes

Cohort / File(s) Summary
Core & Cache
src/gcpath/core.py, src/gcpath/cache.py, src/gcpath/serializers.py
Added labels/tags fields to Folder/Project. Increased cache CACHE_VERSION to 2; (de)serialization now emits/parses optional labels and tags.
Loaders & Parsers
src/gcpath/loaders.py, src/gcpath/parsers.py
Added include_labels to SQL builders and asset loaders; implemented extract_labels and has_labels parsing paths. Added tag-binding parsing, load_tags_asset, _resolve_project_parent, and apply_tags to attach tags to nodes.
CLI
src/gcpath/cli.py
Extended ls, tree, and find with --show-labels/--show-tags and repeatable --label/--tag filters; added parsing/matching/formatting helpers and threaded include flags into hierarchy loading.
Formatters
src/gcpath/formatters.py
Refactored child-collection helpers and added _format_metadata_suffix; format_tree_label and build_tree_view now accept and render show_labels/show_tags.
Tests & Fixtures
tests/fixtures/sample_cache_v2.json, tests/test_cache.py, tests/test_cli.py, tests/test_formatters.py, tests/test_loaders.py, tests/test_parsers.py, tests/test_serializers.py
Added v2 cache fixture and tests covering label/tag round-trip, v1 rejection, parser/extractor behavior, loader SQL & asset label handling, formatter rendering, CLI display and filtering.
Misc (config/formatting)
src/gcpath/config.py, various test refactors
Minor formatting and non-functional refactors (multi-line signatures, wrapping) with no behavioral changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as "CLI"
    participant Core as "Core\n(Hierarchy.load)"
    participant Cache as "Cache"
    participant Loaders as "Loaders"
    participant AssetAPI as "Asset API"

    User->>CLI: run command (e.g. --show-labels --label key=val)
    CLI->>Core: Hierarchy.load(include_labels=True, include_tags=True)
    Core->>Cache: check cached hierarchy (version)
    alt cache hit v2
        Cache-->>Core: cached hierarchy (with labels/tags)
    else cache miss or version mismatch
        Core->>Loaders: load_projects_asset(include_labels=True)
        Loaders->>AssetAPI: query projects (+labels)
        AssetAPI-->>Loaders: project rows
        Loaders->>Loaders: parse_project_row(has_labels=True)
        Loaders-->>Core: projects (labels set)

        Core->>Loaders: load_tags_asset(parent)
        Loaders->>AssetAPI: query TagBinding
        AssetAPI-->>Loaders: tag binding rows
        Loaders-->>Core: tags_map
        Core->>Loaders: apply_tags(hierarchy, tags_map)

        Core->>Cache: serialize and save hierarchy (version 2)
    end
    Core-->>CLI: Hierarchy with labels & tags
    CLI->>CLI: filter by --label/--tag and render with --show-labels/--show-tags
    CLI-->>User: display filtered results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I hopped through cache and SQL nights,
Gathered labels, tied tag lights,
CLI now shows what once was hid,
v2 saved all the things we did,
A little rabbit cheers this tidy bit.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: add GCP labels and tags support to CLI commands' directly and accurately summarizes the main change: adding labels and tags support to CLI commands.
Docstring Coverage ✅ Passed Docstring coverage is 89.80% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/add-cli-tag-options-3WTAx

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 80.51948% with 90 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.29%. Comparing base (8adfd85) to head (41aa1b0).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
src/gcpath/loaders.py 33.82% 45 Missing ⚠️
src/gcpath/cli.py 89.56% 19 Missing ⚠️
src/gcpath/core.py 79.56% 19 Missing ⚠️
src/gcpath/parsers.py 90.19% 5 Missing ⚠️
src/gcpath/serializers.py 83.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #30      +/-   ##
==========================================
- Coverage   89.55%   87.29%   -2.27%     
==========================================
  Files           9        9              
  Lines        1637     1786     +149     
==========================================
+ Hits         1466     1559      +93     
- Misses        171      227      +56     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

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

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 introduces support for GCP labels and tags across the gcpath tool. It updates the core data models, CLI commands (ls, tree, find, ancestors), and the caching mechanism (bumping the cache version to 2) to handle these metadata fields. Users can now display labels and tags in various output formats and filter resources using the new --label and --tag options. Review feedback highlights that the label and tag options for the ancestors command are currently unimplemented and suggests refactoring duplicate filtering and formatting logic into generic helper functions.

Comment thread src/gcpath/cli.py Outdated
Comment on lines +1361 to +1366
show_labels: bool = typer.Option(
False, "--show-labels", help="Display GCP labels on resources"
),
show_tags: bool = typer.Option(
False, "--show-tags", help="Display GCP resource tags"
),
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.

high

The --show-labels and --show-tags options are added to the ancestors command, but they don't appear to be implemented. The command output does not include labels or tags, and the underlying Hierarchy.resolve_ancestry_chain method doesn't fetch this information. These options should either be fully implemented or removed to avoid user confusion.

Comment thread src/gcpath/cli.py Outdated
Comment on lines +93 to +145
def _matches_labels(
obj: Union[OrganizationNode, Folder, Project],
label_filters: List[str],
) -> bool:
"""Check if a resource matches ALL label filters (ANDed)."""
if not label_filters:
return True
labels = getattr(obj, "labels", {})
for lf in label_filters:
key, value = _parse_label_filter(lf)
if value is None:
# Key-only filter: check if key exists
if key not in labels:
return False
else:
if labels.get(key) != value:
return False
return True


def _matches_tags(
obj: Union[OrganizationNode, Folder, Project],
tag_filters: List[str],
) -> bool:
"""Check if a resource matches ALL tag filters (ANDed)."""
if not tag_filters:
return True
tags = getattr(obj, "tags", {})
for tf in tag_filters:
key, value = _parse_label_filter(tf)
if value is None:
if key not in tags:
return False
else:
if tags.get(key) != value:
return False
return True


def _format_labels(obj: Union[OrganizationNode, Folder, Project]) -> str:
"""Format labels as comma-separated key=value string."""
labels = getattr(obj, "labels", {})
if not labels:
return ""
return ", ".join(f"{k}={v}" for k, v in sorted(labels.items()))


def _format_tags(obj: Union[OrganizationNode, Folder, Project]) -> str:
"""Format tags as comma-separated key=value string."""
tags = getattr(obj, "tags", {})
if not tags:
return ""
return ", ".join(f"{k}={v}" for k, v in sorted(tags.items()))
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.

medium

There's some code duplication in the newly added helper functions: _matches_labels and _matches_tags are nearly identical, as are _format_labels and _format_tags. To improve maintainability, consider creating generic helper functions that can handle both labels and tags by taking the attribute name as a parameter. This aligns with the practice of refactoring logic into explicit helper functions for better clarity.

References
  1. When logic is repetitive or complex, refactor it into a more explicit helper function to improve maintainability and readability.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/gcpath/serializers.py (1)

72-77: ⚠️ Potential issue | 🟠 Major

Tree serialization drops folder labels/tags.

Folder nodes produced via _node_to_dict never include labels/tags, so serialize_tree omits metadata for folders even when present.

💡 Proposed fix
 def _node_to_dict(node: Union[OrganizationNode, Folder]) -> Tuple[str, Dict[str, Any]]:
@@
-    return node.name, {
+    d: Dict[str, Any] = {
         "path": node.path,
         "resource_name": node.name,
         "display_name": node.display_name,
         "type": "folder",
     }
+    if node.labels:
+        d["labels"] = node.labels
+    if node.tags:
+        d["tags"] = node.tags
+    return node.name, d
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gcpath/serializers.py` around lines 72 - 77, The folder branch in
_node_to_dict returns a dict with "path", "resource_name", "display_name", and
"type" but drops folder metadata like labels/tags, causing serialize_tree to
omit them; update _node_to_dict (the folder-returning branch) to include the
node's labels and/or tags (e.g., "labels": node.labels and/or "tags": node.tags)
in the returned mapping so serialize_tree will preserve folder metadata, and
ensure keys match what serialize_tree expects.
src/gcpath/cli.py (3)

1355-1394: ⚠️ Potential issue | 🟠 Major

ancestors' new metadata flags are no-ops.

show_labels and show_tags are parsed, but this command still resolves only (resource_name, display_name, type) and never loads or renders metadata in text/JSON/YAML output. Either wire the flags into a metadata-aware lookup path or remove them for now.

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

In `@src/gcpath/cli.py` around lines 1355 - 1394, The new show_labels and
show_tags options are parsed but unused; update the ancestors command to pass
these flags into the ancestry lookup and output logic: call
Hierarchy.resolve_ancestry_chain(resource_name, include_labels=show_labels,
include_tags=show_tags) (or add equivalent params) so the resolver returns
metadata alongside (resource_name, display_name, type), then update
serialize_ancestors and the text/table rendering in ancestors to include
label/tag fields when present (add columns and include metadata in JSON/YAML
dump via _get_dumper), or if you prefer not to support metadata yet, remove the
show_labels and show_tags options from the ancestors function signature to avoid
no-ops.

441-484: ⚠️ Potential issue | 🟠 Major

Don't reuse lean caches for metadata-driven commands.

_try_read_cache() is still keyed only by scope, so a cache populated by plain ls/tree/find is reused for later --show-labels, --show-tags, --label, or --tag runs. Those commands then render/filter against empty dicts and silently miss matches.

Minimal safe fix
-    if is_cacheable and not force_refresh:
+    requires_fresh_metadata = include_labels or include_tags
+    if is_cacheable and not force_refresh and not requires_fresh_metadata:
         cached = _try_read_cache(cache_scope, filter_orgs)
         if cached is not None:
             return cached
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gcpath/cli.py` around lines 441 - 484, The cached hierarchy is being
reused for metadata-driven commands because _try_read_cache is only keyed by
scope; to fix, in _load_hierarchy treat requests that need metadata as
non-cacheable: when include_labels or include_tags is True (and any other
metadata-affecting flags/commands you support, e.g., label/tag mutators), set
is_cacheable = False (or clear cache_scope) so the function skips
reading/writing the lean cache; adjust the is_cacheable calculation near the
is_cacheable variable and ensure write_cache is not invoked for those metadata
requests (references: function _load_hierarchy, variables is_cacheable,
cache_scope, effective_recursive, calls to _try_read_cache and write_cache).

902-1002: ⚠️ Potential issue | 🟠 Major

tree's new label/tag options are only partially wired.

The flags currently trigger metadata loading and descendant formatting, but label_filters / tag_filters never prune the tree, so gcpath tree --label ... still renders the full hierarchy. A scoped root folder is also rendered from node.path, so --show-labels / --show-tags won't surface metadata on that root node either.

🧹 Nitpick comments (3)
tests/test_serializers.py (1)

226-257: Add a tree-serialization metadata regression test.

This class validates serialize_resource, but not folder metadata in serialize_tree_node/serialize_tree. A targeted tree test would catch metadata loss on folder nodes.

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

In `@tests/test_serializers.py` around lines 226 - 257, The test suite covers
serialize_resource but misses verifying folder metadata when serializing entire
trees; add a new test (e.g., in tests/test_serializers.py alongside
TestSerializeResourceWithLabelsAndTags) that constructs a hierarchy with folder
labels/tags, calls serialize_tree_node or serialize_tree on the root, and
asserts that folder nodes in the returned tree retain their "labels" and "tags"
(and that empty ones are omitted) to catch regressions in
serialize_tree_node/serialize_tree handling of folder metadata; reference
serialize_tree_node, serialize_tree, and serialize_resource to locate relevant
serialization logic and mirror the existing assertions for folders and projects.
tests/test_parsers.py (1)

333-351: Add a wrapped-value case for extract_labels.

Please add a case where label map values are wrapper objects (e.g., {"env": {"v": "prod"}}) so parser behavior is validated against MapComposite-like value shapes.

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

In `@tests/test_parsers.py` around lines 333 - 351, Add a new unit test for
extract_labels that covers wrapped-map values (MapComposite-like shapes): create
a labels_col such as {"v": {"env": {"v": "prod"}, "team": {"v": "infra"}}}, call
extract_labels(labels_col) and assert it returns {"env": "prod", "team":
"infra"}; name it something like test_extract_labels_wrapped_values and place it
alongside the other extract_labels tests to validate unwrapping behavior in the
extract_labels function.
tests/test_cli.py (1)

1251-1310: Assert Hierarchy.load opt-in flags in CLI tests.

The new tests verify output, but they don’t check include_labels / include_tags kwargs on Hierarchy.load. Adding those assertions will guard the cost-sensitive opt-in behavior.

💡 Example assertion pattern
 result = runner.invoke(app, ["ls", "--show-labels", "-l"])
 assert result.exit_code == 0
+mock_load.assert_called_once()
+assert mock_load.call_args.kwargs.get("include_labels") is True
 result = runner.invoke(app, ["ls", "--show-tags", "-l", "-R"])
 assert result.exit_code == 0
+mock_load.assert_called_once()
+assert mock_load.call_args.kwargs.get("include_tags") is True
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cli.py` around lines 1251 - 1310, Update the four CLI tests to
assert that Hierarchy.load is called with the correct opt-in flags: in
test_ls_show_labels assert mock_load.called and
mock_load.assert_called_with(..., include_labels=True) (or at least check
include_labels=True in the call kwargs) since runner.invoke(app, ["ls",
"--show-labels", "-l"]) should opt in; in test_ls_label_filter and
test_ls_label_filter_key_only verify Hierarchy.load was called with
include_labels=True because they invoke ["ls", "-R", "--label", ...]; and in
test_ls_show_tags_long assert mock_load was called with include_tags=True for
runner.invoke(app, ["ls", "--show-tags", "-l", "-R"]); for tests that do not
request labels/tags ensure include_labels/include_tags are False (or absent)
accordingly by checking the call kwargs on mock_load to guard the opt-in
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/gcpath/core.py`:
- Around line 226-230: The tag-loading block currently always calls
load_tags_asset(org_node.organization.name) which queries TagBindings at the org
root; change it to use the same scope used for the resource load so
folder-scoped runs only fetch tags within that scope. Specifically, in the
include_tags && !via_resource_manager branch replace the org root argument with
the actual scope identifier used elsewhere (e.g., the folder or project resource
name stored on org_node or the top-level scope variable) so load_tags_asset(...)
is invoked with the requested scope and then call apply_tags(hierarchy,
tags_map) as before.

---

Outside diff comments:
In `@src/gcpath/cli.py`:
- Around line 1355-1394: The new show_labels and show_tags options are parsed
but unused; update the ancestors command to pass these flags into the ancestry
lookup and output logic: call Hierarchy.resolve_ancestry_chain(resource_name,
include_labels=show_labels, include_tags=show_tags) (or add equivalent params)
so the resolver returns metadata alongside (resource_name, display_name, type),
then update serialize_ancestors and the text/table rendering in ancestors to
include label/tag fields when present (add columns and include metadata in
JSON/YAML dump via _get_dumper), or if you prefer not to support metadata yet,
remove the show_labels and show_tags options from the ancestors function
signature to avoid no-ops.
- Around line 441-484: The cached hierarchy is being reused for metadata-driven
commands because _try_read_cache is only keyed by scope; to fix, in
_load_hierarchy treat requests that need metadata as non-cacheable: when
include_labels or include_tags is True (and any other metadata-affecting
flags/commands you support, e.g., label/tag mutators), set is_cacheable = False
(or clear cache_scope) so the function skips reading/writing the lean cache;
adjust the is_cacheable calculation near the is_cacheable variable and ensure
write_cache is not invoked for those metadata requests (references: function
_load_hierarchy, variables is_cacheable, cache_scope, effective_recursive, calls
to _try_read_cache and write_cache).

In `@src/gcpath/serializers.py`:
- Around line 72-77: The folder branch in _node_to_dict returns a dict with
"path", "resource_name", "display_name", and "type" but drops folder metadata
like labels/tags, causing serialize_tree to omit them; update _node_to_dict (the
folder-returning branch) to include the node's labels and/or tags (e.g.,
"labels": node.labels and/or "tags": node.tags) in the returned mapping so
serialize_tree will preserve folder metadata, and ensure keys match what
serialize_tree expects.

---

Nitpick comments:
In `@tests/test_cli.py`:
- Around line 1251-1310: Update the four CLI tests to assert that Hierarchy.load
is called with the correct opt-in flags: in test_ls_show_labels assert
mock_load.called and mock_load.assert_called_with(..., include_labels=True) (or
at least check include_labels=True in the call kwargs) since runner.invoke(app,
["ls", "--show-labels", "-l"]) should opt in; in test_ls_label_filter and
test_ls_label_filter_key_only verify Hierarchy.load was called with
include_labels=True because they invoke ["ls", "-R", "--label", ...]; and in
test_ls_show_tags_long assert mock_load was called with include_tags=True for
runner.invoke(app, ["ls", "--show-tags", "-l", "-R"]); for tests that do not
request labels/tags ensure include_labels/include_tags are False (or absent)
accordingly by checking the call kwargs on mock_load to guard the opt-in
behavior.

In `@tests/test_parsers.py`:
- Around line 333-351: Add a new unit test for extract_labels that covers
wrapped-map values (MapComposite-like shapes): create a labels_col such as {"v":
{"env": {"v": "prod"}, "team": {"v": "infra"}}}, call extract_labels(labels_col)
and assert it returns {"env": "prod", "team": "infra"}; name it something like
test_extract_labels_wrapped_values and place it alongside the other
extract_labels tests to validate unwrapping behavior in the extract_labels
function.

In `@tests/test_serializers.py`:
- Around line 226-257: The test suite covers serialize_resource but misses
verifying folder metadata when serializing entire trees; add a new test (e.g.,
in tests/test_serializers.py alongside TestSerializeResourceWithLabelsAndTags)
that constructs a hierarchy with folder labels/tags, calls serialize_tree_node
or serialize_tree on the root, and asserts that folder nodes in the returned
tree retain their "labels" and "tags" (and that empty ones are omitted) to catch
regressions in serialize_tree_node/serialize_tree handling of folder metadata;
reference serialize_tree_node, serialize_tree, and serialize_resource to locate
relevant serialization logic and mirror the existing assertions for folders and
projects.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc820f85-fe5d-4e1b-a3c2-7cd35a0fd478

📥 Commits

Reviewing files that changed from the base of the PR and between 8adfd85 and 369e46e.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • src/gcpath/cache.py
  • src/gcpath/cli.py
  • src/gcpath/core.py
  • src/gcpath/formatters.py
  • src/gcpath/loaders.py
  • src/gcpath/parsers.py
  • src/gcpath/serializers.py
  • tests/fixtures/sample_cache_v2.json
  • tests/test_cache.py
  • tests/test_cli.py
  • tests/test_formatters.py
  • tests/test_loaders.py
  • tests/test_parsers.py
  • tests/test_serializers.py

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/gcpath/cli.py (2)

436-455: ⚠️ Potential issue | 🟠 Major

Don’t reuse metadata-incomplete cache entries.

The cache lookup here ignores include_labels and include_tags. If the cache was populated by a plain load, later --show-labels, --show-tags, --label, or --tag commands will read empty metadata from cache and produce wrong output until -F is used. Gate cache reuse on metadata completeness, or persist has_labels / has_tags in cache metadata and validate it here.

🩹 Safe stopgap
-    if is_cacheable and not force_refresh:
+    if is_cacheable and not force_refresh and not (include_labels or include_tags):
         cached = _try_read_cache(cache_scope, filter_orgs)
         if cached is not None:
             return cached
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gcpath/cli.py` around lines 436 - 455, The cache lookup currently reuses
entries regardless of requested metadata flags (include_labels/include_tags),
causing incomplete metadata to be returned; update the cache validation in the
_try_read_cache / cache reuse logic to verify that the cached entry actually
contains the requested metadata (either by persisting and checking cache
metadata fields like has_labels and has_tags when write_cache is called, or by
gating reuse based on include_labels/include_tags), and only return cached when
those metadata completeness flags match the current request; if they don’t
match, treat as cache miss (fall through to Hierarchy.load and then write_cache
with the appropriate has_labels/has_tags metadata).

815-840: ⚠️ Potential issue | 🟠 Major

ls --show-labels / --show-tags is a no-op in default text mode.

The new metadata columns are only added inside the if long: branch. In the default path, ls --show-labels still prints bare paths, so the advertised flags look broken unless callers also know to add --long. Either switch to the table automatically when metadata display is requested, or append metadata to the plain-text line format.

💡 Minimal local fix
-        if long:
+        if long or show_labels or show_tags:
             table = Table(
                 show_header=True, header_style="bold magenta", box=None, padding=(0, 1)
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gcpath/cli.py` around lines 815 - 840, The flags show_labels/show_tags
are ignored when long is False because the plain-text branch prints only paths;
change the else branch to iterate for path, obj in items (not path, _) and
append metadata using _format_metadata(obj, "labels") and/or
_format_metadata(obj, "tags") to the printed line when show_labels or show_tags
is set (or alternatively switch to using the table output when either flag is
true); update the loop in the non-long branch to build a single string per item
that includes the path plus the requested metadata fields and print that.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/gcpath/cli.py`:
- Around line 895-902: The tree command currently only toggles
include_labels/include_tags but never prunes the hierarchy by
label_filters/tag_filters, so implement a pruning step that filters the built
hierarchy to only nodes matching the provided label_filters/tag_filters plus any
ancestors required to reach them; add a helper (e.g. prune_tree_by_filters) and
call it in the tree path after _prepare_hierarchy_command and before
build_tree_view/serialization (or modify build_tree_view to accept
label_filters/tag_filters and do the pruning internally), using the existing
label_filters and tag_filters variables and preserving
include_labels/include_tags for display.

In `@src/gcpath/core.py`:
- Around line 226-233: The code currently skips tag loading when
via_resource_manager is true, causing --show-tags/--tag to silently return no
bindings; update the logic in the tag-loading blocks (the if guard that checks
include_tags and not via_resource_manager) to either (A) raise/fail fast with a
clear error when include_tags is requested but
via_resource_manager/--no-use-asset-api prevents tag queries, or (B) allow tag
binding queries to run even when via_resource_manager is true by invoking
load_tags_asset(tag_scope) and apply_tags(hierarchy, tags_map) for the same
tag_scopes; make the same change for the other occurrence referenced around
lines 388-390 so tag behavior is consistent across both code paths.

In `@src/gcpath/formatters.py`:
- Around line 171-186: _format_metadata_suffix builds Rich markup fragments in
parts (e.g., parts entries already contain [dim]...[/dim]) but currently wraps
the whole suffix in literal brackets ("[" + ... + "]"), causing Rich to escape
and render them as literal text; remove the outer brackets so the function
returns a leading space plus the joined parts (i.e., " " + " | ".join(parts)) so
Rich can parse the inner [dim] markup correctly; update the return expression in
_format_metadata_suffix and keep using show_labels/show_tags, parts, label_str
and tag_str as before.

---

Outside diff comments:
In `@src/gcpath/cli.py`:
- Around line 436-455: The cache lookup currently reuses entries regardless of
requested metadata flags (include_labels/include_tags), causing incomplete
metadata to be returned; update the cache validation in the _try_read_cache /
cache reuse logic to verify that the cached entry actually contains the
requested metadata (either by persisting and checking cache metadata fields like
has_labels and has_tags when write_cache is called, or by gating reuse based on
include_labels/include_tags), and only return cached when those metadata
completeness flags match the current request; if they don’t match, treat as
cache miss (fall through to Hierarchy.load and then write_cache with the
appropriate has_labels/has_tags metadata).
- Around line 815-840: The flags show_labels/show_tags are ignored when long is
False because the plain-text branch prints only paths; change the else branch to
iterate for path, obj in items (not path, _) and append metadata using
_format_metadata(obj, "labels") and/or _format_metadata(obj, "tags") to the
printed line when show_labels or show_tags is set (or alternatively switch to
using the table output when either flag is true); update the loop in the
non-long branch to build a single string per item that includes the path plus
the requested metadata fields and print that.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e0ebbe4a-c8cb-41d4-9d37-909e0855b975

📥 Commits

Reviewing files that changed from the base of the PR and between 369e46e and c06c9e7.

📒 Files selected for processing (6)
  • src/gcpath/cache.py
  • src/gcpath/cli.py
  • src/gcpath/core.py
  • src/gcpath/formatters.py
  • src/gcpath/loaders.py
  • src/gcpath/parsers.py
✅ Files skipped from review due to trivial changes (1)
  • src/gcpath/cache.py

Comment thread src/gcpath/cli.py Outdated
Comment on lines 895 to 902
# Implicitly enable label/tag fetching when filters are specified
include_labels = show_labels or bool(label_filters)
include_tags = show_tags or bool(tag_filters)

hctx = _prepare_hierarchy_command(
ctx, "tree", resource, level, yes, force_refresh
ctx, "tree", resource, level, yes, force_refresh,
include_labels=include_labels, include_tags=include_tags,
)
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

Apply --label / --tag to the tree output.

These flags currently only flip include_labels / include_tags; neither the serializer path nor build_tree_view() ever filters by label_filters / tag_filters. gcpath tree --label ... still renders the full hierarchy. The tree path needs a pruning step that keeps matching nodes plus the ancestors needed to reach them.

Also applies to: 914-976

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

In `@src/gcpath/cli.py` around lines 895 - 902, The tree command currently only
toggles include_labels/include_tags but never prunes the hierarchy by
label_filters/tag_filters, so implement a pruning step that filters the built
hierarchy to only nodes matching the provided label_filters/tag_filters plus any
ancestors required to reach them; add a helper (e.g. prune_tree_by_filters) and
call it in the tree path after _prepare_hierarchy_command and before
build_tree_view/serialization (or modify build_tree_view to accept
label_filters/tag_filters and do the pruning internally), using the existing
label_filters and tag_filters variables and preserving
include_labels/include_tags for display.

Comment thread src/gcpath/core.py
Comment on lines +226 to +233
# Load tags if requested (separate Asset API query)
if include_tags and not via_resource_manager:
tag_scopes = [scope_resource] if scope_resource else [
org_node.organization.name for org_node in org_nodes
]
for tag_scope in tag_scopes:
tags_map = load_tags_asset(tag_scope)
apply_tags(hierarchy, tags_map)
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

Tag requests are silently dropped in Resource Manager mode.

Both tag-loading paths still guard on not via_resource_manager, but the CLI now exposes --show-tags / --tag regardless of backend. In practice, --no-use-asset-api will load no tag bindings and can return empty or incorrectly filtered results. Either fail fast on that flag combination, or document and allow the separate TagBinding query even when folders/projects come from Resource Manager.

Also applies to: 388-390

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

In `@src/gcpath/core.py` around lines 226 - 233, The code currently skips tag
loading when via_resource_manager is true, causing --show-tags/--tag to silently
return no bindings; update the logic in the tag-loading blocks (the if guard
that checks include_tags and not via_resource_manager) to either (A) raise/fail
fast with a clear error when include_tags is requested but
via_resource_manager/--no-use-asset-api prevents tag queries, or (B) allow tag
binding queries to run even when via_resource_manager is true by invoking
load_tags_asset(tag_scope) and apply_tags(hierarchy, tags_map) for the same
tag_scopes; make the same change for the other occurrence referenced around
lines 388-390 so tag behavior is consistent across both code paths.

Comment thread src/gcpath/formatters.py
Comment on lines +171 to +186
def _format_metadata_suffix(
item: Union[Folder, Project],
show_labels: bool = False,
show_tags: bool = False,
) -> str:
"""Build a Rich markup suffix for labels and tags."""
parts = []
if show_labels and hasattr(item, "labels") and item.labels:
label_str = ", ".join(f"{k}={v}" for k, v in sorted(item.labels.items()))
parts.append(f"[dim]labels: {label_str}[/dim]")
if show_tags and hasattr(item, "tags") and item.tags:
tag_str = ", ".join(f"{k}={v}" for k, v in sorted(item.tags.items()))
parts.append(f"[dim]tags: {tag_str}[/dim]")
if not parts:
return ""
return " [" + " | ".join(parts) + "]"
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import subprocess
import sys

try:
    from rich.text import Text
except ImportError:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "rich"])
    from rich.text import Text

samples = [
    " [dim]labels: env=prod[/dim]",
    " [[dim]labels: env=prod[/dim]]",
]

for sample in samples:
    try:
        text = Text.from_markup(sample)
        print("OK ", repr(sample), "->", repr(text.plain))
    except Exception as exc:
        print("ERR", repr(sample), type(exc).__name__, exc)
PY

Repository: tardigrde/gcpath

Length of output: 179


🏁 Script executed:

cat -n src/gcpath/formatters.py | sed -n '171,186p'

Repository: tardigrde/gcpath

Length of output: 845


Don't wrap Rich markup fragments in literal [].

_format_metadata_suffix() returns strings like " [[dim]labels: env=prod[/dim]]". In Rich, the leading [[ escapes the bracket and produces literal output [labels: env=prod] without the dim formatting applied. Remove the outer brackets to properly parse the markup.

💡 Minimal fix
-    return " [" + " | ".join(parts) + "]"
+    return " " + " | ".join(parts)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gcpath/formatters.py` around lines 171 - 186, _format_metadata_suffix
builds Rich markup fragments in parts (e.g., parts entries already contain
[dim]...[/dim]) but currently wraps the whole suffix in literal brackets ("[" +
... + "]"), causing Rich to escape and render them as literal text; remove the
outer brackets so the function returns a leading space plus the joined parts
(i.e., " " + " | ".join(parts)) so Rich can parse the inner [dim] markup
correctly; update the return expression in _format_metadata_suffix and keep
using show_labels/show_tags, parts, label_str and tag_str as before.

- Remove unimplemented --show-labels/--show-tags from ancestors command
- Deduplicate _matches_labels/_matches_tags into generic _matches_metadata
- Deduplicate _format_labels/_format_tags into generic _format_metadata
- Use scope_resource for tag lookups instead of always querying org root
- Replace duplicated "organizations/" literal with _RESOURCE_PREFIX_ORGS constant
- Sanitize user-controlled data from cache log message
- Fix single-iteration loop in build_folder_ancestors (parsers.py)
- Reduce cognitive complexity across cli.py, core.py, formatters.py,
  loaders.py, and parsers.py by extracting helper functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

@tardigrde tardigrde merged commit c756062 into main Mar 29, 2026
5 of 8 checks passed
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