> ## Content Index
> Fetch the complete content index at: https://blogs.gardazi.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Tell If a Project Will Merge Your PR
- URL: https://blogs.gardazi.com/how-to-tell-if-a-project-will-merge-your-pr/
- Published: 2026-08-27T13:22:16.000Z
- Updated: 2026-08-27T13:22:16.000Z
- Description: Four numbers predict whether a maintainer will read your diff, and the star count is not one of them. Includes the forty-line screener and the blind spot it has.
- Author: Syed Talha Ahmed Gardazi
- Tags: Open Source, GitHub, Career

I picked a repo to contribute to because it had 2,700 stars and a clean open issue. Two days later the PR had no review, no comment, nothing — and when I actually looked at the project's history, that was entirely predictable.

The repo hadn't been pushed to in 16 days. Its oldest open pull request was **315 days old**. Not a single outside contributor had been merged in the previous month.

None of that is visible from the star count. All of it is one API call away.

## Four numbers that predict a merge

Stars measure how many people liked the README. They say nothing about whether a maintainer will read your diff. These four do:

| Signal                        | Reject when | Why                                   |
| ----------------------------- | ----------- | ------------------------------------- |
| Days since last push          | \> 21       | Nobody is home.                       |
| Age of oldest open PR         | \> 180 days | A graveyard. Yours joins it.          |
| Outside PRs merged in 30 days | 0           | They merge their own work, not yours. |
| Median outsider time-to-merge | \> 14 days  | Slow even when it works.              |

The third is the one that matters most, and it's the one people skip. A repo can be pushed to daily and still never merge a stranger — that's just a team working in public. What you need to know is whether *outsiders* get merged, which is a different question from whether the project is alive.

## Telling an outsider from a maintainer

This is the only fiddly part. GitHub's REST API exposes `author_association` on each pull request — `MEMBER` and `OWNER` are the team, `CONTRIBUTOR` and `FIRST_TIME_CONTRIBUTOR` are everyone else.

`gh pr list --json` does **not** expose that field. You have to go through the REST endpoint:

```
gh api "repos/OWNER/REPO/pulls?state=closed&per_page=100"
```

Without it your numbers are meaningless. One project I screened showed a 0.5-day median time-to-merge, which looked excellent until I filtered out the core committers merging their own work — the median for actual outsiders was a different number entirely, drawn from a different population.

## What it says about real repos

Run against the repo I picked badly, and one I picked well:

```
[AVOID] GodsScion/Auto_job_applier_linkedIn  2737* Python
        pushed 16d ago | open human PRs 14 | oldest 315d
        outsider merges: 14 (0 in 30d) | median TTM 14.7d
        -> PR graveyard (oldest 315d), no outsider merged in 30d, slow TTM

[PASS]  apache/datafusion-ballista  2115* Rust
        pushed 0d ago | open human PRs 44 | oldest 122d
        outsider merges: 37 (37 in 30d) | median TTM 0.8d
```

Fewer stars, better project to contribute to. Every outside PR merged in Ballista's last hundred closed PRs landed within the past month, at a median of under a day.

Screening 68 candidate repos this way, **11 passed**. Most were rejected for being dormant, which is not something you can see by looking at them.

## The failure mode I walked into anyway

One of the 11 was a repo I'd already rejected by hand, days earlier, as a contribution farm — the kind that auto-generates issues like "Add new Trivia Question 19" so people can pad a contribution graph.

It passed with the best numbers in the whole set: 72 outside contributors merged in 30 days, median time-to-merge 0.0 days.

Of course it did. **Instant merging is the entire product.** A farm is, by construction, maximally responsive to outsiders. My screen was measuring exactly the thing farms optimise for and calling it a virtue.

Another one had 163 stars and 85 outside merges in a month, which is a rate no small project sustains on real work.

So responsiveness is a filter you apply *after* deciding the work is real, never instead of it. Both questions have to be asked, and they don't collapse into one number:

- **Is the work real?** — human judgement. Read the issues. Do they describe defects with reproductions, or tasks invented to be completed?
- **Will they merge it?** — the four numbers above.

Skip the first and you get a fast merge that's worth nothing. Skip the second and you get good work that sits for a year.

## The script

```
import json, subprocess
from datetime import datetime, timezone

now = datetime.now(timezone.utc)
def gh(*a): return json.loads(subprocess.run(["gh", *a], capture_output=True, text=True).stdout)
def age(ts): return (now - datetime.fromisoformat(ts.replace("Z", "+00:00"))).days

def screen(repo):
    r    = gh("api", f"repos/{repo}")
    # REST, not `gh pr list` - only this exposes author_association
    raw  = gh("api", f"repos/{repo}/pulls?state=closed&per_page=100")
    opn  = gh("pr", "list", "--repo", repo, "--state", "open",
              "--limit", "100", "--json", "number,createdAt,author")

    OUTSIDER = ("CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR", "FIRST_TIMER", "NONE")
    outside = [p for p in raw if p.get("merged_at")
               and p.get("author_association") in OUTSIDER
               and "[bot]" not in p["user"]["login"]]

    ttm = sorted((datetime.fromisoformat(p["merged_at"].replace("Z","+00:00"))
                - datetime.fromisoformat(p["created_at"].replace("Z","+00:00"))).days
                for p in outside)
    recent = [p for p in outside if age(p["merged_at"]) <= 30]
    oldest = max((age(o["createdAt"]) for o in opn), default=0)

    flags = []
    if age(r["pushed_at"]) > 21:                     flags.append("stale")
    if oldest > 180:                                 flags.append("PR graveyard")
    if not recent:                                   flags.append("no outsider merged in 30d")
    if ttm and ttm[len(ttm)//2] > 14:                flags.append("slow outsider TTM")

    print(repo, "->", ", ".join(flags) if flags else "PASS")
```

Roughly forty lines, three API calls per repo. It would have saved me two PRs sitting in a repo that hasn't merged a stranger since spring.

## The short version

Before you spend an evening on a fix, spend ten seconds on this: **when did an outsider last get merged here?**

If the answer is "not this month," your PR is not the exception.