Skip to content

GSoC Module A : Week 3: feat(harvester): implement incremental change detection (stacked on #983)#985

Open
ParthAggarwal16 wants to merge 21 commits into
OWASP:mainfrom
ParthAggarwal16:week_3-clean
Open

GSoC Module A : Week 3: feat(harvester): implement incremental change detection (stacked on #983)#985
ParthAggarwal16 wants to merge 21 commits into
OWASP:mainfrom
ParthAggarwal16:week_3-clean

Conversation

@ParthAggarwal16

Copy link
Copy Markdown
Contributor

Summary

(this PR is stacked on top of #983 )
This PR introduces change detection and checkpoint persistence for the harvester pipeline.

It adds Git-based change detection utilities for discovering modified files and commits since a checkpoint, along with a lightweight checkpoint store for persisting repository processing state across pipeline runs.

Note: This PR is part of the Week 3 implementation and is intended to be reviewed after Week 2 (Repository Client), even though it targets main.

Added

  • ChangeDetector for Git-based incremental change detection
  • Modified file discovery since a commit
  • Commit history retrieval since a checkpoint
  • JSON-backed CheckpointStore
  • Repository checkpoint persistence
  • Repository checkpoint loading
  • RepositoryCheckpoint model
  • RepositoryChangeSet model
  • Unit tests for change detection
  • Unit tests for checkpoint persistence

Validation Coverage

Change Detection

  • Modified file discovery
  • Duplicate file filtering
  • Commit history retrieval
  • Commit ordering validation

Checkpoint Store

  • Save checkpoint
  • Load existing checkpoint
  • Missing checkpoint file handling
  • Missing repository checkpoint handling

Test Plan

Executed:

python3 -m unittest application.tests.harvester_test.change_detector_test
python3 -m unittest application.tests.harvester_test.checkpoint_store_test
python3 -m unittest discover -s application/tests/harvester_test -p "*_test.py"
make test

All tests passing.

Notes for Reviewers

  • Checkpoints are stored as simple JSON to keep the implementation lightweight and easy to inspect during development.
  • ChangeDetector is intentionally separated from the repository client so repository operations and incremental change detection remain independent concerns.
  • Duplicate modified files are normalized before being returned to downstream pipeline stages.
  • This PR establishes the incremental harvesting foundation used by the following pipeline stages.

1: High-Level Architecture (HLA)

image
  1. Class Diagram
image
  1. Data Flow Diagram — get_modified_files_since
image
  1. Data Flow Diagram — get_commits_since
image
  1. Sequence Diagram — Checkpoint Save/Load
image
  1. End-to-End Incremental Harvest Flow (conceptual, based on the two components together)
image

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ParthAggarwal16, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: c9e0739a-5288-42b1-a1dc-976ab03ae143

📥 Commits

Reviewing files that changed from the base of the PR and between fee277f and ccb515a.

📒 Files selected for processing (1)
  • application/utils/harvester/checkpoint_store.py

Walkthrough

Adds the harvester configuration schema and validation stack, Git repository caching and synchronization utilities, incremental change detection, checkpoint persistence, repository defaults, exclusion patterns, and comprehensive unit-test fixtures.

Changes

Harvester foundation

Layer / File(s) Summary
Configuration and validation contracts
application/utils/harvester/schemas.py, application/utils/harvester/config_loader.py, application/utils/harvester/repos_validator.py, application/utils/harvester/repos.yaml, application/utils/harvester/exclude_patterns.txt, application/tests/harvester_test/config_loader_test.py, application/tests/harvester_test/repos_validator_test.py, application/tests/harvester_test/fixtures/*
Defines Pydantic repository configuration models, YAML loading errors, duplicate-entry validation, default repository configurations, exclusion patterns, and fixture-backed validation tests.
Repository cache and Git lifecycle
application/utils/harvester/repository_client.py, application/utils/harvester/repository_cache.py, application/utils/harvester/git_repository_client.py, application/tests/harvester_test/repository_cache_test.py, application/tests/harvester_test/git_repository_client_test.py
Adds the repository client contract, normalized cache paths, Git clone/fetch/checkout operations, integrity checks, synchronization, and lifecycle tests.
Change detection and checkpoint state
application/utils/harvester/models.py, application/utils/harvester/change_detector.py, application/utils/harvester/checkpoint_store.py, application/tests/harvester_test/change_detector_test.py, application/tests/harvester_test/checkpoint_store_test.py
Adds repository state dataclasses, Git-based modified-file and commit detection, JSON checkpoint save/load behavior, and corresponding unit tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • OWASP/OpenCRE#983: Overlapping harvester configuration, validation, repository client, cache, and test additions.

Suggested reviewers: northdpole, paoga87, robvanderveer, pa04rth

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: incremental change detection for the harvester.
Description check ✅ Passed The description matches the changeset and clearly describes change detection, checkpoint storage, models, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (5)
application/utils/harvester/__init__.py (1)

21-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Sort __all__ list.

The static analysis tool Ruff flags that the __all__ list is not sorted. Using a standard ASCII sort (uppercase before lowercase) resolves this warning and follows standard Python conventions.

♻️ Proposed fix
 __all__ = [
-    "build_repository_cache_path",
     "ChunkingConfig",
     "ConfigLoaderError",
     "GitRepositoryClient",
     "PathRules",
     "PollingConfig",
     "RepositoryClient",
     "RepositoryConfig",
     "RepositoryValidationError",
     "ReposFile",
+    "build_repository_cache_path",
     "load_repo_config",
     "validate_repositories",
 ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/utils/harvester/__init__.py` around lines 21 - 34, Sort the
__all__ entries in application/utils/harvester/__init__.py using standard ASCII
ordering, placing uppercase names before lowercase names, without changing the
exported symbols.

Source: Linters/SAST tools

application/tests/harvester_test/git_repository_client_test.py (2)

32-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mock the filesystem to prevent flaky tests.

This test asserts that the local repository path does not exist. However, if a developer runs this test locally after having previously run a harvest or sync, the .harvester_cache directory might actually exist on their machine, causing the test to fail.

Consider mocking Path.exists to fully isolate the test from the local filesystem state.

♻️ Proposed refactor
-    def test_repository_exists_locally_false(self):
+    `@patch`("pathlib.Path.exists")
+    def test_repository_exists_locally_false(self, mock_exists):
+        mock_exists.return_value = False
         client = GitRepositoryClient(
             owner="OWASP",
             repository="ASVS",
         )
 
         self.assertFalse(client.exists_locally())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/tests/harvester_test/git_repository_client_test.py` around lines
32 - 38, Update test_repository_exists_locally_false to mock pathlib.Path.exists
so it deterministically reports that the repository path is absent, then assert
GitRepositoryClient.exists_locally() returns False without consulting the
developer’s filesystem.

27-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use .as_posix() to prevent cross-platform test failures.

Comparing stringified Path objects to a UNIX-style string path (.harvester_cache/owasp/asvs/main) can cause test failures on Windows, where str(Path) uses backslashes (\). Using .as_posix() ensures the output uses forward slashes on all platforms.

♻️ Proposed refactor
-        self.assertEqual(
-            str(client.get_local_path()),
-            ".harvester_cache/owasp/asvs/main",
-        )
+        self.assertEqual(
+            client.get_local_path().as_posix(),
+            ".harvester_cache/owasp/asvs/main",
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/tests/harvester_test/git_repository_client_test.py` around lines
27 - 30, Update the path assertion for client.get_local_path() to call
.as_posix() before comparing with the expected UNIX-style path, preserving the
existing expected value and test behavior across platforms.
application/utils/harvester/change_detector.py (2)

47-60: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider returning commits in chronological order.

By default, git log outputs commits in reverse chronological order (newest first). If the orchestrator uses this list to process commits sequentially or incrementally replay history, it typically requires chronological order (oldest first).

You can pass the --reverse flag to git log to achieve this.

💡 Proposed change
             result = subprocess.run(
                 [
                     "git",
                     "-C",
                     str(self.repository_client.get_local_path()),
                     "log",
+                    "--reverse",
                     "--format=%H",
                     f"{commit_sha}..HEAD",
                 ],
                 capture_output=True,
                 text=True,
                 check=True,
                 timeout=60,
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/utils/harvester/change_detector.py` around lines 47 - 60, Update
the git log invocation in the change-detection method to include the --reverse
option, ensuring commits between commit_sha and HEAD are returned in
chronological order while preserving the existing output and filtering behavior.

20-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using -z for robust file path parsing.

Git may escape or quote filenames containing special characters or newlines by default (e.g., "path/to/file with spaces.md"). Passing -z to git diff produces null-terminated output, which guarantees accurate parsing of all file names without interference from Git's quotation rules.

♻️ Proposed refactor
             result = subprocess.run(
                 [
                     "git",
                     "-C",
                     str(self.repository_client.get_local_path()),
                     "diff",
+                    "-z",
                     "--name-only",
                     commit_sha,
                     "HEAD",
                 ],
                 capture_output=True,
                 text=True,
                 check=True,
                 timeout=60,
             )

If you apply this, update the parsing logic on line 40 to split by \0:

        files = [
            file_path for file_path in result.stdout.split("\0") if file_path.strip()
        ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/utils/harvester/change_detector.py` around lines 20 - 34, Update
the git diff invocation in the change-detection flow to include the
null-delimited output option, then update the parsing logic that consumes
result.stdout to split on "\0" and ignore empty entries. Preserve accurate
filenames, including spaces, quotes, and newlines, through the existing
file-processing path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@application/utils/harvester/checkpoint_store.py`:
- Around line 35-54: Update the checkpoint_store save method to serialize the
modified data into a temporary file in the checkpoint file’s directory, then
atomically replace the existing checkpoint file with it. Preserve the current
JSON content and encoding, and ensure temporary-file cleanup on write or
replacement failure.

---

Nitpick comments:
In `@application/tests/harvester_test/git_repository_client_test.py`:
- Around line 32-38: Update test_repository_exists_locally_false to mock
pathlib.Path.exists so it deterministically reports that the repository path is
absent, then assert GitRepositoryClient.exists_locally() returns False without
consulting the developer’s filesystem.
- Around line 27-30: Update the path assertion for client.get_local_path() to
call .as_posix() before comparing with the expected UNIX-style path, preserving
the existing expected value and test behavior across platforms.

In `@application/utils/harvester/__init__.py`:
- Around line 21-34: Sort the __all__ entries in
application/utils/harvester/__init__.py using standard ASCII ordering, placing
uppercase names before lowercase names, without changing the exported symbols.

In `@application/utils/harvester/change_detector.py`:
- Around line 47-60: Update the git log invocation in the change-detection
method to include the --reverse option, ensuring commits between commit_sha and
HEAD are returned in chronological order while preserving the existing output
and filtering behavior.
- Around line 20-34: Update the git diff invocation in the change-detection flow
to include the null-delimited output option, then update the parsing logic that
consumes result.stdout to split on "\0" and ignore empty entries. Preserve
accurate filenames, including spaces, quotes, and newlines, through the existing
file-processing path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 45a49c90-33e4-4f20-9cac-25cd04822648

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1aa47 and fee277f.

📒 Files selected for processing (31)
  • application/tests/harvester_test/__init__.py
  • application/tests/harvester_test/change_detector_test.py
  • application/tests/harvester_test/checkpoint_store_test.py
  • application/tests/harvester_test/config_loader_test.py
  • application/tests/harvester_test/fixtures/duplicate_include_paths.yaml
  • application/tests/harvester_test/fixtures/duplicate_repo_ids.yaml
  • application/tests/harvester_test/fixtures/duplicate_repositories.yaml
  • application/tests/harvester_test/fixtures/empty_include_paths.yaml
  • application/tests/harvester_test/fixtures/empty_owner.yaml
  • application/tests/harvester_test/fixtures/invalid_chunk_size.yaml
  • application/tests/harvester_test/fixtures/invalid_chunking_strategy.yaml
  • application/tests/harvester_test/fixtures/invalid_missing_id.yaml
  • application/tests/harvester_test/fixtures/invalid_polling_interval.yaml
  • application/tests/harvester_test/fixtures/invalid_polling_mode.yaml
  • application/tests/harvester_test/fixtures/invalid_yaml.yaml
  • application/tests/harvester_test/fixtures/valid_repos.yaml
  • application/tests/harvester_test/git_repository_client_test.py
  • application/tests/harvester_test/repos_validator_test.py
  • application/tests/harvester_test/repository_cache_test.py
  • application/utils/harvester/__init__.py
  • application/utils/harvester/change_detector.py
  • application/utils/harvester/checkpoint_store.py
  • application/utils/harvester/config_loader.py
  • application/utils/harvester/exclude_patterns.txt
  • application/utils/harvester/git_repository_client.py
  • application/utils/harvester/models.py
  • application/utils/harvester/repos.yaml
  • application/utils/harvester/repos_validator.py
  • application/utils/harvester/repository_cache.py
  • application/utils/harvester/repository_client.py
  • application/utils/harvester/schemas.py

Comment thread application/utils/harvester/checkpoint_store.py

@northdpole northdpole left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Week 3 review — required before approval

This layer establishes durable incremental state, so checkpoint persistence should use Postgres now rather than introducing a JSON runtime contract that must be replaced later. Full multi-worker concurrency can be deferred, provided this version explicitly enforces a single harvester worker with no overlapping runs.

Required in this PR:

  • replace JSON checkpoint persistence with the minimal Postgres model/migration described inline;
  • enforce the single-worker/no-overlap execution constraint;
  • replay commits oldest-first;
  • use validated immutable base/target SHAs instead of mutable HEAD;
  • add Postgres checkpoint tests and a deterministic temporary-Git integration test.

Please open separate follow-up tickets for multi-worker lease/CAS fencing, deletion/rename semantics, NUL-safe filenames, force-push/history-rewrite recovery, and broader failure/concurrency coverage. Every follow-up ticket should carry both the Module B and V1 labels so these are tracked as required next steps.

Verdict: changes are required before approval, but full concurrency support is not required in this PR.

from .models import RepositoryCheckpoint


class CheckpointStore:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocker: replace the JSON runtime store with Postgres in this PR. Add a harvester_checkpoint SQLAlchemy model and an Alembic migration based on the current migration head after rebasing. The minimum schema should contain: immutable repository_id as the primary key, canonical source identity (provider/owner/repository/exact branch, with a unique canonical source key), nullable last_processed_commit, and timezone-aware created_at/updated_at. Replace this Path-based API with database load(repository_id) and transactional upsert behavior using the application SQLAlchemy session. Do not fall back to JSON on a database error; fail closed and leave the checkpoint unchanged. Full lease/CAS columns may be added in the labelled V1 follow-up, but this PR must explicitly enforce single-worker/no-overlap execution until then.

"diff",
"--name-only",
commit_sha,
"HEAD",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do not diff against mutable HEAD. Change the API to receive both a validated base SHA from the checkpoint and an immutable target SHA captured after synchronization, then invoke git diff <base> <target>. Validate each value as a full commit object before use (for example with rev-parse --verify --end-of-options <sha>^{commit}) and pass only the resolved SHA to subsequent Git commands. Add a temporary local-repository test where the branch advances after target capture and prove the detected range remains stable. Force-push/history-rewrite reconciliation can be handled in a Module B + V1 follow-up ticket, but this API should not bake in HEAD.

str(self.repository_client.get_local_path()),
"log",
"--format=%H",
f"{commit_sha}..HEAD",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Git log is newest-first by default, which is unsafe for sequential replay and checkpoint progression. Add --reverse and use the explicit validated <base>..<target> range rather than <base>..HEAD, making the method contract oldest-first. Update the unit test to assert the complete Git argument list, and add a deterministic temporary-Git test with at least three commits that verifies the returned order.


try:
store = CheckpointStore(
tmp_dir / "checkpoints.json",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These filesystem/JSON tests should be replaced with Postgres-backed persistence tests using the repository's application/SQLAlchemy test setup. Cover: create/load, update/upsert, two repository rows remaining isolated, duplicate canonical source identity rejection, immutable repository identity, null initial checkpoint, transaction rollback leaving the previous checkpoint intact, and database failure with no JSON fallback. Also add a migration check that upgrades a clean Postgres database and verifies the table, primary key, unique identity constraint, and timestamps. Multi-worker contention tests belong in the follow-up ticket labelled Module B and V1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants