# PR/MR Tracking Capabilities Analysis

**Research Date**: 2026-02-25
**Project**: gitflow-analytics
**Researcher**: Claude Code (Research Agent)
**Objective**: Understand current PR/MR tracking capabilities and identify where to implement enhanced PR tracking features (comments, approvals, change requests, revision counting, enhanced reporting).

---

## 1. Executive Summary

The gitflow-analytics codebase has a functional but **shallow** PR tracking implementation. The system fetches merged PR data from GitHub via PyGitHub, caches it in SQLite, maps PRs to commits, and produces basic PR summary metrics. However, it does **not** collect PR reviews, approval/change-request states, individual reviewer data, or revision counts. These are explicitly noted in the code as placeholders or missing schema fields. The architecture is well-structured and ready to accept these enhancements with targeted additions to the database schema, the GitHub API extractor, the cache layer, and the report writers.

---

## 2. Current PR Tracking Capabilities

### 2.1 What is Currently Collected

The `_extract_pr_data()` method in `src/gitflow_analytics/integrations/github_integration.py` fetches and returns the following fields for each merged PR:

| Field              | GitHub API Attribute        | Notes                                  |
|--------------------|-----------------------------|----------------------------------------|
| `number`           | `pr.number`                 | PR identifier                          |
| `title`            | `pr.title`                  | PR title                               |
| `description`      | `pr.body`                   | Full PR body text                      |
| `author`           | `pr.user.login`             | GitHub username of the PR author       |
| `created_at`       | `pr.created_at`             | PR creation timestamp                  |
| `merged_at`        | `pr.merged_at`              | PR merge timestamp                     |
| `story_points`     | extracted from title+body   | Via `StoryPointExtractor`              |
| `labels`           | `pr.labels`                 | List of label names                    |
| `commit_hashes`    | `pr.get_commits()`          | All SHAs included in the PR            |
| `ticket_references`| extracted from title+body   | Via `TicketExtractor`                  |
| `review_comments`  | `pr.review_comments`        | Count of inline code review comments   |
| `changed_files`    | `pr.changed_files`          | Number of files changed                |
| `additions`        | `pr.additions`              | Lines added                            |
| `deletions`        | `pr.deletions`              | Lines deleted                          |

**Not collected**: reviewer identities, approval/change-request states, PR-level (non-inline) comments, revision/push count, requested reviewers, time-to-first-review.

### 2.2 What is Currently Calculated (Metrics)

`calculate_pr_metrics()` in `github_integration.py` computes:

- `total_prs`
- `avg_pr_size` (additions + deletions / PR count)
- `avg_pr_lifetime_hours` (created_at to merged_at)
- `avg_files_per_pr`
- `total_review_comments` (summed `pr.review_comments`)
- `prs_with_story_points`
- `story_point_coverage` (percentage)

### 2.3 What is Reported

The Markdown narrative report (`src/gitflow_analytics/reports/narrative_writer.py`) includes a "Pull Request Analysis" section (`_write_pr_analysis()`) that renders:

- Total PRs Merged
- Average PR Size
- Average PR Lifetime (if available)
- Story Point Coverage (if available)
- Total Review Comments + Average per PR (if non-zero)

The JSON exporter (`json_exporter.py`) includes PR counts in executive summary, per-project aggregates, and weekly time series. The CSV writer and HTML generator do **not** include dedicated PR sections.

---

## 3. Database Schema for PR Data

### 3.1 `pull_request_cache` Table

File: `src/gitflow_analytics/models/database.py`, class `PullRequestCache`

```
Table: pull_request_cache
+------------------+-----------------+----------+----------------------------------+
| Column           | Type            | Nullable | Notes                            |
+------------------+-----------------+----------+----------------------------------+
| id               | Integer PK      | No       | Auto-increment                   |
| repo_path        | String          | No       | GitHub "owner/repo"              |
| pr_number        | Integer         | No       | PR identifier                    |
| title            | String          | Yes      |                                  |
| description      | String          | Yes      | Full PR body                     |
| author           | String          | Yes      | GitHub username                  |
| created_at       | DateTime TZ     | Yes      |                                  |
| merged_at        | DateTime TZ     | Yes      |                                  |
| story_points     | Integer         | Yes      |                                  |
| labels           | JSON            | Yes      | List of label strings            |
| commit_hashes    | JSON            | Yes      | List of commit SHAs              |
| cached_at        | DateTime TZ     | No       | Cache insertion time             |
+------------------+-----------------+----------+----------------------------------+
Unique Index: (repo_path, pr_number)
```

**Absent columns** (noted explicitly in the codebase as "Not stored in current schema"):
- `review_comments` count
- `changed_files` count
- `additions`, `deletions` counts
- Reviewer lists
- Approval / change-request states
- Revision (push) count
- Time-to-first-review

### 3.2 `RepositoryAnalysisStatus` Table (PR tracking field)

The `repository_analysis_status` table has:
- `pr_analysis_complete` (Boolean)
- `pr_count` (Integer)

This is used to track whether a full PR fetch has been done for a given repo+time-window.

### 3.3 `PullRequestData` Dataclass (Reports Layer)

File: `src/gitflow_analytics/reports/data_models.py`

The reports-layer dataclass already has **richer fields** than the cache table:

```python
@dataclass
class PullRequestData:
    id: Union[int, str]
    title: str
    author: str
    created_at: datetime
    merged_at: Optional[datetime]
    state: str                          # open, closed, merged
    is_merged: bool
    commits_count: int
    additions: int
    deletions: int
    files_changed: int
    comments_count: int                 # PR-level comments
    review_comments_count: int          # Inline code review comments
    reviewers: List[str]               # Reviewer usernames
    approved_by: List[str]             # Who approved
    time_to_merge_hours: Optional[float]
    time_to_first_review_hours: Optional[float]
    labels: List[str]
    metadata: Dict[str, Any]
```

This dataclass is **defined but not populated** from real data — no code path currently maps GitHub API data into `PullRequestData` objects. It is forward-looking scaffolding.

---

## 4. GitHub API Integration Points

### 4.1 Primary Integration Class

File: `src/gitflow_analytics/integrations/github_integration.py`
Class: `GitHubIntegration`
Library: `PyGitHub` (`github` package)

Key methods and their roles:

| Method | Role |
|--------|------|
| `enrich_repository_with_prs()` | Top-level: fetches PRs, caches, maps to commits |
| `_get_pull_requests()` | Fetches merged PRs from GitHub API with rate-limit retry |
| `_extract_pr_data()` | Converts a PyGitHub `PullRequest` object to a dict |
| `_get_cached_prs_bulk()` | Bulk retrieval from SQLite cache |
| `_cache_prs_bulk()` | Writes new PR data to cache |
| `calculate_pr_metrics()` | Aggregate metrics from list of PR dicts |

### 4.2 Orchestration Layer

File: `src/gitflow_analytics/integrations/orchestrator.py`
Class: `IntegrationOrchestrator`

The `enrich_repository_data()` method coordinates the full enrichment pipeline. It calls `github_integration.enrich_repository_with_prs()` and stores the result as `enrichment["prs"]` alongside `enrichment["pr_metrics"]`.

### 4.3 Available PyGitHub API Capabilities (Not Yet Used)

The PyGitHub library supports fetching much richer PR data. The following are available on `PullRequest` objects but **not currently called**:

```python
# Review data
pr.get_reviews()              # Returns list of PullRequestReview objects
  # .user.login               # Reviewer username
  # .state                    # "APPROVED", "CHANGES_REQUESTED", "COMMENTED", "DISMISSED"
  # .submitted_at             # When the review was submitted
  # .body                     # Review body text

# Review comments (inline code comments)
pr.get_review_comments()      # Returns list of PullRequestComment objects
  # .user.login
  # .created_at
  # .body

# PR-level (issue) comments
pr.get_issue_comments()       # Returns list of IssueComment objects
  # .user.login
  # .created_at

# Requested reviewers
pr.requested_reviewers        # List of users requested as reviewers
pr.get_review_requests()      # Same as above but with teams

# PR revision/push count
pr.commits                    # Count of commits (property, no API call)
# To get revision count (number of times code was pushed), need:
pr.get_commits()              # Returns PaginatedList; len() gives commit count
# Revision count approximation: number of "Pushed to" events
pr.get_issue_events()         # Get events including force-push, reopened, etc.
```

---

## 5. Data Flow from API to Reports

The current PR data flow:

```
GitHub API (PyGitHub)
        |
        v
GitHubIntegration._extract_pr_data()
    - Collects: number, title, author, timestamps, labels,
      commit_hashes, story_points, ticket_refs, review_comments,
      changed_files, additions, deletions
        |
        v
GitAnalysisCache.cache_pr()
    - Stores: subset only (no review_comments, changed_files,
      additions, deletions, reviewers, approvals)
        |
        v
IntegrationOrchestrator.enrich_repository_data()
    - enrichment["prs"] = list of PR dicts
    - enrichment["pr_metrics"] = calculate_pr_metrics() result
        |
        v
CLI (cli.py ~line 3872)
    - Aggregates pr_metrics across all repo enrichments
    - Passes pr_metrics to report generators
        |
        v
NarrativeWriter._write_pr_analysis()     [Markdown report]
ComprehensiveJSONExporter (partial)      [JSON export]
```

---

## 6. Identified Gaps

### Gap 1: Missing Data in Cache Schema (HIGH PRIORITY)
`PullRequestCache` does not store `review_comments`, `changed_files`, `additions`, `deletions`. When a PR is served from cache, these values return 0. This means:
- PR size metrics are wrong for cached runs
- Review comment counts are wrong for cached runs

File: `src/gitflow_analytics/models/database.py` lines 133-159
Code comment: `"review_comments": 0,  # Not stored in current schema` (line 197, github_integration.py)

### Gap 2: No Review/Approval Fetching (HIGH PRIORITY)
The `_extract_pr_data()` method does not call `pr.get_reviews()`. There is no tracking of:
- Who reviewed (reviewer usernames)
- Review outcomes (APPROVED, CHANGES_REQUESTED, DISMISSED)
- Number of review rounds
- Time to first review

### Gap 3: No PR Revision Counting (MEDIUM PRIORITY)
There is no tracking of how many times code was pushed/revised after a PR was opened. The `pr.commits` count exists on the PyGitHub object but is not captured. Approximating revision counts would require either commit count delta or inspection of PR events.

### Gap 4: No PR-Level Comments (MEDIUM PRIORITY)
`pr.review_comments` only captures inline code comments. PR-level discussion comments (`pr.get_issue_comments()`) are not fetched or stored.

### Gap 5: Placeholder in Quality Compliance Analysis (MEDIUM PRIORITY)
`src/gitflow_analytics/qualitative/enhanced_analyzer.py` lines 1915-1916:
```python
# PR approval compliance (if PR data available - placeholder)
pr_approval_rate = 75  # Default assumption when PR data not available
```
This is a hardcoded placeholder. Once real approval data is collected, this should be derived from actual data.

### Gap 6: PullRequestData Dataclass Not Used (LOW PRIORITY)
The `PullRequestData` dataclass in `reports/data_models.py` (lines 122-171) already defines the richer schema including `reviewers`, `approved_by`, `time_to_first_review_hours`, but no code path currently populates this dataclass from real GitHub data.

### Gap 7: PR Metrics Not in CSV or HTML Reports (LOW PRIORITY)
The CSV writer (`csv_writer.py`) and HTML generator (`html_generator.py`) have no PR-specific sections. PR data is only reported in Markdown narrative and JSON export.

---

## 7. Recommended Implementation Approach

### Phase 1: Fix Cache Schema (Required Foundation)

**File**: `src/gitflow_analytics/models/database.py`

Add missing columns to `PullRequestCache`:
```python
# Size metrics (were fetched but not stored)
review_comments = Column(Integer, default=0)
changed_files = Column(Integer, default=0)
additions = Column(Integer, default=0)
deletions = Column(Integer, default=0)

# New: Review tracking
reviewers = Column(JSON)           # List of reviewer usernames
approvals = Column(JSON)           # List of {user, state, submitted_at}
change_requests_count = Column(Integer, default=0)
approvals_count = Column(Integer, default=0)
time_to_first_review_hours = Column(Float, nullable=True)

# New: Revision tracking
revision_count = Column(Integer, default=0)  # Number of pushes after PR opened
pr_comments_count = Column(Integer, default=0)  # PR-level discussion comments
```

Also add a schema migration via the existing `schema_version.py` pattern.

### Phase 2: Extend GitHub API Extraction

**File**: `src/gitflow_analytics/integrations/github_integration.py`

Extend `_extract_pr_data()` to call additional PyGitHub methods:

```python
# Get reviews
reviews = list(pr.get_reviews())
reviewers = list({r.user.login for r in reviews if r.user})
approvals = [r.user.login for r in reviews if r.state == "APPROVED"]
change_requests = [r for r in reviews if r.state == "CHANGES_REQUESTED"]

# Time to first review
review_times = [r.submitted_at for r in reviews if r.submitted_at]
time_to_first_review = (
    (min(review_times) - pr.created_at).total_seconds() / 3600
    if review_times else None
)

# PR-level comments
pr_comments = pr.get_issue_comments()
pr_comments_count = pr_comments.totalCount

# Revision count approximation
revision_count = sum(1 for e in pr.get_issue_events() if e.event == "committed")
```

**Important**: Each `pr.get_reviews()` and `pr.get_issue_comments()` makes an additional GitHub API call. This will increase API consumption. Consider:
- Only fetching reviews/comments when `fetch_reviews: true` config flag is set
- Caching aggressively (reviews rarely change after merge)
- Using the existing `ttl_hours` and rate-limit retry logic

### Phase 3: Update Cache Layer

**File**: `src/gitflow_analytics/core/cache.py`

Update `cache_pr()` to store the new fields and update `_pr_to_dict()` to return them. Update `_get_cached_prs_bulk()` to populate the new fields from the cache instead of returning 0.

### Phase 4: Enhance PR Metrics Calculation

**File**: `src/gitflow_analytics/integrations/github_integration.py`

Extend `calculate_pr_metrics()`:
```python
# Add to return dict
"avg_approvals_per_pr": total_approvals / len(prs),
"avg_change_requests_per_pr": total_change_requests / len(prs),
"prs_requiring_changes": count of prs with >0 change_requests,
"avg_revision_count": total_revisions / len(prs),
"avg_time_to_first_review_hours": ...,
"prs_with_no_review": count of prs with 0 reviewers,
"review_coverage_pct": prs with at least one review / total prs * 100,
```

### Phase 5: Update Report Writers

**File**: `src/gitflow_analytics/reports/narrative_writer.py`

Extend `_write_pr_analysis()` to include:
- Review coverage percentage
- Average approvals per PR
- PRs requiring change requests
- Average revision count
- Average time-to-first-review

**File**: `src/gitflow_analytics/qualitative/enhanced_analyzer.py`

Replace the `pr_approval_rate = 75` placeholder with real data derived from `pr_metrics.get("review_coverage_pct", 75)`.

**Files**: `src/gitflow_analytics/reports/csv_writer.py` and `html_generator.py`

Add PR metrics sections to these report formats.

### Phase 6: Add Configuration Options

**File**: `src/gitflow_analytics/config/schema.py`

Extend `GitHubConfig` with:
```python
fetch_pr_reviews: bool = False    # Off by default (extra API calls)
fetch_pr_comments: bool = False   # Off by default
fetch_pr_revisions: bool = False  # Off by default
```

This allows users to opt into richer data collection without impacting existing deployments.

---

## 8. File Map for Implementation

| File | Change Needed |
|------|---------------|
| `src/gitflow_analytics/models/database.py` | Add columns to `PullRequestCache` |
| `src/gitflow_analytics/core/cache.py` | Update `cache_pr()`, `_pr_to_dict()`, `_get_cached_prs_bulk()` |
| `src/gitflow_analytics/integrations/github_integration.py` | Extend `_extract_pr_data()`, `calculate_pr_metrics()` |
| `src/gitflow_analytics/config/schema.py` | Add fetch flags to `GitHubConfig` |
| `src/gitflow_analytics/core/schema_version.py` | Update schema version for incremental fetch tracking |
| `src/gitflow_analytics/reports/narrative_writer.py` | Extend `_write_pr_analysis()` |
| `src/gitflow_analytics/reports/data_models.py` | Populate `PullRequestData` from real data |
| `src/gitflow_analytics/qualitative/enhanced_analyzer.py` | Replace hardcoded `pr_approval_rate` |
| `src/gitflow_analytics/reports/csv_writer.py` | Add PR metrics section (optional) |
| `src/gitflow_analytics/reports/html_generator.py` | Add PR metrics section (optional) |

---

## 9. Technical Constraints and Risks

1. **GitHub API Rate Limits**: Fetching reviews and comments adds N additional API calls per PR (where N = number of review/comment fetches). With PyGitHub lazy loading, calling `pr.get_reviews()` is one paginated list call. For repos with hundreds of PRs, this can consume the 5,000 requests/hour rate limit quickly. Mitigation: config flags (off by default), cache aggressively.

2. **Schema Migration**: Adding columns to `pull_request_cache` requires a migration. The `schema_version.py` module provides `SchemaVersionManager` and `create_schema_manager()` infrastructure that already handles schema change detection and triggers full re-fetches. However, SQLite `ALTER TABLE ADD COLUMN` must be executed carefully — the `Database` class needs to handle this gracefully, possibly using the existing `OperationalError` handling pattern.

3. **Cache Staleness for Historical PRs**: Merged PRs that are already cached will return 0 for new review/approval fields until the cache is invalidated. This is acceptable behavior — the schema version change will trigger a re-fetch.

4. **PyGitHub Object Consistency**: `pr.get_reviews()` is not available on PR objects loaded from the "list pulls" endpoint with shallow pagination. The existing code uses `repo.get_pulls(state="all")` which returns lazy-loaded objects. Accessing `.get_reviews()` will trigger individual PR API calls, which is the expected cost.

5. **Revision Count Ambiguity**: There is no direct GitHub API field for "number of times code was revised after PR opened." Approximations available are: (a) commits count on the PR branch, (b) number of `synchronize` events in the PR timeline. Events are available via `pr.get_issue_events()` filtered to `event == "synchronize"`.

---

## 10. Key Code Locations Summary

```
src/gitflow_analytics/
├── models/
│   └── database.py                      # PullRequestCache (line 133), DailyMetrics (line 769)
├── core/
│   └── cache.py                         # cache_pr() (~line 307), get_cached_pr() (~line 288)
│                                        # _pr_to_dict() (~line 906), _get_cached_prs_bulk() via github_integration
├── integrations/
│   ├── github_integration.py            # GitHubIntegration (line 14)
│   │                                    # _extract_pr_data() (line 272) -- main extraction
│   │                                    # calculate_pr_metrics() (line 307)
│   │                                    # _get_cached_prs_bulk() (line 154) -- cache read
│   │                                    # _cache_prs_bulk() (line 206) -- cache write
│   └── orchestrator.py                  # enrich_repository_data() (line 170)
├── reports/
│   ├── data_models.py                   # PullRequestData dataclass (line 122) -- forward-looking
│   └── narrative_writer.py              # _write_pr_analysis() (line 1979)
├── qualitative/
│   └── enhanced_analyzer.py             # Hardcoded pr_approval_rate placeholder (line 1916)
└── config/
    └── schema.py                        # GitHubConfig (line 28) -- add fetch flags here
```

---

*Research saved to: `docs/research/pr-tracking-capabilities-analysis-2026-02-25.md`*
*No ticket context provided — file-based capture only.*
