attocode-code-intel 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""File content and tree endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import mimetypes
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
10
|
+
|
|
11
|
+
from attocode_intel.api.auth import verify_auth
|
|
12
|
+
from attocode_intel.api.deps import BranchParam, ensure_branch_supported, get_service_or_404
|
|
13
|
+
from attocode_intel.api.models import (
|
|
14
|
+
FileContentResponse,
|
|
15
|
+
FileTreeResponse,
|
|
16
|
+
TreeEntry,
|
|
17
|
+
)
|
|
18
|
+
from attocode_intel.api.utils import LANG_MAP, MAX_FILE_SIZE, detect_language
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
router = APIRouter(
|
|
23
|
+
prefix="/api/v1/projects/{project_id}",
|
|
24
|
+
tags=["files"],
|
|
25
|
+
dependencies=[Depends(verify_auth)],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_path(project_dir: str, rel_path: str) -> str:
|
|
30
|
+
"""Resolve and validate a relative path within the project."""
|
|
31
|
+
abs_path = os.path.realpath(os.path.join(project_dir, rel_path))
|
|
32
|
+
real_root = os.path.realpath(project_dir)
|
|
33
|
+
if not abs_path.startswith(real_root + os.sep) and abs_path != real_root:
|
|
34
|
+
raise HTTPException(status_code=400, detail="Path traversal not allowed")
|
|
35
|
+
return abs_path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.get("/files/{path:path}", response_model=FileContentResponse)
|
|
39
|
+
async def get_file_content(
|
|
40
|
+
project_id: str,
|
|
41
|
+
path: str,
|
|
42
|
+
branch: BranchParam = "",
|
|
43
|
+
) -> FileContentResponse:
|
|
44
|
+
"""Read file content with metadata."""
|
|
45
|
+
ensure_branch_supported(branch)
|
|
46
|
+
svc = await get_service_or_404(project_id)
|
|
47
|
+
abs_path = _resolve_path(svc.project_dir, path)
|
|
48
|
+
|
|
49
|
+
if not os.path.isfile(abs_path):
|
|
50
|
+
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
|
51
|
+
|
|
52
|
+
stat = os.stat(abs_path)
|
|
53
|
+
if stat.st_size > MAX_FILE_SIZE:
|
|
54
|
+
raise HTTPException(
|
|
55
|
+
status_code=413,
|
|
56
|
+
detail=f"File too large ({stat.st_size} bytes, max {MAX_FILE_SIZE})",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Check if binary
|
|
60
|
+
mime_type, _ = mimetypes.guess_type(abs_path)
|
|
61
|
+
if mime_type and not mime_type.startswith("text/") and mime_type != "application/json":
|
|
62
|
+
# Allow known text formats
|
|
63
|
+
ext = os.path.splitext(abs_path)[1].lower()
|
|
64
|
+
if ext not in LANG_MAP and ext not in {".cfg", ".ini", ".env", ".gitignore"}:
|
|
65
|
+
raise HTTPException(status_code=415, detail="Binary file — content not available via this endpoint")
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
with open(abs_path, encoding="utf-8", errors="replace") as f:
|
|
69
|
+
content = f.read()
|
|
70
|
+
except OSError as exc:
|
|
71
|
+
logger.exception("Error reading %s", abs_path)
|
|
72
|
+
raise HTTPException(status_code=500, detail="Error reading file") from exc
|
|
73
|
+
|
|
74
|
+
line_count = content.count("\n") + (1 if content and not content.endswith("\n") else 0)
|
|
75
|
+
|
|
76
|
+
return FileContentResponse(
|
|
77
|
+
path=path,
|
|
78
|
+
content=content,
|
|
79
|
+
language=detect_language(path),
|
|
80
|
+
size_bytes=stat.st_size,
|
|
81
|
+
line_count=line_count,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.get("/tree/{path:path}", response_model=FileTreeResponse)
|
|
86
|
+
async def get_file_tree(
|
|
87
|
+
project_id: str,
|
|
88
|
+
path: str = "",
|
|
89
|
+
branch: BranchParam = "",
|
|
90
|
+
) -> FileTreeResponse:
|
|
91
|
+
"""List directory entries."""
|
|
92
|
+
ensure_branch_supported(branch)
|
|
93
|
+
svc = await get_service_or_404(project_id)
|
|
94
|
+
abs_path = _resolve_path(svc.project_dir, path) if path else svc.project_dir
|
|
95
|
+
|
|
96
|
+
if not os.path.isdir(abs_path):
|
|
97
|
+
raise HTTPException(status_code=404, detail=f"Directory not found: {path}")
|
|
98
|
+
|
|
99
|
+
entries: list[TreeEntry] = []
|
|
100
|
+
try:
|
|
101
|
+
for entry in sorted(os.scandir(abs_path), key=lambda e: (not e.is_dir(), e.name)):
|
|
102
|
+
# Skip hidden files and common noise
|
|
103
|
+
if entry.name.startswith(".") or entry.name in {"__pycache__", "node_modules", ".git"}:
|
|
104
|
+
continue
|
|
105
|
+
if entry.is_dir(follow_symlinks=False):
|
|
106
|
+
entries.append(TreeEntry(name=entry.name, type="dir"))
|
|
107
|
+
elif entry.is_file(follow_symlinks=False):
|
|
108
|
+
try:
|
|
109
|
+
size = entry.stat().st_size
|
|
110
|
+
except OSError:
|
|
111
|
+
size = None
|
|
112
|
+
entries.append(TreeEntry(
|
|
113
|
+
name=entry.name,
|
|
114
|
+
type="file",
|
|
115
|
+
size_bytes=size,
|
|
116
|
+
language=detect_language(entry.name),
|
|
117
|
+
))
|
|
118
|
+
except OSError as exc:
|
|
119
|
+
logger.exception("Error reading directory %s", abs_path)
|
|
120
|
+
raise HTTPException(status_code=500, detail="Error reading directory") from exc
|
|
121
|
+
|
|
122
|
+
return FileTreeResponse(path=path or ".", entries=entries)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@router.get("/tree", response_model=FileTreeResponse)
|
|
126
|
+
async def get_root_tree(
|
|
127
|
+
project_id: str,
|
|
128
|
+
branch: BranchParam = "",
|
|
129
|
+
) -> FileTreeResponse:
|
|
130
|
+
"""List root directory entries."""
|
|
131
|
+
ensure_branch_supported(branch)
|
|
132
|
+
return await get_file_tree(project_id, path="", branch=branch)
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Service-mode file/tree browsing via bare git repos (v2).
|
|
2
|
+
|
|
3
|
+
Supports two backends:
|
|
4
|
+
1. Bare git clone on disk (repos with clone_path) — uses GitManager
|
|
5
|
+
2. DB-backed manifest (remote-connected repos without clone_path) — uses BranchOverlay + ContentStore
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
from sqlalchemy import func, select
|
|
16
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
17
|
+
|
|
18
|
+
from attocode_intel.api.auth import resolve_auth
|
|
19
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
20
|
+
from attocode_intel.api.deps import get_branch_context, get_db_session, get_git_manager
|
|
21
|
+
from attocode_intel.api.utils import (
|
|
22
|
+
MAX_FILE_SIZE,
|
|
23
|
+
detect_language,
|
|
24
|
+
is_binary,
|
|
25
|
+
)
|
|
26
|
+
from attocode_intel.api.utils import get_repo_by_id as _get_repo
|
|
27
|
+
|
|
28
|
+
router = APIRouter(prefix="/api/v2/projects/{project_id}", tags=["files-v2"])
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --- Response models ---
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TreeEntryV2(BaseModel):
|
|
36
|
+
name: str
|
|
37
|
+
path: str
|
|
38
|
+
type: str # file|directory
|
|
39
|
+
size: int = 0
|
|
40
|
+
language: str = ""
|
|
41
|
+
children: list[TreeEntryV2] | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TreeResponse(BaseModel):
|
|
45
|
+
path: str
|
|
46
|
+
ref: str
|
|
47
|
+
entries: list[TreeEntryV2]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FileContentV2Response(BaseModel):
|
|
51
|
+
path: str
|
|
52
|
+
ref: str
|
|
53
|
+
content: str
|
|
54
|
+
language: str
|
|
55
|
+
size_bytes: int
|
|
56
|
+
line_count: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RepoStatsResponse(BaseModel):
|
|
60
|
+
total_files: int = 0
|
|
61
|
+
total_symbols: int = 0
|
|
62
|
+
languages: dict[str, int] = {}
|
|
63
|
+
embedded_files: int = 0
|
|
64
|
+
total_size_bytes: int = 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --- Helpers ---
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def _resolve_ref(repo, ref: str) -> str:
|
|
71
|
+
"""Resolve ref to use — defaults to repo's default_branch."""
|
|
72
|
+
return ref if ref else repo.default_branch
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def _build_tree_from_manifest(
|
|
76
|
+
session: AsyncSession,
|
|
77
|
+
repo_id: uuid.UUID,
|
|
78
|
+
ref: str,
|
|
79
|
+
path: str = "",
|
|
80
|
+
) -> list[TreeEntryV2]:
|
|
81
|
+
"""Build nested tree from BranchFile manifest (for repos without bare git clones).
|
|
82
|
+
|
|
83
|
+
Resolves the manifest once, then builds the tree in-memory.
|
|
84
|
+
"""
|
|
85
|
+
from attocode_intel.db.models import FileContent
|
|
86
|
+
|
|
87
|
+
branch_ctx = await get_branch_context(repo_id, ref, session)
|
|
88
|
+
manifest = branch_ctx.manifest # dict[str, str] path → sha
|
|
89
|
+
|
|
90
|
+
# Fetch file sizes in bulk
|
|
91
|
+
shas = list(set(manifest.values()))
|
|
92
|
+
sizes: dict[str, int] = {}
|
|
93
|
+
if shas:
|
|
94
|
+
result = await session.execute(
|
|
95
|
+
select(FileContent.sha256, FileContent.size_bytes)
|
|
96
|
+
.where(FileContent.sha256.in_(shas))
|
|
97
|
+
)
|
|
98
|
+
sizes = {row[0]: row[1] for row in result}
|
|
99
|
+
|
|
100
|
+
return _assemble_tree(manifest, sizes, path)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _assemble_tree(
|
|
104
|
+
manifest: dict[str, str],
|
|
105
|
+
sizes: dict[str, int],
|
|
106
|
+
path: str = "",
|
|
107
|
+
) -> list[TreeEntryV2]:
|
|
108
|
+
"""Build a nested TreeEntryV2 list from a flat manifest (pure in-memory, no DB calls)."""
|
|
109
|
+
prefix = (path.rstrip("/") + "/") if path else ""
|
|
110
|
+
|
|
111
|
+
dirs: dict[str, list[str]] = {}
|
|
112
|
+
files: list[tuple[str, str, int]] = []
|
|
113
|
+
|
|
114
|
+
for file_path, sha in manifest.items():
|
|
115
|
+
if prefix and not file_path.startswith(prefix):
|
|
116
|
+
continue
|
|
117
|
+
relative = file_path[len(prefix):]
|
|
118
|
+
parts = relative.split("/")
|
|
119
|
+
if len(parts) == 1:
|
|
120
|
+
files.append((parts[0], file_path, sizes.get(sha, 0)))
|
|
121
|
+
else:
|
|
122
|
+
dir_name = parts[0]
|
|
123
|
+
if dir_name not in dirs:
|
|
124
|
+
dirs[dir_name] = []
|
|
125
|
+
dirs[dir_name].append(file_path)
|
|
126
|
+
|
|
127
|
+
entries: list[TreeEntryV2] = []
|
|
128
|
+
|
|
129
|
+
for dir_name in sorted(dirs.keys(), key=str.lower):
|
|
130
|
+
dir_path = f"{prefix}{dir_name}" if prefix else dir_name
|
|
131
|
+
children = _assemble_tree(manifest, sizes, dir_path)
|
|
132
|
+
entries.append(TreeEntryV2(
|
|
133
|
+
name=dir_name,
|
|
134
|
+
path=dir_path,
|
|
135
|
+
type="directory",
|
|
136
|
+
size=0,
|
|
137
|
+
language="",
|
|
138
|
+
children=children,
|
|
139
|
+
))
|
|
140
|
+
|
|
141
|
+
for name, full_path, size in sorted(files, key=lambda x: x[0].lower()):
|
|
142
|
+
entries.append(TreeEntryV2(
|
|
143
|
+
name=name,
|
|
144
|
+
path=full_path,
|
|
145
|
+
type="file",
|
|
146
|
+
size=size,
|
|
147
|
+
language=detect_language(name),
|
|
148
|
+
))
|
|
149
|
+
|
|
150
|
+
return entries
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# --- Endpoints ---
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _build_tree_recursive(
|
|
157
|
+
git, project_id: str, ref: str, path: str = "", max_depth: int = 10,
|
|
158
|
+
) -> list[TreeEntryV2]:
|
|
159
|
+
"""Build a nested tree by recursively walking git entries."""
|
|
160
|
+
if max_depth <= 0:
|
|
161
|
+
return []
|
|
162
|
+
try:
|
|
163
|
+
entries = git.get_tree(project_id, ref, path) if path else git.get_tree(project_id, ref)
|
|
164
|
+
except (FileNotFoundError, ValueError, KeyError):
|
|
165
|
+
return []
|
|
166
|
+
|
|
167
|
+
result = []
|
|
168
|
+
for e in entries:
|
|
169
|
+
node_type = "directory" if e.type == "tree" else "file"
|
|
170
|
+
children = None
|
|
171
|
+
if e.type == "tree":
|
|
172
|
+
children = _build_tree_recursive(git, project_id, ref, e.path, max_depth - 1)
|
|
173
|
+
result.append(TreeEntryV2(
|
|
174
|
+
name=e.name,
|
|
175
|
+
path=e.path,
|
|
176
|
+
type=node_type,
|
|
177
|
+
size=e.size,
|
|
178
|
+
language=detect_language(e.name) if e.type == "blob" else "",
|
|
179
|
+
children=children,
|
|
180
|
+
))
|
|
181
|
+
# Sort: directories first, then files, alphabetically
|
|
182
|
+
result.sort(key=lambda x: (0 if x.type == "directory" else 1, x.name.lower()))
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@router.get("/tree", response_model=TreeResponse)
|
|
187
|
+
async def get_root_tree(
|
|
188
|
+
project_id: uuid.UUID,
|
|
189
|
+
ref: str = Query("", description="Git ref (branch, tag, commit). Defaults to default branch."),
|
|
190
|
+
recursive: bool = Query(True, description="Return nested tree with children"),
|
|
191
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
192
|
+
session: AsyncSession = Depends(get_db_session),
|
|
193
|
+
) -> TreeResponse:
|
|
194
|
+
"""List root tree at a ref. With recursive=true, returns full nested tree."""
|
|
195
|
+
repo = await _get_repo(project_id, session, auth)
|
|
196
|
+
resolved_ref = await _resolve_ref(repo, ref)
|
|
197
|
+
|
|
198
|
+
# DB-backed fallback for repos without a bare git clone (remote-connected)
|
|
199
|
+
if not repo.clone_path:
|
|
200
|
+
entries = await _build_tree_from_manifest(session, project_id, resolved_ref)
|
|
201
|
+
return TreeResponse(path="", ref=resolved_ref, entries=entries)
|
|
202
|
+
|
|
203
|
+
git = get_git_manager()
|
|
204
|
+
|
|
205
|
+
if recursive:
|
|
206
|
+
import asyncio
|
|
207
|
+
loop = asyncio.get_running_loop()
|
|
208
|
+
entries = await loop.run_in_executor(
|
|
209
|
+
None, _build_tree_recursive, git, str(project_id), resolved_ref, "", 10,
|
|
210
|
+
)
|
|
211
|
+
return TreeResponse(path="", ref=resolved_ref, entries=entries)
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
entries = git.get_tree(str(project_id), resolved_ref)
|
|
215
|
+
except (FileNotFoundError, ValueError) as e:
|
|
216
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
217
|
+
|
|
218
|
+
return TreeResponse(
|
|
219
|
+
path="",
|
|
220
|
+
ref=resolved_ref,
|
|
221
|
+
entries=[
|
|
222
|
+
TreeEntryV2(
|
|
223
|
+
name=e.name,
|
|
224
|
+
path=e.path,
|
|
225
|
+
type="directory" if e.type == "tree" else "file",
|
|
226
|
+
size=e.size,
|
|
227
|
+
language=detect_language(e.name) if e.type == "blob" else "",
|
|
228
|
+
)
|
|
229
|
+
for e in entries
|
|
230
|
+
],
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@router.get("/tree/{path:path}", response_model=TreeResponse)
|
|
235
|
+
async def get_subtree(
|
|
236
|
+
project_id: uuid.UUID,
|
|
237
|
+
path: str,
|
|
238
|
+
ref: str = Query("", description="Git ref. Defaults to default branch."),
|
|
239
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
240
|
+
session: AsyncSession = Depends(get_db_session),
|
|
241
|
+
) -> TreeResponse:
|
|
242
|
+
"""List subtree at a path and ref."""
|
|
243
|
+
repo = await _get_repo(project_id, session, auth)
|
|
244
|
+
resolved_ref = await _resolve_ref(repo, ref)
|
|
245
|
+
|
|
246
|
+
# Validate path
|
|
247
|
+
if ".." in path.split("/"):
|
|
248
|
+
raise HTTPException(status_code=400, detail="Path traversal not allowed")
|
|
249
|
+
|
|
250
|
+
# DB-backed fallback for repos without a bare git clone
|
|
251
|
+
if not repo.clone_path:
|
|
252
|
+
entries = await _build_tree_from_manifest(session, project_id, resolved_ref, path)
|
|
253
|
+
return TreeResponse(path=path, ref=resolved_ref, entries=entries)
|
|
254
|
+
|
|
255
|
+
git = get_git_manager()
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
entries = git.get_tree(str(project_id), resolved_ref, path)
|
|
259
|
+
except (FileNotFoundError, ValueError, KeyError):
|
|
260
|
+
raise HTTPException(status_code=404, detail=f"Path not found: {path}")
|
|
261
|
+
|
|
262
|
+
return TreeResponse(
|
|
263
|
+
path=path,
|
|
264
|
+
ref=resolved_ref,
|
|
265
|
+
entries=[
|
|
266
|
+
TreeEntryV2(
|
|
267
|
+
name=e.name,
|
|
268
|
+
path=e.path,
|
|
269
|
+
type="directory" if e.type == "tree" else "file",
|
|
270
|
+
size=e.size,
|
|
271
|
+
language=detect_language(e.name) if e.type == "blob" else "",
|
|
272
|
+
)
|
|
273
|
+
for e in entries
|
|
274
|
+
],
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@router.get("/files/{path:path}", response_model=FileContentV2Response)
|
|
279
|
+
async def get_file_content(
|
|
280
|
+
project_id: uuid.UUID,
|
|
281
|
+
path: str,
|
|
282
|
+
ref: str = Query("", description="Git ref. Defaults to default branch."),
|
|
283
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
284
|
+
session: AsyncSession = Depends(get_db_session),
|
|
285
|
+
) -> FileContentV2Response:
|
|
286
|
+
"""Read file content at a specific ref."""
|
|
287
|
+
repo = await _get_repo(project_id, session, auth)
|
|
288
|
+
resolved_ref = await _resolve_ref(repo, ref)
|
|
289
|
+
|
|
290
|
+
# Validate path
|
|
291
|
+
if ".." in path.split("/"):
|
|
292
|
+
raise HTTPException(status_code=400, detail="Path traversal not allowed")
|
|
293
|
+
|
|
294
|
+
# DB-backed fallback: read from ContentStore via manifest SHA
|
|
295
|
+
if not repo.clone_path:
|
|
296
|
+
from attocode_intel.storage.content_store import ContentStore
|
|
297
|
+
|
|
298
|
+
branch_ctx = await get_branch_context(project_id, resolved_ref, session)
|
|
299
|
+
sha = branch_ctx.manifest.get(path)
|
|
300
|
+
if sha is None:
|
|
301
|
+
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
|
302
|
+
content_store = ContentStore(session)
|
|
303
|
+
data = await content_store.get(sha)
|
|
304
|
+
if data is None:
|
|
305
|
+
raise HTTPException(status_code=404, detail=f"Content not found for {path}")
|
|
306
|
+
else:
|
|
307
|
+
git = get_git_manager()
|
|
308
|
+
try:
|
|
309
|
+
data = git.read_file(str(project_id), resolved_ref, path)
|
|
310
|
+
except (FileNotFoundError, KeyError):
|
|
311
|
+
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
|
312
|
+
|
|
313
|
+
if len(data) > MAX_FILE_SIZE:
|
|
314
|
+
raise HTTPException(
|
|
315
|
+
status_code=413,
|
|
316
|
+
detail=f"File too large ({len(data)} bytes, max {MAX_FILE_SIZE})",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if is_binary(data):
|
|
320
|
+
raise HTTPException(status_code=415, detail="Binary file — content not available via this endpoint")
|
|
321
|
+
|
|
322
|
+
content = data.decode("utf-8", errors="replace")
|
|
323
|
+
line_count = content.count("\n") + (1 if content and not content.endswith("\n") else 0)
|
|
324
|
+
|
|
325
|
+
return FileContentV2Response(
|
|
326
|
+
path=path,
|
|
327
|
+
ref=resolved_ref,
|
|
328
|
+
content=content,
|
|
329
|
+
language=detect_language(path),
|
|
330
|
+
size_bytes=len(data),
|
|
331
|
+
line_count=line_count,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@router.get("/stats", response_model=RepoStatsResponse)
|
|
336
|
+
async def get_repo_stats(
|
|
337
|
+
project_id: uuid.UUID,
|
|
338
|
+
ref: str = Query("", description="Git ref. Defaults to default branch."),
|
|
339
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
340
|
+
session: AsyncSession = Depends(get_db_session),
|
|
341
|
+
) -> RepoStatsResponse:
|
|
342
|
+
"""Get repository file/symbol/language/embedding stats."""
|
|
343
|
+
from attocode_intel.db.models import Embedding, Symbol
|
|
344
|
+
|
|
345
|
+
repo = await _get_repo(project_id, session, auth)
|
|
346
|
+
resolved_ref = ref if ref else repo.default_branch
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
branch_ctx = await get_branch_context(project_id, resolved_ref, session)
|
|
350
|
+
except HTTPException:
|
|
351
|
+
return RepoStatsResponse()
|
|
352
|
+
|
|
353
|
+
manifest = branch_ctx.manifest
|
|
354
|
+
content_shas = branch_ctx.content_shas
|
|
355
|
+
|
|
356
|
+
# Count files and aggregate languages
|
|
357
|
+
languages: dict[str, int] = {}
|
|
358
|
+
for path in manifest:
|
|
359
|
+
lang = detect_language(path)
|
|
360
|
+
if lang:
|
|
361
|
+
languages[lang] = languages.get(lang, 0) + 1
|
|
362
|
+
|
|
363
|
+
# Count symbols
|
|
364
|
+
symbol_count = 0
|
|
365
|
+
if content_shas:
|
|
366
|
+
result = await session.execute(
|
|
367
|
+
select(func.count()).select_from(
|
|
368
|
+
select(Symbol.id).where(Symbol.content_sha.in_(content_shas)).subquery()
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
symbol_count = result.scalar() or 0
|
|
372
|
+
|
|
373
|
+
# Count embedded files
|
|
374
|
+
embedded_count = 0
|
|
375
|
+
if content_shas:
|
|
376
|
+
result = await session.execute(
|
|
377
|
+
select(func.count(func.distinct(Embedding.content_sha))).where(
|
|
378
|
+
Embedding.content_sha.in_(content_shas)
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
embedded_count = result.scalar() or 0
|
|
382
|
+
|
|
383
|
+
return RepoStatsResponse(
|
|
384
|
+
total_files=len(manifest),
|
|
385
|
+
total_symbols=symbol_count,
|
|
386
|
+
languages=languages,
|
|
387
|
+
embedded_files=embedded_count,
|
|
388
|
+
)
|