Skip to content

Clean Architecture Infrastructure Refactoring#271

Merged
unhappychoice merged 8 commits intomainfrom
feature/clean-architecture-infrastructure-refactoring
Sep 29, 2025
Merged

Clean Architecture Infrastructure Refactoring#271
unhappychoice merged 8 commits intomainfrom
feature/clean-architecture-infrastructure-refactoring

Conversation

@unhappychoice
Copy link
Copy Markdown
Owner

@unhappychoice unhappychoice commented Sep 29, 2025

Summary

  • Comprehensive refactoring of infrastructure components following Clean Architecture principles
  • Migration of cache infrastructure to domain repositories
  • Unification of storage implementations with AppDataProvider trait
  • Reorganization of database infrastructure
  • Code quality improvements with clippy fixes and formatting

Changes

  • Domain Repositories: Migrated cache infrastructure to proper domain repositories with improved test coverage
  • Storage Unification: Unified storage implementations using the AppDataProvider trait
  • Config Management: Moved ConfigManager to domain/services and integrated with FileStorage
  • Version Infrastructure: Clean architecture refactoring of version-related infrastructure
  • Database Organization: Reorganized database infrastructure following Clean Architecture
  • Git Repository Processing: Optimized processing and fixed extraction logic
  • OSS Insight Integration: Extracted OSS Insight API client to infrastructure/http
  • Code Quality: Resolved clippy warnings and applied consistent formatting

Test plan

  • All tests pass (627 tests)
  • Cargo check passes
  • Cargo clippy passes with strict warnings
  • Code is properly formatted with cargo fmt
  • No breaking changes to public APIs
  • Infrastructure follows Clean Architecture principles

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Version check with user prompt for updates.
    • Trending repositories fetched via a new cached service with graceful fallback.
    • Git repository cloning with progress and local cache detection.
    • New configuration service for loading/saving app settings.
    • Repository URLs normalized/displayed as HTTPS.
  • Changes

    • Removed --config CLI option; theme initialization no longer needs a path.
    • Challenge cache now managed via a repository-backed service.
  • Refactor

    • Reorganized git, storage, and database infrastructure; removed legacy managers/utilities.
  • Tests

    • Added unit tests for repositories, git clients, config, and version services.
  • Chores

    • Dependency updates and packaging excludes.

unhappychoice and others added 8 commits September 27, 2025 17:54
- Add OSS Insight API client to infrastructure/http module
- Update trending.rs to use new API client
- Update trending_unified_view.rs to use new API structure

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Eliminate duplicate git repository root detection across components
- Optimize git_root calculation by computing once per batch extraction
- Remove duplicate find_git_repository_root implementation from CommonExtractor
- Move git-related processing to infrastructure layer following Clean Architecture
- Fix cloning_step branching logic and make extraction responsibilities clear
- Implement mandatory git_root requirement with proper error handling
- Use functional programming patterns (map, and_then) for cleaner code
- Add test-specific helper functions to avoid modifying production behavior

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ture

- Move storage/database.rs to database/database.rs
- Move storage/daos/ to database/daos/
- Move storage/migrations/ to database/migrations/
- Move storage/seeders/ to database/seeders/
- Update all imports to use new database module structure
- Remove storage/integration_test.rs (functionality moved to unit tests)
- Clean up storage module to only contain file-based storage if needed

This completes the Clean Architecture separation by creating a dedicated
database infrastructure module separate from general storage concerns.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Move GitHub API communication to infrastructure/http module
- Create generic FileStorage in infrastructure/storage/file without cache-specific terminology
- Extract version checking business logic to domain layer (repositories and services)
- Repository layer handles cache concerns while Service layer remains cache-agnostic
- Delete infrastructure/version directory completely
- Add comprehensive tests with mock implementations using cfg(feature) pattern
- Tests no longer depend on Internet connectivity or CARGO_PKG_VERSION
- Self-referential dev-dependency pattern enables automatic mock activation during testing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
…leStorage

- Move ConfigManager from infrastructure/config to domain/services layer
- Rename ConfigManager to ConfigService following Clean Architecture principles
- Update ConfigService to use infrastructure/storage/file/file_storage.rs for file operations
- Remove CLI config option and simplify ThemeManager initialization
- Add comprehensive tests for ConfigService in tests/unit/domain/services/
- Remove infrastructure/config module and external module references
- Update all imports to use new ConfigService location

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add AppDataProvider trait with default implementation for get_app_data_dir
- Consolidate file_storage and compressed_file_storage to use shared implementation
- Use conditional compilation for production vs test environments
- Remove duplicate code while maintaining feature parity
- All 594 tests passing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
…st improvements

- Move ChallengeCache to domain/repositories/ChallengeRepository
- Move TrendingCache to domain/repositories/TrendingRepository
- Implement Repository pattern with unified caching and API integration
- Fix flaky tests by removing shared static storage
- Add comprehensive test coverage for both repositories
- Remove conditional compilation in favor of instance-based storage
- Add git_repository_ref_parser comprehensive test suite
- Resolve all compilation warnings

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add module_inception allow for database mod.rs and version_service_tests.rs
- Fix absurd extreme comparisons in remote_git_repository_client.rs (total <= 0 to total == 0)
- Apply cargo fmt for consistent code formatting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 29, 2025

Walkthrough

Restructures infrastructure (adds database/git/http; removes cache/config/version/repo_manager), introduces new domain repositories/services (ChallengeRepository, TrendingRepository, VersionRepository, ConfigService), adds Git parsing/clients, refactors extractors to require git_root, updates CLI/game flows and views to new APIs, and revises tests accordingly. Adds async-trait and feature flags.

Changes

Cohort / File(s) Summary
Build & Features
Cargo.toml
Adds async-trait dep; introduces feature flags (default=[], test-mocks=[]); adds dev-dependency with features; expands package exclude list.
Domain Models
src/domain/models/git_repository_ref.rs, src/domain/models/mod.rs
Adds GitRepositoryRef model and re-export.
Domain Repositories - Challenge
src/domain/repositories/challenge_repository.rs, src/presentation/cli/runner.rs, src/presentation/game/models/loading_steps/{cache_check_step.rs,generating_step.rs}
Replaces ChallengeCache with ChallengeRepository using CompressedFileStorage; updates cache APIs (save/load/stats/list/invalidate); updates consumers.
Domain Repositories - Trending
src/domain/repositories/trending_repository.rs, src/presentation/cli/commands/trending.rs, src/presentation/cli/views/{trending_repository_selection_view.rs,trending_unified_view.rs}
Adds TrendingRepository with API-first async flow and cache; introduces TrendingRepositoryInfo; updates CLI/view types and calls.
Domain Repositories - Version
src/domain/repositories/version_repository.rs, src/domain/services/version_service.rs, src/domain/repositories/mod.rs, src/domain/services/mod.rs
Adds VersionRepository (caching + GitHub API) and VersionService (version check); exports modules.
Config Service
src/domain/services/config_manager.rs, src/domain/services/theme_manager.rs, src/presentation/game/screens/settings_screen.rs
Replaces infrastructure ConfigManager with domain ConfigService; updates ThemeManager::init() signature (no path) and settings save path.
Extractor Refactor
src/domain/services/extractor/{code_chunk_extractor.rs,core/extractor.rs}, tests/integration/indent_treesitter_tests.rs, tests/integration/{mod.rs,comment_processing_tests.rs}
Adds explicit git_root to extraction; removes internal root discovery and extract_from_file; updates test helpers/usages.
Remove Old Git Extractor
src/domain/services/extractor/{git_repository_extractor.rs,mod.rs}
Deletes GitRepositoryExtractor and its re-export.
Infrastructure Reorg - Database
src/infrastructure/database/{mod.rs,seeders/database_seeder.rs}, examples/seed_database.rs, src/domain/repositories/{git_repository_repository.rs,session_repository.rs,stage_repository.rs}, src/presentation/game/models/loading_steps/database_init_step.rs, src/presentation/game/screens/records_screen.rs, src/presentation/cli/commands/repo.rs
Introduces database module tree and updates imports from storage::* to database::*; no logic changes.
Infrastructure Reorg - Storage
src/infrastructure/storage/{mod.rs,app_data_provider.rs,compressed_file_storage.rs,file_storage.rs}, src/infrastructure/storage/integration_test.rs
Adds AppDataProvider, CompressedFileStorage, FileStorage; removes storage database/daos exposure; deletes storage integration test.
Infrastructure Reorg - Git
src/infrastructure/git/{mod.rs,git_repository_ref_parser.rs}, src/infrastructure/git/local/{mod.rs,local_git_repository_client.rs}, src/infrastructure/git/remote/{mod.rs,remote_git_repository_client.rs}
Adds GitRepositoryRefParser; adds Local/Remote Git clients; exposes git module API.
Infrastructure Reorg - HTTP
src/infrastructure/http/{mod.rs,github_api_client.rs,oss_insight_client.rs}
Adds GitHubApiClient (real/mocked) and OssInsightClient; re-exports in http module.
Remove Legacy Infra
src/infrastructure/{cache/*,config/mod.rs,repository_manager.rs,version/*,external/mod.rs,mod.rs}
Removes cache (Challenge/Trending/Gzip), ConfigManager, RepositoryManager, VersionCache/Checker, external comment; updates infrastructure modules (add database/git/http; remove cache/config/repository_manager/version).
Presentation CLI & Views
src/presentation/cli/{args.rs,commands/{game.rs,repo.rs,trending.rs},views/{mod.rs,repo_list_view.rs,repo_play_view.rs,repo_utils.rs}}
Removes --config arg; switches to VersionService; uses RemoteGitRepositoryClient and repo.http_url(); deletes repo_utils helpers; updates imports.
Game Loading Steps
src/presentation/game/models/loading_steps/{cloning_step.rs,scanning_step.rs}
Cloning now via RemoteGitRepositoryClient with progress; metadata via LocalGitRepositoryClient; removes prior extractor usage; adjusts error paths.
Tests - Unit & Integration
tests/unit/**, tests/integration/**
Adds tests for new Git clients/parsers, ConfigService, VersionService, repositories; removes old cache/version tests; updates extractor tests for new signatures and helpers.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant CLI
  participant VersionService
  participant VersionRepository
  participant GitHubApiClient

  CLI->>VersionService: check()
  VersionService->>VersionRepository: fetch_latest_version()
  alt Cache valid
    VersionRepository-->>VersionService: latest_version
  else Cache miss/invalid
    VersionRepository->>GitHubApiClient: fetch_latest_release()
    GitHubApiClient-->>VersionRepository: GitHubRelease(tag)
    VersionRepository-->>VersionService: latest_version (cached)
  end
  VersionService-->>CLI: (has_update, current, latest)
Loading
sequenceDiagram
  autonumber
  participant UI
  participant TrendingRepository
  participant OssInsightClient
  participant Storage as CompressedFileStorage

  UI->>TrendingRepository: get_trending_repositories(key, lang, period)
  TrendingRepository->>Storage: load(cache_file)
  alt Cache hit
    Storage-->>TrendingRepository: data
    TrendingRepository-->>UI: repositories
  else Cache miss
    TrendingRepository->>OssInsightClient: fetch_trending_repositories(lang, period)
    alt API ok
      OssInsightClient-->>TrendingRepository: repos
      TrendingRepository->>Storage: save(cache_file, data)
      TrendingRepository-->>UI: repos
    else API error
      TrendingRepository-->>UI: []
    end
  end
Loading
sequenceDiagram
  autonumber
  participant Loader
  participant RemoteGit as RemoteGitRepositoryClient
  participant LocalGit as LocalGitRepositoryClient
  participant Screen

  Loader->>RemoteGit: clone_repository(repo_spec, progress_cb)
  RemoteGit-->>Loader: local_repo_path
  Loader->>LocalGit: extract_git_repository(local_repo_path)
  LocalGit-->>Loader: GitRepository
  Loader->>Screen: update progress / set context
Loading
sequenceDiagram
  autonumber
  participant Game
  participant ChallengeRepo as ChallengeRepository
  participant Storage as CompressedFileStorage

  Game->>ChallengeRepo: load_challenges_with_progress(repo, reporter?)
  alt Cache present and valid
    ChallengeRepo->>Storage: load(cache_file)
    Storage-->>ChallengeRepo: challenges
    ChallengeRepo-->>Game: Some(challenges)
  else Missing/invalid
    ChallengeRepo-->>Game: None
    Game->>ChallengeRepo: save_challenges(repo, generated)
    ChallengeRepo->>Storage: save(cache_file, data)
    Storage-->>ChallengeRepo: ok
    ChallengeRepo-->>Game: ok
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

A nibble of bytes, a hop through trees,
New burrows for Git in rustling leaves.
I cached my carrots, compressed and neat,
Checked versions with a thump-thump beat.
Trends on the wind, I sniff—then fetch!
With tidy paths, I race and stretch.
Happy commits! 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.13% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Clean Architecture Infrastructure Refactoring” succinctly captures the primary change of restructuring the infrastructure layer to adhere to Clean Architecture principles, aligning with the pull request’s comprehensive refactoring objectives without extraneous detail.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/clean-architecture-infrastructure-refactoring

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

@unhappychoice
Copy link
Copy Markdown
Owner Author

Ref: #267

@codecov
Copy link
Copy Markdown

codecov bot commented Sep 29, 2025

Codecov Report

❌ Patch coverage is 56.05381% with 294 lines in your changes missing coverage. Please review.
✅ Project coverage is 22.57%. Comparing base (27486c0) to head (0fd88e2).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
...structure/git/local/local_git_repository_client.rs 5.00% 76 Missing ⚠️
...ructure/git/remote/remote_git_repository_client.rs 8.00% 69 Missing ⚠️
src/domain/repositories/version_repository.rs 46.66% 32 Missing ⚠️
.../domain/services/extractor/code_chunk_extractor.rs 0.00% 20 Missing ⚠️
src/presentation/cli/commands/trending.rs 0.00% 15 Missing ⚠️
...entation/game/models/loading_steps/cloning_step.rs 0.00% 11 Missing ⚠️
.../infrastructure/storage/compressed_file_storage.rs 83.05% 10 Missing ⚠️
src/presentation/cli/commands/game.rs 0.00% 8 Missing ⚠️
src/domain/services/version_service.rs 82.50% 7 Missing ⚠️
src/infrastructure/storage/file_storage.rs 62.50% 6 Missing ⚠️
... and 16 more

❌ Your patch status has failed because the patch coverage (56.05%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.
❌ Your project status has failed because the head coverage (22.57%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #271      +/-   ##
==========================================
+ Coverage   21.89%   22.57%   +0.67%     
==========================================
  Files         187      192       +5     
  Lines       21895    21603     -292     
==========================================
+ Hits         4794     4876      +82     
+ Misses      17101    16727     -374     
Files with missing lines Coverage Δ
...c/domain/repositories/git_repository_repository.rs 0.00% <ø> (ø)
src/domain/repositories/session_repository.rs 5.70% <ø> (ø)
src/domain/repositories/stage_repository.rs 0.00% <ø> (ø)
src/domain/services/config_manager.rs 100.00% <100.00%> (ø)
src/domain/services/extractor/core/extractor.rs 92.94% <100.00%> (+0.31%) ⬆️
src/infrastructure/database/daos/challenge_dao.rs 0.00% <ø> (ø)
src/infrastructure/database/daos/repository_dao.rs 0.00% <ø> (ø)
src/infrastructure/database/daos/session_dao.rs 0.00% <ø> (ø)
src/infrastructure/database/daos/stage_dao.rs 0.00% <ø> (ø)
src/infrastructure/database/database.rs 87.24% <ø> (ø)
... and 35 more

... and 2 files with indirect coverage changes

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

Copy link
Copy Markdown
Contributor

@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: 15

Caution

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

⚠️ Outside diff range comments (4)
src/domain/models/storage/repository.rs (1)

1-27: Preserve clean architecture layering between domain and infrastructure

Importing GitRepositoryRefParser from infrastructure inside a domain model ties the core domain layer back to infrastructure, reversing the dependency flow this refactor is trying to enforce. That coupling risks cyclic builds and makes future infrastructure swaps impossible without touching domain code. Please move the parsing logic into the domain (or inject the already-parsed reference from infrastructure) so the domain stays infrastructure-agnostic.

src/domain/services/theme_manager.rs (2)

88-93: Major: path handling bypasses AppDataProvider; inconsistent across OS/debug/tests.

Using HOME and defaulting to "." conflicts with AppDataProvider (debug uses current_dir, test-mocks uses /tmp/test, Windows may lack HOME). This can split config vs theme locations and break tests or Windows users. Switch to AppDataProvider.

Imports to add (top of file):

+use crate::infrastructure::storage::{file_storage::FileStorage, AppDataProvider};

Function change:

-    fn get_custom_theme_path() -> std::path::PathBuf {
-        let home_dir = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
-        std::path::PathBuf::from(home_dir)
-            .join(".gittype")
-            .join("custom-theme.json")
-    }
+    fn get_custom_theme_path() -> std::path::PathBuf {
+        // Align with AppDataProvider semantics (debug/test-mocks/OS-specific dirs)
+        let app_dir = <FileStorage as AppDataProvider>::get_app_data_dir()
+            .unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| ".".into()));
+        app_dir.join("custom-theme.json")
+    }

Based on learnings.


88-93: Consolidate all hard-coded “.gittype” paths into AppDataProvider
Multiple modules still construct paths via env::var("HOME") or dirs::home_dir() then .join(".gittype") (e.g. in infrastructure/logging.rs, git/remote/remote_git_repository_client.rs, cli/commands/repo.rs, cli/views/repo_list_view.rs, infrastructure/database/database.rs, domain/services/theme_manager.rs, domain/repositories/*, and various presentation/game/screens). Replace these with a single, centralized helper in AppDataProvider (e.g. AppDataProvider::base_dir()?.join("…")) to ensure consistency and simplify future changes.

src/domain/repositories/trending_repository.rs (1)

91-134: Avoid blocking std::fs calls inside async path

get_trending_repositories is async, but both get_from_cache and save_to_cache synchronously call std::fs::read_to_string, fs::write, and fs::remove_file. On a Tokio runtime this blocks the worker thread and can starve unrelated tasks. Please move the disk I/O onto Tokio’s blocking pool (tokio::fs or tokio::task::spawn_blocking) so the async path stays non-blocking.(docs.rs)

🧹 Nitpick comments (36)
src/presentation/game/screens/settings_screen.rs (1)

117-125: Surface config persistence failures.

We still drop both the constructor and save() errors on the floor, so a filesystem issue will quietly lose settings. Even a log::warn! on the Err paths would help diagnose user reports.

src/infrastructure/git/mod.rs (1)

1-7: Public API consolidation looks good; consider minimal docs.

Re-exports create a clean surface. Consider adding a short module-level doc and brief item docs to make the intended entry points clear to downstream users.

src/domain/repositories/mod.rs (1)

1-6: Consider limiting module visibility to reduce public surface.

If external crates shouldn’t rely on module paths, make new modules private and keep the public re-exports. This preserves the API of traits/types without exposing layout details.

Example change:

-pub mod challenge_repository;
+mod challenge_repository;
...
-pub mod trending_repository;
+mod trending_repository;
-pub mod version_repository;
+mod version_repository;

Can you confirm whether these modules are intentionally part of the public API? If yes, ignore this suggestion.

src/domain/services/theme_manager.rs (4)

21-29: Minor: rename local for clarity.

Variable name suggests the old type. Use config_service to match the new type.

-    pub fn init() -> anyhow::Result<()> {
-        let config_manager = ConfigService::new()?;
+    pub fn init() -> anyhow::Result<()> {
+        let config_service = ConfigService::new()?;
...
-        let config = config_manager.get_config();
+        let config = config_service.get_config();

32-32: Prefer expect over unwrap on lock to aid diagnostics.

Panic message will be clearer if the lock is poisoned.

-        let mut manager = THEME_MANAGER.write().unwrap();
+        let mut manager = THEME_MANAGER
+            .write()
+            .expect("THEME_MANAGER write lock poisoned");

-        let manager = THEME_MANAGER.read().unwrap();
+        let manager = THEME_MANAGER
+            .read()
+            .expect("THEME_MANAGER read lock poisoned");

Also applies to: 47-49


61-85: Tiny: avoid recomputing the theme path.

Store let custom_path = Self::get_custom_theme_path(); and reuse it for exists(), read_to_string to reduce duplicated I/O and path computation.


95-121: Optional: use shared storage helpers for JSON I/O.

Consider reading/writing the custom theme via your FileStorage to centralize serialization, error handling, and testing (mocks).

src/domain/services/config_manager.rs (1)

29-36: Consider atomic writes and recovery from malformed JSON.

  • Write config to a temp file and rename to avoid partial writes on crash.
  • Optionally, on parse errors, back up the old file and start with defaults to keep the app usable.

Can you confirm whether FileStorage.write_json is atomic and whether read_json distinguishes “missing” vs “malformed”? If not, I can propose a small write_json_atomic helper and error-handling pattern.

src/infrastructure/storage/mod.rs (1)

1-6: Storage surface looks cohesive.

Splitting storage concerns and re-exporting AppDataProvider aligns with the refactor goals. Consider a brief module doc explaining intended consumers for each submodule.

src/infrastructure/storage/app_data_provider.rs (2)

9-21: Make debug and release resolve a consistent app subdirectory.

Writing directly to the CWD in debug is surprising; append “.gittype” in both modes to avoid polluting project roots.

-            let data_dir = if cfg!(debug_assertions) {
-                std::env::current_dir().map_err(|e| {
+            let base_dir = if cfg!(debug_assertions) {
+                std::env::current_dir().map_err(|e| {
                     GitTypeError::ExtractionFailed(format!(
                         "Could not get current directory: {}",
                         e
                     ))
                 })?
             } else {
                 let home_dir = dirs::home_dir().ok_or_else(|| {
                     GitTypeError::ExtractionFailed("Could not determine home directory".to_string())
                 })?;
-                home_dir.join(".gittype")
+                home_dir
             };
+            let data_dir = base_dir.join(".gittype");

17-21: Consider OS‑appropriate data dir (XDG/Windows/macOS).

dirs::home_dir().join(".gittype") is simple but non‑standard. Prefer dirs::data_dir() or a higher‑level crate like directories for platform‑correct paths (e.g., %APPDATA%\\gittype, ~/Library/Application Support/gittype, $XDG_DATA_HOME/gittype).

tests/unit/domain/services/version_service_tests.rs (1)

1-2: Avoid module‑inception; flatten tests.

The inner mod version_service_tests triggers clippy and needs an allow. Prefer top‑level tests in this file and drop the module and allow. Keeps clippy clean.

src/infrastructure/git/local/local_git_repository_client.rs (4)

15-29: Use Repository::discover for robust root detection (nested dirs, worktrees, bare repos).

Walking for .git misses bare repositories and can mis-handle worktrees/symlinks. Let libgit2 resolve it, then derive the root.

-    pub fn get_repository_root(path: &Path) -> Option<PathBuf> {
-        let mut current_path = path.to_path_buf();
-
-        loop {
-            if Self::is_git_repository(&current_path) {
-                return Some(current_path);
-            }
-
-            if !current_path.pop() {
-                break;
-            }
-        }
-
-        None
-    }
+    pub fn get_repository_root(path: &Path) -> Option<PathBuf> {
+        Repository::discover(path)
+            .ok()
+            .and_then(|repo| {
+                repo.workdir()
+                    .map(|p| p.to_path_buf())
+                    .or_else(|| Some(repo.path().to_path_buf()))
+            })
+    }
@@
-        let canonical_path = repo_path.canonicalize().map_err(|_| {
-            GitTypeError::ExtractionFailed("Path canonicalization failed".to_string())
-        })?;
-
-        let git_root = Self::get_repository_root(&canonical_path).ok_or_else(|| {
-            GitTypeError::ExtractionFailed("Git repository not found".to_string())
-        })?;
-
-        let repo = Repository::open(&git_root).map_err(|e| {
-            GitTypeError::ExtractionFailed(format!("Failed to open git repository: {}", e))
-        })?;
+        let canonical_path = repo_path.canonicalize().map_err(|e| {
+            GitTypeError::ExtractionFailed(format!(
+                "Path canonicalization failed for {}: {}",
+                repo_path.display(),
+                e
+            ))
+        })?;
+
+        let repo = Repository::discover(&canonical_path).map_err(|e| {
+            GitTypeError::ExtractionFailed(format!(
+                "Failed to discover git repository from {}: {}",
+                canonical_path.display(),
+                e
+            ))
+        })?;
+
+        let git_root = repo
+            .workdir()
+            .map(|p| p.to_path_buf())
+            .unwrap_or_else(|| repo.path().to_path_buf());

Also applies to: 31-43


77-86: Clarify branch error for detached HEAD.

The current message blames UTF‑8; detached HEAD is a common, different case. Return a better message.

-            .and_then(|name_opt| {
-                name_opt.ok_or_else(|| {
-                    GitTypeError::ExtractionFailed("Branch name is not valid UTF-8".to_string())
-                })
-            })
+            .and_then(|name_opt| {
+                name_opt.ok_or_else(|| {
+                    GitTypeError::ExtractionFailed(
+                        "HEAD is detached or branch name is not Unicode".to_string(),
+                    )
+                })
+            })

88-97: Resolve commit via peel_to_commit for both symbolic and detached HEAD.

head.target() may be None for symbolic refs. Peeling is more robust.

-    fn get_current_commit_hash(repo: &Repository) -> Result<String> {
-        repo.head()
-            .map_err(|e| GitTypeError::ExtractionFailed(format!("Failed to get HEAD: {}", e)))
-            .map(|head| head.target().map(|oid| oid.to_string()))
-            .and_then(|oid_opt| {
-                oid_opt.ok_or_else(|| {
-                    GitTypeError::ExtractionFailed("HEAD does not point to a commit".to_string())
-                })
-            })
-    }
+    fn get_current_commit_hash(repo: &Repository) -> Result<String> {
+        let commit = repo
+            .head()
+            .and_then(|h| h.peel_to_commit())
+            .map_err(|e| {
+                GitTypeError::ExtractionFailed(format!("Failed to resolve HEAD to commit: {}", e))
+            })?;
+        Ok(commit.id().to_string())
+    }

31-62: Propagate parse errors verbosely.

Swallowing the parser’s error obscures root causes (e.g., unsupported protocols). Include details.

-        let repo_ref = GitRepositoryRefParser::parse(&remote_url).map_err(|_| {
-            GitTypeError::ExtractionFailed("Failed to parse remote URL".to_string())
-        })?;
+        let repo_ref = GitRepositoryRefParser::parse(&remote_url).map_err(|e| {
+            GitTypeError::ExtractionFailed(format!("Failed to parse remote URL '{}': {}", remote_url, e))
+        })?;
src/infrastructure/http/oss_insight_client.rs (4)

32-40: Build URL with a proper builder to avoid encoding/concat pitfalls

Use reqwest::Url (re-export of url) to assemble query params. This removes manual concat and double‑encoding risks.

-        let mut url = format!(
-            "https://api.ossinsight.io/v1/trends/repos/?period={}",
-            api_period
-        );
-
-        if let Some(lang) = language {
-            let api_lang = self.map_language_name(lang);
-            url = format!("{}&language={}", url, urlencoding::encode(&api_lang));
-        }
+        let mut url = reqwest::Url::parse("https://api.ossinsight.io/v1/trends/repos/")
+            .expect("static URL");
+        url.query_pairs_mut().append_pair("period", api_period);
+        if let Some(lang) = language {
+            let api_lang = self.map_language_name(lang);
+            url.query_pairs_mut().append_pair("language", &api_lang);
+        }

51-56: Include response body snippet on non‑success to aid diagnosis

Surface a short body excerpt alongside status. Helps when API returns structured errors.

-        if !response.status().is_success() {
-            return Err(GitTypeError::ApiError(format!(
-                "OSS Insight API request failed: {}",
-                response.status()
-            )));
-        }
+        if !response.status().is_success() {
+            let status = response.status();
+            let body = response.text().await.unwrap_or_default();
+            let preview = body.chars().take(200).collect::<String>();
+            return Err(GitTypeError::ApiError(format!(
+                "OSS Insight API request failed: {} — {}",
+                status, preview
+            )));
+        }

64-81: Title‑case hyphenated or space‑separated languages (Objective‑C, Visual Basic)

Current logic lowercases the rest of the string, breaking Objective-C. Title‑case by segments while preserving known special cases.

     fn map_language_name(&self, lang: &str) -> String {
         match lang.to_lowercase().as_str() {
             "javascript" => "JavaScript".to_string(),
             "typescript" => "TypeScript".to_string(),
             "c++" => "C++".to_string(),
             "c#" => "C#".to_string(),
             "php" => "PHP".to_string(),
             _ => {
-                let mut chars = lang.chars();
-                match chars.next() {
-                    None => lang.to_string(),
-                    Some(first) => {
-                        first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
-                    }
-                }
+                // Title-case by '-' and ' ' while leaving other punctuation intact
+                lang.split(&['-', ' '][..])
+                    .map(|seg| {
+                        let mut it = seg.chars();
+                        match it.next() {
+                            None => String::new(),
+                            Some(f) => f.to_uppercase().collect::<String>()
+                                + &it.as_str().to_lowercase()
+                        }
+                    })
+                    .collect::<Vec<_>>()
+                    .join(if lang.contains('-') { "-" } else { " " })
             }
         }
     }

20-31: Validate period early or document fallback

Unknown periods silently map to daily. Either validate here with a ValidationError or document the fallback; consider aligning with CLI validation.

src/presentation/cli/commands/trending.rs (6)

41-52: Language validation UX is good; consider sharing the source of truth

Validation duplicates language knowledge that also exists in the HTTP client’s mapping. Extract a shared normalizer/validator to avoid drift.


55-56: Remove the dummy _client: &() parameter

This arg is unused and leaks refactor scaffolding into the public surface.

-        let repos = fetch_trending_repositories_cached(&(), language.as_deref(), &period).await?;
+        let repos = fetch_trending_repositories_cached(language.as_deref(), &period).await?;

And change the function signature below accordingly.

Also applies to: 72-73


112-121: Clean up fetch_trending_repositories_cached signature and normalize cache key

Normalize language for the key to prevent duplicate entries ("Rust" vs "rust").

-pub async fn fetch_trending_repositories_cached(
-    _client: &(),
-    language: Option<&str>,
-    period: &str,
-) -> Result<Vec<TrendingRepositoryInfo>> {
-    let cache_key = format!("{}:{}", language.unwrap_or("all"), period);
+pub async fn fetch_trending_repositories_cached(
+    language: Option<&str>,
+    period: &str,
+) -> Result<Vec<TrendingRepositoryInfo>> {
+    let cache_key = format!(
+        "{}:{}",
+        language.map(|s| s.to_ascii_lowercase()).as_deref().unwrap_or("all"),
+        period
+    );

119-132: Error handling: consider surfacing a concise user hint instead of silently empty list

Swallowing errors degrades silently. At least print a one‑liner to stderr for CLI users while keeping graceful fallback.

-        Err(e) => {
-            log::warn!("Failed to retrieve trending repositories: {}", e);
-            Ok(Vec::new()) // Return empty vec for graceful degradation
-        }
+        Err(e) => {
+            log::warn!("Failed to retrieve trending repositories: {}", e);
+            eprintln!("⚠️  Unable to fetch trending repositories right now. Showing no results.");
+            Ok(Vec::new())
+        }

135-142: Repository selection: prefer exact match before substring

Avoid surprising matches when many repos contain the same keyword. Try exact owner/repo first, then fallback to substring.

 fn select_repository_by_name<'a>(
     repos: &'a [TrendingRepositoryInfo],
     name: &str,
 ) -> Option<&'a TrendingRepositoryInfo> {
-    repos
-        .iter()
-        .find(|repo| repo.repo_name.to_lowercase().contains(&name.to_lowercase()))
+    let needle = name.to_ascii_lowercase();
+    repos.iter().find(|r| r.repo_name.eq_ignore_ascii_case(name))
+        .or_else(|| {
+            repos.iter().find(|r| {
+                r.repo_name
+                    .rsplit('/')
+                    .next()
+                    .map(|base| base.eq_ignore_ascii_case(&needle))
+                    .unwrap_or(false)
+            })
+        })
+        .or_else(|| {
+            repos
+                .iter()
+                .find(|r| r.repo_name.to_ascii_lowercase().contains(&needle))
+        })
 }

35-39: Validate period input at the CLI boundary

Provide fast feedback for typos and align with server expectations (daily|weekly|monthly).

 pub async fn run_trending(
     language: Option<String>,
     repo_name: Option<String>,
     period: String,
 ) -> Result<()> {
+    const SUPPORTED_PERIODS: &[&str] = &["daily", "weekly", "monthly"];
+    if !SUPPORTED_PERIODS.contains(&period.as_str()) {
+        eprintln!("❌ Unsupported period: '{}'", period);
+        eprintln!("📚 Supported periods: daily, weekly, monthly");
+        return Err(GitTypeError::ValidationError(format!(
+            "Unsupported period: {}",
+            period
+        )));
+    }
tests/unit/domain/repositories/trending_repository_tests.rs (4)

6-8: Avoid hardcoded cache path; use temp dir to keep tests hermetic.
Hardcoding “/mock/trending_cache” can fail on CI or non-Unix hosts. Prefer OS temp dir.

-fn create_test_trending_repository() -> TrendingRepository {
-    TrendingRepository::with_cache_dir(PathBuf::from("/mock/trending_cache"))
-}
+fn create_test_trending_repository() -> TrendingRepository {
+    let dir = std::env::temp_dir().join("gittype_trending_cache_test");
+    TrendingRepository::with_cache_dir(dir)
+}

25-35: Unit tests should not hit the live network.
These calls exercise real HTTP; results are nondeterministic and rate‑limit prone. Gate tests behind a mock (feature = "test-mocks") or inject a mock client into TrendingRepository.

If a mock feature exists (as with GitHubApiClient), consider mirroring it for OssInsightClient and running these tests with --features test-mocks.


63-74: Brittle assumption about failure path.
Asserting Ok+empty on “invalid” inputs ties the test to current error‑handling. Prefer explicit mocking to force an error and verify graceful degradation in a controlled way.


99-109: Implement the Default trait for TrendingRepository.
Calling TrendingRepository::default() works only because there’s an inherent fn default(). Idiomatic Rust expects impl Default. Consider implementing the trait and keeping the inherent method delegating to it.

src/infrastructure/http/github_api_client.rs (2)

33-51: Harden the request: GitHub headers, optional auth, better error context.
Add Accept and API‑Version headers, support GITHUB_TOKEN for higher rate limits, and include a short body snippet on non‑2xx for diagnostics.

-        pub async fn fetch_latest_release(&self) -> Result<GitHubRelease> {
-            let url = "https://api.github.com/repos/unhappychoice/gittype/releases/latest";
-            let response = self.client.get(url).send().await.map_err(|e| {
+        pub async fn fetch_latest_release(&self) -> Result<GitHubRelease> {
+            let url = "https://api.github.com/repos/unhappychoice/gittype/releases/latest";
+            let mut req = self
+                .client
+                .get(url)
+                .header("Accept", "application/vnd.github+json")
+                .header("X-GitHub-Api-Version", "2022-11-28");
+            if let Ok(token) = std::env::var("GITHUB_TOKEN") {
+                req = req.bearer_auth(token);
+            }
+            let response = req.send().await.map_err(|e| {
                 GitTypeError::ExtractionFailed(format!("Failed to fetch release: {}", e))
             })?;
 
             if !response.status().is_success() {
-                return Err(GitTypeError::ExtractionFailed(format!(
-                    "GitHub API request failed with status: {}",
-                    response.status()
-                )));
+                let status = response.status();
+                let snippet = response.text().await.unwrap_or_default();
+                let snippet = snippet.chars().take(200).collect::<String>();
+                return Err(GitTypeError::ExtractionFailed(format!(
+                    "GitHub API request failed: {} - {}",
+                    status, snippet
+                )));
             }
 
             let release: GitHubRelease = response.json().await.map_err(|e| {
                 GitTypeError::ExtractionFailed(format!("Failed to parse JSON: {}", e))
             })?;
 
             Ok(release)
         }

20-31: Consider making owner/repo configurable.
Hardcoding the repository ties the client to a single project. Accept (owner, repo) in new() or fetch_latest_release() and default to the current repo when not provided.

src/domain/services/version_service.rs (1)

53-63: Prefer the semver crate for robustness.
If tags include pre‑releases (e.g., “1.2.0‑beta”), numeric parsing will fail. Using semver::Version would handle this cleanly.

src/domain/repositories/version_repository.rs (2)

54-56: Avoid disabling cache in debug builds globally.
Returning None in debug bypasses cache for all debug runs, including integration tests, causing unnecessary network calls. Gate this under a test feature (e.g., test-mocks) or an env flag.

-        if cfg!(debug_assertions) {
+        #[cfg(feature = "test-mocks")]
+        {
             return Ok(None);
-        }
+        }

104-107: Normalize tags case‑insensitively and trim whitespace.
Git tags sometimes use uppercase ‘V’ or have stray whitespace.

-    fn normalize_version_tag(tag: &str) -> String {
-        tag.strip_prefix('v').unwrap_or(tag).to_string()
-    }
+    fn normalize_version_tag(tag: &str) -> String {
+        let t = tag.trim();
+        if let Some(rest) = t.strip_prefix('v').or_else(|| t.strip_prefix('V')) {
+            rest.to_string()
+        } else {
+            t.to_string()
+        }
+    }
src/infrastructure/git/remote/remote_git_repository_client.rs (1)

46-48: Be cautious removing existing directories.
Blind remove_dir_all can delete user data if path resolution misbehaves. Consider validating the path prefix (e.g., ensure it’s under $HOME/.gittype/repos) before removal.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27486c0 and 0fd88e2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (85)
  • Cargo.toml (3 hunks)
  • examples/seed_database.rs (1 hunks)
  • src/domain/models/git_repository_ref.rs (1 hunks)
  • src/domain/models/mod.rs (2 hunks)
  • src/domain/models/storage/repository.rs (2 hunks)
  • src/domain/repositories/challenge_repository.rs (6 hunks)
  • src/domain/repositories/git_repository_repository.rs (1 hunks)
  • src/domain/repositories/mod.rs (1 hunks)
  • src/domain/repositories/session_repository.rs (1 hunks)
  • src/domain/repositories/stage_repository.rs (1 hunks)
  • src/domain/repositories/trending_repository.rs (7 hunks)
  • src/domain/repositories/version_repository.rs (1 hunks)
  • src/domain/services/config_manager.rs (1 hunks)
  • src/domain/services/extractor/code_chunk_extractor.rs (5 hunks)
  • src/domain/services/extractor/core/extractor.rs (2 hunks)
  • src/domain/services/extractor/git_repository_extractor.rs (0 hunks)
  • src/domain/services/extractor/mod.rs (0 hunks)
  • src/domain/services/mod.rs (1 hunks)
  • src/domain/services/theme_manager.rs (2 hunks)
  • src/domain/services/version_service.rs (1 hunks)
  • src/infrastructure/cache/gzip_storage.rs (0 hunks)
  • src/infrastructure/cache/mod.rs (0 hunks)
  • src/infrastructure/config/mod.rs (0 hunks)
  • src/infrastructure/database/mod.rs (1 hunks)
  • src/infrastructure/database/seeders/database_seeder.rs (1 hunks)
  • src/infrastructure/external/mod.rs (0 hunks)
  • src/infrastructure/git/git_repository_ref_parser.rs (1 hunks)
  • src/infrastructure/git/local/local_git_repository_client.rs (1 hunks)
  • src/infrastructure/git/local/mod.rs (1 hunks)
  • src/infrastructure/git/mod.rs (1 hunks)
  • src/infrastructure/git/remote/mod.rs (1 hunks)
  • src/infrastructure/git/remote/remote_git_repository_client.rs (1 hunks)
  • src/infrastructure/http/github_api_client.rs (1 hunks)
  • src/infrastructure/http/mod.rs (1 hunks)
  • src/infrastructure/http/oss_insight_client.rs (1 hunks)
  • src/infrastructure/mod.rs (1 hunks)
  • src/infrastructure/repository_manager.rs (0 hunks)
  • src/infrastructure/storage/app_data_provider.rs (1 hunks)
  • src/infrastructure/storage/compressed_file_storage.rs (1 hunks)
  • src/infrastructure/storage/file_storage.rs (1 hunks)
  • src/infrastructure/storage/integration_test.rs (0 hunks)
  • src/infrastructure/storage/mod.rs (1 hunks)
  • src/infrastructure/version/cache.rs (0 hunks)
  • src/infrastructure/version/checker.rs (0 hunks)
  • src/infrastructure/version/mod.rs (0 hunks)
  • src/presentation/cli/args.rs (0 hunks)
  • src/presentation/cli/commands/game.rs (4 hunks)
  • src/presentation/cli/commands/repo.rs (1 hunks)
  • src/presentation/cli/commands/trending.rs (4 hunks)
  • src/presentation/cli/runner.rs (3 hunks)
  • src/presentation/cli/views/mod.rs (0 hunks)
  • src/presentation/cli/views/repo_list_view.rs (3 hunks)
  • src/presentation/cli/views/repo_play_view.rs (2 hunks)
  • src/presentation/cli/views/repo_utils.rs (0 hunks)
  • src/presentation/cli/views/trending_repository_selection_view.rs (2 hunks)
  • src/presentation/cli/views/trending_unified_view.rs (3 hunks)
  • src/presentation/game/models/loading_steps/cache_check_step.rs (2 hunks)
  • src/presentation/game/models/loading_steps/cloning_step.rs (2 hunks)
  • src/presentation/game/models/loading_steps/database_init_step.rs (1 hunks)
  • src/presentation/game/models/loading_steps/generating_step.rs (2 hunks)
  • src/presentation/game/models/loading_steps/scanning_step.rs (2 hunks)
  • src/presentation/game/screens/records_screen.rs (1 hunks)
  • src/presentation/game/screens/settings_screen.rs (2 hunks)
  • tests/integration/comment_processing_tests.rs (2 hunks)
  • tests/integration/indent_treesitter_tests.rs (1 hunks)
  • tests/integration/mod.rs (3 hunks)
  • tests/unit/domain/mod.rs (1 hunks)
  • tests/unit/domain/repositories/challenge_repository_tests.rs (1 hunks)
  • tests/unit/domain/repositories/mod.rs (1 hunks)
  • tests/unit/domain/repositories/trending_repository_tests.rs (1 hunks)
  • tests/unit/domain/services/config_service_tests.rs (1 hunks)
  • tests/unit/domain/services/extractor/core/mod.rs (1 hunks)
  • tests/unit/domain/services/extractor/mod.rs (1 hunks)
  • tests/unit/domain/services/mod.rs (1 hunks)
  • tests/unit/domain/services/version_service_tests.rs (1 hunks)
  • tests/unit/infrastructure/cache/challenge_cache_tests.rs (0 hunks)
  • tests/unit/infrastructure/cache/mod.rs (0 hunks)
  • tests/unit/infrastructure/git/git_repository_ref_parser_test.rs (1 hunks)
  • tests/unit/infrastructure/git/local_git_repository_client_test.rs (1 hunks)
  • tests/unit/infrastructure/git/mod.rs (1 hunks)
  • tests/unit/infrastructure/git/remote_git_repository_client_test.rs (1 hunks)
  • tests/unit/infrastructure/mod.rs (1 hunks)
  • tests/unit/infrastructure/version/cache_test.rs (0 hunks)
  • tests/unit/infrastructure/version/checker_test.rs (0 hunks)
  • tests/unit/infrastructure/version/mod.rs (0 hunks)
💤 Files with no reviewable changes (19)
  • src/infrastructure/version/mod.rs
  • tests/unit/infrastructure/cache/mod.rs
  • tests/unit/infrastructure/version/mod.rs
  • src/infrastructure/config/mod.rs
  • tests/unit/infrastructure/version/checker_test.rs
  • src/infrastructure/version/checker.rs
  • src/domain/services/extractor/mod.rs
  • src/presentation/cli/args.rs
  • src/presentation/cli/views/mod.rs
  • src/presentation/cli/views/repo_utils.rs
  • tests/unit/infrastructure/version/cache_test.rs
  • src/infrastructure/repository_manager.rs
  • src/infrastructure/external/mod.rs
  • src/infrastructure/storage/integration_test.rs
  • src/domain/services/extractor/git_repository_extractor.rs
  • src/infrastructure/cache/mod.rs
  • src/infrastructure/cache/gzip_storage.rs
  • src/infrastructure/version/cache.rs
  • tests/unit/infrastructure/cache/challenge_cache_tests.rs
🧰 Additional context used
🧬 Code graph analysis (43)
tests/unit/domain/services/config_service_tests.rs (1)
src/domain/services/config_manager.rs (2)
  • new (11-19)
  • get_config (21-23)
src/domain/models/git_repository_ref.rs (1)
src/domain/models/storage/repository.rs (1)
  • http_url (23-27)
tests/unit/infrastructure/git/local_git_repository_client_test.rs (1)
src/infrastructure/git/local/local_git_repository_client.rs (1)
  • is_git_repository (10-13)
src/presentation/cli/views/repo_play_view.rs (1)
src/infrastructure/git/remote/remote_git_repository_client.rs (1)
  • is_repository_cached (99-105)
src/presentation/game/models/loading_steps/cloning_step.rs (4)
src/infrastructure/git/remote/remote_git_repository_client.rs (1)
  • clone_repository (32-90)
src/infrastructure/git/local/local_git_repository_client.rs (1)
  • extract_git_repository (31-62)
src/presentation/game/screens/loading_screen.rs (1)
  • set_git_repository (187-217)
src/presentation/game/session_manager.rs (1)
  • set_git_repository (244-254)
src/domain/services/theme_manager.rs (1)
src/domain/services/config_manager.rs (1)
  • new (11-19)
src/infrastructure/database/seeders/database_seeder.rs (2)
src/domain/repositories/git_repository_repository.rs (1)
  • database (69-71)
src/infrastructure/database/database.rs (1)
  • database (118-118)
tests/unit/domain/services/version_service_tests.rs (2)
src/domain/repositories/version_repository.rs (1)
  • new (16-21)
src/domain/services/version_service.rs (1)
  • new (9-13)
tests/unit/domain/repositories/trending_repository_tests.rs (2)
src/domain/repositories/trending_repository.rs (3)
  • with_cache_dir (53-61)
  • default (33-35)
  • new (39-51)
src/infrastructure/http/oss_insight_client.rs (2)
  • default (101-103)
  • new (14-18)
src/presentation/game/models/loading_steps/database_init_step.rs (5)
src/domain/repositories/git_repository_repository.rs (1)
  • database (69-71)
src/domain/repositories/session_repository.rs (1)
  • database (478-480)
src/domain/repositories/stage_repository.rs (1)
  • database (86-88)
src/infrastructure/database/seeders/database_seeder.rs (1)
  • database (261-263)
src/infrastructure/database/database.rs (1)
  • database (118-118)
src/infrastructure/http/oss_insight_client.rs (1)
src/domain/repositories/trending_repository.rs (1)
  • new (39-51)
src/infrastructure/database/mod.rs (5)
src/domain/repositories/git_repository_repository.rs (1)
  • database (69-71)
src/domain/repositories/session_repository.rs (1)
  • database (478-480)
src/domain/repositories/stage_repository.rs (1)
  • database (86-88)
src/infrastructure/database/seeders/database_seeder.rs (1)
  • database (261-263)
src/infrastructure/database/database.rs (1)
  • database (118-118)
src/presentation/game/screens/records_screen.rs (2)
src/domain/repositories/session_repository.rs (1)
  • database (478-480)
src/infrastructure/database/database.rs (1)
  • database (118-118)
src/infrastructure/git/remote/remote_git_repository_client.rs (1)
src/infrastructure/git/git_repository_ref_parser.rs (1)
  • parse (7-17)
src/domain/repositories/session_repository.rs (4)
src/domain/repositories/git_repository_repository.rs (1)
  • database (69-71)
src/domain/repositories/stage_repository.rs (1)
  • database (86-88)
src/infrastructure/database/seeders/database_seeder.rs (1)
  • database (261-263)
src/infrastructure/database/database.rs (1)
  • database (118-118)
src/presentation/cli/commands/trending.rs (1)
src/domain/repositories/trending_repository.rs (1)
  • new (39-51)
src/domain/repositories/stage_repository.rs (4)
src/domain/repositories/git_repository_repository.rs (1)
  • database (69-71)
src/domain/repositories/session_repository.rs (1)
  • database (478-480)
src/infrastructure/database/seeders/database_seeder.rs (1)
  • database (261-263)
src/infrastructure/database/database.rs (1)
  • database (118-118)
src/domain/repositories/version_repository.rs (4)
src/domain/services/config_manager.rs (2)
  • FileStorage (14-15)
  • new (11-19)
src/domain/services/version_service.rs (1)
  • new (9-13)
src/infrastructure/http/github_api_client.rs (2)
  • new (21-31)
  • new (62-64)
src/infrastructure/storage/app_data_provider.rs (1)
  • get_app_data_dir (5-31)
src/domain/services/extractor/code_chunk_extractor.rs (3)
src/domain/services/extractor/parsers/mod.rs (1)
  • parse_with_thread_local (179-197)
src/infrastructure/git/local/local_git_repository_client.rs (1)
  • get_repository_root (15-29)
src/domain/services/extractor/core/extractor.rs (1)
  • extract_chunks_from_tree (11-194)
tests/unit/domain/repositories/challenge_repository_tests.rs (1)
src/domain/repositories/challenge_repository.rs (4)
  • with_cache_dir (47-52)
  • save_challenges (54-92)
  • clear_cache (147-153)
  • invalidate_repository (165-175)
src/domain/models/storage/repository.rs (2)
src/domain/models/git_repository_ref.rs (1)
  • http_url (9-11)
src/infrastructure/git/git_repository_ref_parser.rs (1)
  • parse (7-17)
src/presentation/game/screens/settings_screen.rs (1)
src/domain/services/config_manager.rs (1)
  • new (11-19)
src/domain/repositories/trending_repository.rs (2)
src/domain/repositories/challenge_repository.rs (3)
  • default (279-281)
  • new (36-45)
  • with_cache_dir (47-52)
src/infrastructure/http/oss_insight_client.rs (2)
  • default (101-103)
  • new (14-18)
src/infrastructure/git/local/local_git_repository_client.rs (1)
src/infrastructure/git/git_repository_ref_parser.rs (1)
  • parse (7-17)
tests/integration/mod.rs (3)
src/domain/services/extractor/parsers/mod.rs (2)
  • parse_with_thread_local (179-197)
  • new (52-87)
src/domain/services/extractor/core/extractor.rs (1)
  • extract_chunks_from_tree (11-194)
src/domain/services/extractor/language_registry.rs (1)
  • from_extension (65-69)
tests/integration/comment_processing_tests.rs (1)
tests/integration/mod.rs (1)
  • extract_from_file_for_test (50-59)
src/presentation/cli/views/trending_unified_view.rs (2)
src/domain/repositories/trending_repository.rs (1)
  • new (39-51)
src/infrastructure/http/oss_insight_client.rs (1)
  • new (14-18)
src/presentation/cli/views/repo_list_view.rs (1)
src/infrastructure/git/remote/remote_git_repository_client.rs (1)
  • is_repository_cached (99-105)
src/infrastructure/storage/file_storage.rs (2)
src/domain/services/config_manager.rs (1)
  • FileStorage (14-15)
src/infrastructure/storage/compressed_file_storage.rs (4)
  • delete_file (77-82)
  • delete_file (182-186)
  • file_exists (104-106)
  • file_exists (188-191)
src/domain/services/extractor/core/extractor.rs (1)
src/domain/services/extractor/parsers/mod.rs (1)
  • get_parser_registry (170-172)
src/domain/repositories/challenge_repository.rs (2)
src/domain/repositories/trending_repository.rs (3)
  • new (39-51)
  • default (33-35)
  • with_cache_dir (53-61)
src/infrastructure/storage/compressed_file_storage.rs (1)
  • default (123-127)
src/domain/services/version_service.rs (2)
src/domain/repositories/version_repository.rs (1)
  • new (16-21)
src/infrastructure/http/github_api_client.rs (2)
  • new (21-31)
  • new (62-64)
examples/seed_database.rs (5)
src/domain/repositories/git_repository_repository.rs (1)
  • database (69-71)
src/domain/repositories/session_repository.rs (1)
  • database (478-480)
src/domain/repositories/stage_repository.rs (1)
  • database (86-88)
src/infrastructure/database/seeders/database_seeder.rs (1)
  • database (261-263)
src/infrastructure/database/database.rs (1)
  • database (118-118)
src/domain/services/config_manager.rs (2)
src/domain/repositories/version_repository.rs (1)
  • new (16-21)
src/infrastructure/storage/app_data_provider.rs (1)
  • get_app_data_dir (5-31)
tests/unit/infrastructure/git/git_repository_ref_parser_test.rs (1)
src/infrastructure/git/git_repository_ref_parser.rs (1)
  • parse (7-17)
src/infrastructure/http/github_api_client.rs (1)
src/domain/repositories/version_repository.rs (1)
  • new (16-21)
src/domain/repositories/git_repository_repository.rs (4)
src/domain/repositories/session_repository.rs (1)
  • database (478-480)
src/domain/repositories/stage_repository.rs (1)
  • database (86-88)
src/infrastructure/database/seeders/database_seeder.rs (1)
  • database (261-263)
src/infrastructure/database/database.rs (1)
  • database (118-118)
tests/unit/infrastructure/git/remote_git_repository_client_test.rs (1)
src/infrastructure/git/remote/remote_git_repository_client.rs (1)
  • is_repository_complete (92-97)
tests/integration/indent_treesitter_tests.rs (1)
src/domain/services/extractor/core/extractor.rs (1)
  • extract_chunks_from_tree (11-194)
src/presentation/cli/commands/game.rs (4)
src/domain/services/version_service.rs (1)
  • new (9-13)
src/presentation/game/screens/version_check_screen.rs (1)
  • show_legacy (31-44)
src/domain/services/theme_manager.rs (1)
  • init (22-44)
src/infrastructure/logging.rs (1)
  • log_error_to_file (133-163)
src/presentation/cli/commands/repo.rs (2)
src/domain/repositories/git_repository_repository.rs (1)
  • database (69-71)
src/infrastructure/database/database.rs (1)
  • database (118-118)
src/infrastructure/storage/compressed_file_storage.rs (2)
src/domain/repositories/challenge_repository.rs (3)
  • new (36-45)
  • default (279-281)
  • storage (184-185)
src/infrastructure/storage/file_storage.rs (4)
  • delete_file (60-65)
  • delete_file (99-101)
  • file_exists (68-70)
  • file_exists (103-105)
src/infrastructure/mod.rs (5)
src/domain/repositories/git_repository_repository.rs (1)
  • database (69-71)
src/domain/repositories/session_repository.rs (1)
  • database (478-480)
src/domain/repositories/stage_repository.rs (1)
  • database (86-88)
src/infrastructure/database/seeders/database_seeder.rs (1)
  • database (261-263)
src/infrastructure/database/database.rs (1)
  • database (118-118)
🔇 Additional comments (13)
tests/unit/domain/services/config_service_tests.rs (1)

6-36: Confirm tests run against an isolated config path

Line [6] and the other tests invoke ConfigService::new()/save() directly. Unless the test build swaps in an in-memory FileStorage via the new infrastructure, this will hit the real config path (e.g., ~/.config/gittype) and create/overwrite user files when the suite runs. Please confirm that the test-mocks wiring actually shields the filesystem here, or redirect the service to a temp directory inside the test to keep the suite hermetic.

src/infrastructure/database/mod.rs (1)

1-5: Clean module façade

The module surface now mirrors the new database layout and keeps lint noise out—looks good.

src/domain/models/git_repository_ref.rs (1)

1-11: Solid Git repository ref helper

Deriving Debug/Clone and centralizing http_url() generation gives downstream code a clean, testable primitive. Nice addition.

src/infrastructure/http/mod.rs (1)

1-5: HTTP façade reads well

Re-exporting the clients from the module root makes the new HTTP layer easy to consume—no issues spotted.

src/presentation/game/models/loading_steps/cloning_step.rs (1)

2-72: Cloning flow integrates smoothly

The step now wires progress reporting, cloning, and metadata extraction through the new clients without regressions—LGTM.

src/infrastructure/mod.rs (1)

1-3: Module surface matches new infrastructure split.

The updated re-exports cleanly mirror the new database/git/http folders without leaving stale modules behind. 👍

Cargo.toml (1)

76-89: Thank you for wiring the test-mocks feature path.

Adding async-trait and the self-referencing dev dependency keeps integration tests aligned with the new mocks while leaving the release feature surface empty, which is exactly what we want here.

src/presentation/game/models/loading_steps/cache_check_step.rs (1)

82-92: Repository swap keeps cache-check semantics intact.

load_challenges_with_progress slots into the existing control flow, preserves the early-return on miss, and continues to update progress when available—looks solid.

src/presentation/cli/commands/repo.rs (1)

1-8: Imports now reflect the database refactor.

Pointing the CLI commands at infrastructure::database and trimming the old config dependency lines everything up with the new layout—nice catch.

src/domain/repositories/mod.rs (1)

8-13: Re-exports are consistent and clear.

The chosen names and exports align with Clean Architecture boundaries and keep call sites tidy.

src/domain/services/theme_manager.rs (1)

21-44: Sanity check: corrupted config should not brick theme init.

Currently, any error in ConfigService::new() aborts init. If you prefer resilience, catch and log, then proceed with defaults.

Would you like a small patch to default on config read errors and log a warning instead?

src/domain/services/config_manager.rs (1)

10-19: Config loading path is clean and aligns with storage provider.

Constructing the path via AppDataProvider and defaulting when missing keeps things predictable.

tests/unit/domain/services/version_service_tests.rs (1)

2-2: Remove redundant #[cfg(feature = "test-mocks")] guard
Integration tests already enable the test-mocks feature via the crate’s [dev-dependencies], so gating the version_service_tests module is unnecessary.

Likely an incorrect or invalid review comment.

Comment on lines +91 to +102
/// Check if a cache entry is still valid
fn is_cache_valid(&self, entry: &VersionCacheEntry, frequency_hours: u64) -> bool {
let now = chrono::Utc::now();
let hours_since_check = (now - entry.last_checked).num_hours();
let time_valid = hours_since_check < frequency_hours as i64;

// Also check if the current version matches
let current_version = env!("CARGO_PKG_VERSION");
let version_valid = entry.current_version == current_version;

time_valid && version_valid
}
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.

⚠️ Potential issue | 🟡 Minor

Guard against clock skew when validating cache.
If last_checked is in the future, hours_since_check is negative and the entry is incorrectly treated as valid.

     fn is_cache_valid(&self, entry: &VersionCacheEntry, frequency_hours: u64) -> bool {
         let now = chrono::Utc::now();
-        let hours_since_check = (now - entry.last_checked).num_hours();
+        let hours_since_check = (now - entry.last_checked).num_hours();
+        if hours_since_check < 0 {
+            return false;
+        }
         let time_valid = hours_since_check < frequency_hours as i64;
 
         // Also check if the current version matches
         let current_version = env!("CARGO_PKG_VERSION");
         let version_valid = entry.current_version == current_version;
 
         time_valid && version_valid
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Check if a cache entry is still valid
fn is_cache_valid(&self, entry: &VersionCacheEntry, frequency_hours: u64) -> bool {
let now = chrono::Utc::now();
let hours_since_check = (now - entry.last_checked).num_hours();
let time_valid = hours_since_check < frequency_hours as i64;
// Also check if the current version matches
let current_version = env!("CARGO_PKG_VERSION");
let version_valid = entry.current_version == current_version;
time_valid && version_valid
}
/// Check if a cache entry is still valid
fn is_cache_valid(&self, entry: &VersionCacheEntry, frequency_hours: u64) -> bool {
let now = chrono::Utc::now();
let hours_since_check = (now - entry.last_checked).num_hours();
if hours_since_check < 0 {
return false;
}
let time_valid = hours_since_check < frequency_hours as i64;
// Also check if the current version matches
let current_version = env!("CARGO_PKG_VERSION");
let version_valid = entry.current_version == current_version;
time_valid && version_valid
}
🤖 Prompt for AI Agents
In src/domain/repositories/version_repository.rs around lines 91 to 102, the
cache validation allows entries whose last_checked timestamp is in the future
(clock skew) because num_hours() becomes negative; modify the function to
explicitly guard against future timestamps by returning false if
entry.last_checked > now, otherwise compute hours_since_check normally and
proceed with the existing frequency and version checks. Ensure you compare
chrono::DateTime values directly for the future check and only cast num_hours()
after confirming now >= last_checked.

Comment on lines +32 to +39
// Find git root once at the beginning using the first file
let git_root = files_to_process
.first()
.map(|(first_file, _)| first_file)
.and_then(|path| LocalGitRepositoryClient::get_repository_root(path))
.ok_or_else(|| {
GitTypeError::ExtractionFailed("Git repository not found".to_string())
})?;
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.

⚠️ Potential issue | 🟠 Major

Handle empty inputs and non-git paths without aborting
This new guard now returns ExtractionFailed whenever files_to_process is empty or when the first path is outside a git repo. Both cases previously worked (we simply returned an empty chunk list or used the absolute path), and several flows still rely on that—for example, CLI calls where nothing matches a filter, or test fixtures in temp dirs that aren’t part of a repo. After this change those scenarios now error out before any extraction runs. Please keep the early-return/parent-path fallback so we don’t regress these code paths. Based on learnings

Here is a possible fix:

+        if files_to_process.is_empty() {
+            progress.set_file_counts(StepType::Extracting, 0, 0, None);
+            return Ok(Vec::new());
+        }
+
-        let git_root = files_to_process
-            .first()
-            .map(|(first_file, _)| first_file)
-            .and_then(|path| LocalGitRepositoryClient::get_repository_root(path))
-            .ok_or_else(|| {
-                GitTypeError::ExtractionFailed("Git repository not found".to_string())
-            })?;
+        let (first_path, _) = files_to_process.first().expect("checked above");
+        let git_root = LocalGitRepositoryClient::get_repository_root(first_path)
+            .or_else(|| first_path.parent().map(|p| p.to_path_buf()))
+            .unwrap_or_else(|| first_path.clone());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Find git root once at the beginning using the first file
let git_root = files_to_process
.first()
.map(|(first_file, _)| first_file)
.and_then(|path| LocalGitRepositoryClient::get_repository_root(path))
.ok_or_else(|| {
GitTypeError::ExtractionFailed("Git repository not found".to_string())
})?;
// Find git root once at the beginning using the first file
if files_to_process.is_empty() {
progress.set_file_counts(StepType::Extracting, 0, 0, None);
return Ok(Vec::new());
}
let (first_path, _) = files_to_process.first().expect("checked above");
let git_root = LocalGitRepositoryClient::get_repository_root(first_path)
.or_else(|| first_path.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| first_path.clone());

Comment on lines +33 to +51
/// Compare two version strings and determine if the first is newer
fn is_version_newer(latest: &str, current: &str) -> bool {
let latest_parts = Self::parse_version(latest);
let current_parts = Self::parse_version(current);

match (latest_parts, current_parts) {
(Ok(latest), Ok(current)) => {
for (l, c) in latest.iter().zip(current.iter()) {
if l > c {
return true;
} else if l < c {
return false;
}
}
latest.len() > current.len()
}
_ => false,
}
}
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.

⚠️ Potential issue | 🟠 Major

Fix version comparison: treat trailing zeros as equal.
Current logic marks 1.2.0 newer than 1.2, which is incorrect. Trim trailing zeros before length comparison.

-    fn is_version_newer(latest: &str, current: &str) -> bool {
-        let latest_parts = Self::parse_version(latest);
-        let current_parts = Self::parse_version(current);
-
-        match (latest_parts, current_parts) {
-            (Ok(latest), Ok(current)) => {
-                for (l, c) in latest.iter().zip(current.iter()) {
-                    if l > c {
-                        return true;
-                    } else if l < c {
-                        return false;
-                    }
-                }
-                latest.len() > current.len()
-            }
-            _ => false,
-        }
-    }
+    fn is_version_newer(latest: &str, current: &str) -> bool {
+        let mut latest = match Self::parse_version(latest) { Ok(v) => v, Err(_) => return false };
+        let mut current = match Self::parse_version(current) { Ok(v) => v, Err(_) => return false };
+        while latest.last().is_some() && latest.last() == Some(&0) { latest.pop(); }
+        while current.last().is_some() && current.last() == Some(&0) { current.pop(); }
+        for (l, c) in latest.iter().zip(current.iter()) {
+            if l > c { return true; }
+            if l < c { return false; }
+        }
+        latest.len() > current.len()
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Compare two version strings and determine if the first is newer
fn is_version_newer(latest: &str, current: &str) -> bool {
let latest_parts = Self::parse_version(latest);
let current_parts = Self::parse_version(current);
match (latest_parts, current_parts) {
(Ok(latest), Ok(current)) => {
for (l, c) in latest.iter().zip(current.iter()) {
if l > c {
return true;
} else if l < c {
return false;
}
}
latest.len() > current.len()
}
_ => false,
}
}
/// Compare two version strings and determine if the first is newer
fn is_version_newer(latest: &str, current: &str) -> bool {
let mut latest = match Self::parse_version(latest) { Ok(v) => v, Err(_) => return false };
let mut current = match Self::parse_version(current) { Ok(v) => v, Err(_) => return false };
while latest.last().is_some() && latest.last() == Some(&0) { latest.pop(); }
while current.last().is_some() && current.last() == Some(&0) { current.pop(); }
for (l, c) in latest.iter().zip(current.iter()) {
if l > c { return true; }
if l < c { return false; }
}
latest.len() > current.len()
}
🤖 Prompt for AI Agents
In src/domain/services/version_service.rs around lines 33 to 51, the comparison
treats versions like "1.2.0" as newer than "1.2"; to fix it, trim trailing zeros
from both parsed version component vectors before performing the final
length-based comparison: after parsing, remove any trailing zeros from
latest_parts and current_parts, then iterate over the zipped components to
compare numerically as before, and finally use the trimmed lengths to decide
(longer => newer); ensure non-numeric parse errors still return false.

Comment on lines +8 to +17
match repository_ref {
spec if spec.contains('@') => Self::parse_ssh_format(spec),
spec if spec.starts_with("http") => Self::parse_https_format(spec),
spec if spec.contains('/') && !spec.contains(' ') => Self::parse_short_format(spec),
_ => Err(GitTypeError::InvalidRepositoryFormat(format!(
"Unsupported repository format: {}",
repository_ref
))),
}
}
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.

⚠️ Potential issue | 🟠 Major

Avoid misclassifying scheme URIs (e.g., ssh://) and guard short refs against ':'

Current dispatch treats any string with '@' as SCP‑style SSH and any with '/' as short, which misroutes inputs like ssh://git@host/owner/name and accepts invalid github.com:owner/name as short. Tighten the conditions and trim input.

Apply:

-    pub fn parse(repository_ref: &str) -> Result<GitRepositoryRef> {
-        match repository_ref {
-            spec if spec.contains('@') => Self::parse_ssh_format(spec),
-            spec if spec.starts_with("http") => Self::parse_https_format(spec),
-            spec if spec.contains('/') && !spec.contains(' ') => Self::parse_short_format(spec),
+    pub fn parse(repository_ref: &str) -> Result<GitRepositoryRef> {
+        let spec = repository_ref.trim();
+        match spec {
+            s if s.starts_with("http://") || s.starts_with("https://") => {
+                Self::parse_https_format(s)
+            }
+            // SCP-like only: must contain '@' and ':' and not be a scheme URL
+            s if s.contains('@') && s.contains(':') && !s.contains("://") => {
+                Self::parse_ssh_format(s)
+            }
+            // Short refs must not contain ':' or a scheme
+            s if s.contains('/') && !s.contains(' ') && !s.contains(':') && !s.contains("://") => {
+                Self::parse_short_format(s)
+            }
             _ => Err(GitTypeError::InvalidRepositoryFormat(format!(
                 "Unsupported repository format: {}",
-                repository_ref
+                spec
             ))),
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
match repository_ref {
spec if spec.contains('@') => Self::parse_ssh_format(spec),
spec if spec.starts_with("http") => Self::parse_https_format(spec),
spec if spec.contains('/') && !spec.contains(' ') => Self::parse_short_format(spec),
_ => Err(GitTypeError::InvalidRepositoryFormat(format!(
"Unsupported repository format: {}",
repository_ref
))),
}
}
pub fn parse(repository_ref: &str) -> Result<GitRepositoryRef> {
let spec = repository_ref.trim();
match spec {
s if s.starts_with("http://") || s.starts_with("https://") => {
Self::parse_https_format(s)
}
// SCP-like only: must contain '@' and ':' and not be a scheme URL
s if s.contains('@') && s.contains(':') && !s.contains("://") => {
Self::parse_ssh_format(s)
}
// Short refs must not contain ':' or a scheme
s if s.contains('/') && !s.contains(' ') && !s.contains(':') && !s.contains("://") => {
Self::parse_short_format(s)
}
_ => Err(GitTypeError::InvalidRepositoryFormat(format!(
"Unsupported repository format: {}", spec
))),
}
}
🤖 Prompt for AI Agents
In src/infrastructure/git/git_repository_ref_parser.rs around lines 8 to 17, the
dispatcher misclassifies URIs like "ssh://..." as SCP-style and accepts refs
with ':' as short form; trim repository_ref first, then refine the branches:
check explicit URI schemes first (e.g., starts_with("ssh://") or
starts_with("http")) to route to parse_ssh_format or parse_https_format; detect
SCP-style SSH only when the trimmed string contains '@' AND contains ':' AND
does not contain "://"; only treat the short format when it contains '/' and
does NOT contain ':' or "://"; on failure return the same
InvalidRepositoryFormat error. Ensure trimming is applied once at the top.

Comment on lines +19 to +43
fn parse_ssh_format(repo_spec: &str) -> Result<GitRepositoryRef> {
let (host_part, repo_part) = repo_spec.split_once(':').ok_or_else(|| {
GitTypeError::InvalidRepositoryFormat("Invalid SSH repository format".to_string())
})?;

let origin = host_part
.split('@')
.nth(1)
.unwrap_or("github.com")
.to_string();

let (owner, name) = repo_part
.strip_suffix(".git")
.unwrap_or(repo_part)
.split_once('/')
.ok_or_else(|| {
GitTypeError::InvalidRepositoryFormat("Invalid repository path format".to_string())
})?;

Ok(GitRepositoryRef {
origin,
owner: owner.to_string(),
name: name.to_string(),
})
}
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.

⚠️ Potential issue | 🟠 Major

SSH parsing: don’t default origin; validate non-empty owner/name; accept optional leading “/” after colon

Falling back to github.com hides malformed inputs. Also guard empty segments and normalize repo_part.

-    fn parse_ssh_format(repo_spec: &str) -> Result<GitRepositoryRef> {
+    fn parse_ssh_format(repo_spec: &str) -> Result<GitRepositoryRef> {
         let (host_part, repo_part) = repo_spec.split_once(':').ok_or_else(|| {
             GitTypeError::InvalidRepositoryFormat("Invalid SSH repository format".to_string())
         })?;
 
-        let origin = host_part
-            .split('@')
-            .nth(1)
-            .unwrap_or("github.com")
-            .to_string();
+        let origin = host_part
+            .split('@')
+            .nth(1)
+            .ok_or_else(|| {
+                GitTypeError::InvalidRepositoryFormat(
+                    "Invalid SSH repository format (missing '@')".to_string(),
+                )
+            })?
+            .to_string();
 
-        let (owner, name) = repo_part
-            .strip_suffix(".git")
-            .unwrap_or(repo_part)
-            .split_once('/')
+        // Support "git@host:/owner/name(.git)" and validate parts
+        let repo_part = repo_part.trim_start_matches('/');
+        let (owner, name_raw) = repo_part
+            .split_once('/')
             .ok_or_else(|| {
                 GitTypeError::InvalidRepositoryFormat("Invalid repository path format".to_string())
             })?;
+        let name = name_raw.strip_suffix(".git").unwrap_or(name_raw);
+        if owner.is_empty() || name.is_empty() {
+            return Err(GitTypeError::InvalidRepositoryFormat(
+                "Invalid repository path format".to_string(),
+            ));
+        }
 
         Ok(GitRepositoryRef {
             origin,
             owner: owner.to_string(),
             name: name.to_string(),
         })
     }
🤖 Prompt for AI Agents
In src/infrastructure/git/git_repository_ref_parser.rs around lines 19 to 43,
the SSH parser currently defaults a missing host to "github.com", doesn't accept
an optional leading '/' after the colon, and doesn't validate that owner and
name are non-empty; change it to (1) extract the host from host_part by taking
the segment after '@' (or the whole host_part if no '@') and return a
GitTypeError::InvalidRepositoryFormat when that host is empty instead of
defaulting to github.com; (2) normalize repo_part by trimming a leading '/'
before processing; (3) strip the ".git" suffix then split_once('/') and
explicitly check that both owner and name are non-empty, returning the same
InvalidRepositoryFormat error when either is empty; finally construct
GitRepositoryRef with the validated origin, owner and name.

Comment on lines +27 to +30
#[cfg(feature = "test-mocks")]
{
Ok(PathBuf::from("/tmp/test"))
}
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.

⚠️ Potential issue | 🟠 Major

Hard‑coded “/tmp/test” breaks on Windows and may not exist.

Use a cross‑platform temp location and ensure it exists.

-            Ok(PathBuf::from("/tmp/test"))
+            {
+                let dir = std::env::temp_dir().join("gittype-test");
+                std::fs::create_dir_all(&dir).map_err(|e| {
+                    GitTypeError::ExtractionFailed(format!(
+                        "Failed to create test app data dir {}: {}",
+                        dir.display(),
+                        e
+                    ))
+                })?;
+                Ok(dir)
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[cfg(feature = "test-mocks")]
{
Ok(PathBuf::from("/tmp/test"))
}
#[cfg(feature = "test-mocks")]
{
let dir = std::env::temp_dir().join("gittype-test");
std::fs::create_dir_all(&dir).map_err(|e| {
GitTypeError::ExtractionFailed(format!(
"Failed to create test app data dir {}: {}",
dir.display(),
e
))
})?;
Ok(dir)
}
🤖 Prompt for AI Agents
In src/infrastructure/storage/app_data_provider.rs around lines 27 to 30, the
code returns a hard-coded "/tmp/test" which fails on Windows and may not exist;
replace it with std::env::temp_dir(), append a suitable subdirectory (e.g.
"test" or crate-specific folder), create the directory if missing using
std::fs::create_dir_all, and return the resulting PathBuf wrapped in Ok,
propagating any filesystem errors as appropriate.

Comment on lines +95 to +101
std::fs::read_dir(dir_path)
.unwrap_or_else(|_| std::fs::read_dir(".").unwrap())
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.is_file())
.collect()
}
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.

⚠️ Potential issue | 🔴 Critical

Fix the fallback that wipes unrelated files on read errors
If read_dir(dir_path) errors (permission, transient FS issue, etc.) we fall back to read_dir("."). clear_cache() and every other caller then happily delete whatever is in the process working directory. One permission glitch on the cache dir can nuke unrelated project files. Please stop falling back to . and surface the error or return an empty list instead.

-        std::fs::read_dir(dir_path)
-            .unwrap_or_else(|_| std::fs::read_dir(".").unwrap())
-            .filter_map(|entry| entry.ok())
-            .map(|entry| entry.path())
-            .filter(|path| path.is_file())
-            .collect()
+        match std::fs::read_dir(dir_path) {
+            Ok(entries) => entries
+                .filter_map(|entry| entry.ok())
+                .map(|entry| entry.path())
+                .filter(|path| path.is_file())
+                .collect(),
+            Err(_) => Vec::new(),
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
std::fs::read_dir(dir_path)
.unwrap_or_else(|_| std::fs::read_dir(".").unwrap())
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.is_file())
.collect()
}
match std::fs::read_dir(dir_path) {
Ok(entries) => entries
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.is_file())
.collect(),
Err(_) => Vec::new(),
}
}
🤖 Prompt for AI Agents
In src/infrastructure/storage/compressed_file_storage.rs around lines 95 to 101,
the code falls back to reading the process CWD on read_dir(dir_path) errors
which can cause deletion of unrelated files; instead, stop using read_dir(".")
and either propagate the error to the caller or return an empty Vec<PathBuf>.
Modify this function to handle the read_dir(dir_path) Result explicitly: on Err,
either map the error to a returned Err (adjust the function signature to return
Result<Vec<PathBuf>, std::io::Error>) or log and return Ok(Vec::new()) so
callers won't act on the wrong directory; remove the current unwrap_or_else
fallback to ".".

Comment on lines +71 to +72
if let Ok(chunks) = extract_from_file_for_test(file_path, language.name()) {
all_chunks.extend(chunks);
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.

⚠️ Potential issue | 🟠 Major

Don't swallow extraction failures

By wrapping extract_from_file_for_test in if let Ok(...), any parsing/extraction error quietly disappears and the tests still pass, masking real regressions. These helpers should surface failures just like the production extractor so the test suite goes red when chunk extraction breaks. Please propagate the error instead of discarding it.

-                if let Ok(chunks) = extract_from_file_for_test(file_path, language.name()) {
-                    all_chunks.extend(chunks);
-                }
+                let chunks = extract_from_file_for_test(file_path, language.name())?;
+                all_chunks.extend(chunks);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Ok(chunks) = extract_from_file_for_test(file_path, language.name()) {
all_chunks.extend(chunks);
let chunks = extract_from_file_for_test(file_path, language.name())?;
all_chunks.extend(chunks);
🤖 Prompt for AI Agents
In tests/integration/mod.rs around lines 71-72, the test currently swallows
extraction errors by using `if let Ok(...)` which hides failures; change the
call to propagate errors so the test fails on extraction problems — replace the
`if let Ok(chunks) = extract_from_file_for_test(...) {
all_chunks.extend(chunks); }` pattern with a form that returns or bubbles up the
error (use the `?` operator if the test fn returns Result, or call
`.expect("chunk extraction failed")`/`.unwrap()` if it must panic), ensuring
extraction errors are not silently ignored.

Comment on lines +91 to +92
if let Ok(chunks) = extract_from_file_for_test(&file_path, language.name()) {
all_chunks.extend(chunks);
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.

⚠️ Potential issue | 🟠 Major

Propagate extraction errors in test helper

Same issue here: swallowing the Err means broken extraction won’t fail the test run. Let the Result bubble up so tests remain trustworthy.

-        if let Ok(chunks) = extract_from_file_for_test(&file_path, language.name()) {
-            all_chunks.extend(chunks);
-        }
+        let chunks = extract_from_file_for_test(&file_path, language.name())?;
+        all_chunks.extend(chunks);
🤖 Prompt for AI Agents
In tests/integration/mod.rs around lines 91-92, the helper currently swallows
extraction errors with an if let Ok(...) pattern, which hides failures; replace
the if-let with a direct call that propagates the Result (use the ? operator) so
any Err bubbles up to the test runner, and update the helper function's
signature to return Result<..., E> (or anyhow::Result) to accommodate
propagation; ensure callers handle the propagated Result accordingly.

Comment on lines +34 to +37
fn create_test_cache() -> ChallengeRepository {
ChallengeRepository::with_cache_dir(PathBuf::from("/mock/cache"))
}

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.

⚠️ Potential issue | 🔴 Critical

Fix cache dir setup: current path breaks the tests.

create_test_cache hard-codes /mock/cache. On a typical CI/Local run the process cannot create folders under /, so save_challenges returns Err and every assertion expecting Ok(()) will fail. Even if the directory existed, all tests would contend for the same path; when the suite runs in parallel, clear_cache/invalidate_repository from one test wipe the data another test is asserting on. Please give each test its own writable temp directory (e.g., tempfile::TempDir) and keep it alive for the test’s duration before calling ChallengeRepository::with_cache_dir. For example:

-    fn create_test_cache() -> ChallengeRepository {
-        ChallengeRepository::with_cache_dir(PathBuf::from("/mock/cache"))
-    }
+    fn create_test_cache() -> (ChallengeRepository, tempfile::TempDir) {
+        let dir = tempfile::TempDir::new().expect("temp cache dir");
+        let cache = ChallengeRepository::with_cache_dir(dir.path().to_path_buf());
+        (cache, dir)
+    }

Then destructure (cache, _dir) in each test so the temp directory isn’t dropped prematurely. This keeps the tests hermetic and fixes the permission failures.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn create_test_cache() -> ChallengeRepository {
ChallengeRepository::with_cache_dir(PathBuf::from("/mock/cache"))
}
fn create_test_cache() -> (ChallengeRepository, tempfile::TempDir) {
let dir = tempfile::TempDir::new().expect("temp cache dir");
let cache = ChallengeRepository::with_cache_dir(dir.path().to_path_buf());
(cache, dir)
}
🤖 Prompt for AI Agents
In tests/unit/domain/repositories/challenge_repository_tests.rs around lines 34
to 37, create_test_cache currently returns a ChallengeRepository using a
hard-coded "/mock/cache" which fails on CI and causes test interference; change
it to create a unique writable temp directory per test using tempfile::TempDir,
keep the TempDir alive for the test's duration (return or destructure as (repo,
_dir) in each test so the TempDir isn't dropped), and pass TempDir.path()
(converted to PathBuf) into ChallengeRepository::with_cache_dir so each test
gets its own hermetic, writable cache directory.

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.

1 participant