GitHub file history
skill · published by agentleFS · version 1 · approved by frdchang@gmail.com on 2026-10-12
Collect every version of any file across public GitHub repos, from a list, a search or a GH Archive census.
MountGitHub file history
Pulls the full git history of chosen files from many public GitHub repos, one TSV per file.
Each file becomes one TSV (gzipped by default) with a row per commit that touched it, plus a
manifest. The repo list can come from a plain file, a GitHub search export, a random draw from a
census list, or the agentleFS public catalog. It needs no GitHub token: it uses public
git clone and raw.githubusercontent.com only, and neither counts against the REST API rate
limit.
Example output (the smoke test below, 2026-10-11; base64 bodies cut short):
$ bash $SKILL/scripts/collect.sh $SKILL/examples/smoke.txt /tmp/gfh-smoke 4
listed 3 files collected 4 errors 1
$ zcat /tmp/gfh-smoke/vers/jshttp_mime-types__README.md.tsv.gz | head -2
80cdcb346c66fb39ab17be88a0a1031ffaee678f 2026-01-08T15:03:44-06:00 0 IyBtaW1lLXR5cGVzCgpbIVtOUE0g...
3aabd6896010e1d2c0c75d5cea36f90cd403b623 2025-11-25T11:12:26+01:00 0 IyBtaW1lLXR5cGVzCgpbIVtOUE0g...Use it when:
- you want to see how a kind of file changes over time across projects: READMEs, package.json, Dockerfiles, CI workflows, agent instruction files, editor rules
- you need input for
text-rule-trends
Requirements
bash,git2.22+ (partial clone and%(trailers)),gzip,base64,awk,timeout- Python 3.8+, standard library only (for
make_list.py) - Network access to
github.com, and toraw.githubusercontent.comfor--probe. Thecatalogsource also readsagentlefs.com.
Inputs
A list file with one wanted file per line, as owner/repo/PATTERN. A pattern is a path or a glob:
| pattern | matches |
README.md | that path only |
*.md | any .md file at the root |
.cursor/rules/*.mdc | .mdc files in that folder (not below it) |
docs/**/*.md | .md files anywhere under docs/ |
[Rr][Ee][Aa][Dd][Mm][Ee]* | README, readme.md, README.rst, ... |
A glob is matched against every path that ever existed on the default branch. So it also finds files that were later deleted or renamed. Each match becomes its own file history.
You can write the list by hand, or build it with make_list.py from one of four sources.
Steps
Run the scripts by their path and write outputs to a folder of your own, never inside the skill
folder. Below, SKILL is this skill's folder.
1. Make the list
# a plain list of repos (owner/repo or GitHub URLs, one per line)
python3 $SKILL/scripts/make_list.py file repos.txt --path README.md --path package.json > list.txt
# a GitHub search export: gh search repos ... --json fullName, a saved /search/repositories
# response, or a CSV with full_name / repo_name / nameWithOwner
python3 $SKILL/scripts/make_list.py search repos.json --path '.cursor/rules/*.mdc' > list.txt
# a random draw from any census list, keeping only files that exist today
python3 $SKILL/scripts/make_list.py census census.csv.gz --n 1000 --seed 1 \
--path Dockerfile --probe --sampled drawn.txt > list.txt
# the agentleFS public catalog: its own agent files, or its repos crossed with --path
python3 $SKILL/scripts/make_list.py catalog --names CLAUDE.md AGENTS.md > list.txt
python3 $SKILL/scripts/make_list.py catalog --path package.json --limit 50 > list.txt--probesends a HEAD request tohttps://raw.githubusercontent.com/<repo>/HEAD/<path>and keeps the pairs that answer 200. It works for literal paths only. Globs pass through. Non-404 errors are retried 4 times, then printed asERR <code> <repo> <path>.--jobs(default 16) sets how many probes run at once.censusreads a CSV with arepo_namecolumn (gzipped or not; the header may come after junk lines) or oneowner/repoper line. Lines that are not a validowner/repoare dropped and counted on stderr. It draws withrandom.Random(seed).sample.--sampledkeeps the drawn repos, so you have the denominator on record.- No census ships with this skill. Make one from GH Archive's free hourly event files:
That reads one hour a day for 30 days and keeps every repo with a push. One hour is about 18 MB and gave 39,397 repos (2026-10-01 12:00 UTC, tested 2026-10-11). Any list of public repos works instead, in either format above. Your list decides what "random" means, so write down how you made it.python3 $SKILL/scripts/gharchive_census.py --start 2026-10-01T00 --hours 720 --every 24 > census.txt catalogreadshttps://agentlefs.com/context/sitemap.xml. It lists agent instruction files in public repos with 1,000+ stars. Without--path, it lists the catalog's files (root only unless--any-depth, filtered by--names). With--path, it is a source of popular repos.
2. Collect histories
bash $SKILL/scripts/collect.sh list.txt data/mysample 8collect.sh groups the list by repo, so each repo is cloned once. For each repo, history.sh
makes a bare, blob-less clone (git clone --bare --filter=blob:none). It expands any globs
against the history, lists the commits that touched each file, and fetches only those blobs, in
batches of 500 per git fetch rather than one round trip each. If a blob still cannot be read,
that file is not written and ERR blob is logged, so a failed fetch never looks like a deletion,
and a re-run tries the file again.
It is safe to re-run. Finished files are skipped, and literal paths already on disk skip the clone.
One repo on its own: bash $SKILL/scripts/history.sh owner/repo OUTDIR README.md 'docs/*.md'.
Settings, as environment variables:
CLONE_TIMEOUT(seconds, default 300) limits each clone. Huge repos can need more.HISTORY_FORMAT=tsvwrites plain.tsvfiles instead of.tsv.gz, for stores that hold text only.
Outputs
data/<sample>/
manifest.tsv key <TAB> owner/repo <TAB> path (one line per file collected)
err.log always written; "ERR clone <repo>", "ERR nohistory <repo> <pattern>", or
"ERR blob <repo> <path> <sha>" when a version could not be fetched
vers/<key>.tsv.gz one per file; key = <owner>_<repo>__<path with / as _>Each vers row is one commit that touched the file, in git log order, with four tab-separated
fields. That order is newest first by commit graph, not strictly by author date: author dates can
go backwards (rebases, cherry-picks, wrong clocks, mixed UTC offsets). Sort by the parsed date
if you need date order; text-rule-trends does.
| field | meaning |
| sha | commit id |
| author_date | ISO 8601 author date with offset, e.g. 2026-03-31T14:14:16-07:00 |
| claude_coauthor | 1 if a Co-authored-by trailer mentions "claude", else 0 |
| body | the file's content after that commit, base64 (no newlines). Empty if the commit deleted it |
text-rule-trends reads this format, .tsv.gz or .tsv.
Examples
README and package.json in five repos (examples/readme-package/)
repos.txt names five public repos. list.txt was made from it:
python3 $SKILL/scripts/make_list.py file $SKILL/examples/readme-package/repos.txt \
--path package.json --path '[Rr][Ee][Aa][Dd][Mm][Ee]*' > list.txt
bash $SKILL/scripts/collect.sh list.txt /tmp/readme-package 5
# listed 10 files collected 12 errors 1On 2026-10-11 this took 3.4 seconds. The README glob found 7 files in 5 repos, including
renames: expressjs/cors had README.markdown and then README.md, and pallets/itsdangerous
had README, then README.rst, then README.md. The one error is pallets/itsdangerous, a
Python project with no package.json. Row counts grow as the repos get new commits.
Smoke test (examples/smoke.txt)
Two README histories, one glob, and one repo that does not exist. A few seconds:
bash $SKILL/scripts/collect.sh $SKILL/examples/smoke.txt /tmp/gfh-smoke 4
# listed 3 files collected 4 errors 1The glob jshttp/mime-types/*.md found HISTORY.md, README.md and SOURCES.md (2026-10-11).
The error is ERR clone nobody-here/not-a-repo-xyz. Every vers row should have 4 fields, and
each body should decode with base64 -d.
Worked example. These skills were first written for the agentleFS article "CLAUDE.md best
practices" (https://agentlefs.com/blog/claude-md-best-practices), which studied 5,786 CLAUDE.md and
AGENTS.md files. Its data, settings and step-by-step recipe live outside the skills, in the
article-claude-md-best-practices folder (RECIPE.md and data/).
Limits and caveats
- Your repo list decides what your sample means. A random draw from a census is only as random as the census. The catalog covers popular repos (1,000+ stars, licences that allow redistribution), which are not typical repos. Write down where the list came from.
- The catalog sitemap has a cap of 20,000 URLs, most stars first. It was at the cap on 2026-10-11. The script prints a note when the cap is reached. The catalog changes every week.
--probeonly sees files that exist today on the default branch. Deleted or renamed files are missed. Globs are not probed, so they find deleted files too, but they cost a clone per repo.- History follows one path, with git's default history simplification. A file that only ever lived on a side branch that was merged has no history
that way; for those (and only those)
history.shretries with--full-history, so they are collected instead of logged as nohistory. Histories that the default finds are never changed. A rename looks like a deletion plus a new file. A glob that matches both names gives you both files. Dates are author dates, which people can set to anything. - Keys can collide in rare cases, because
/and_both become_(a/b_canda_b/c). Paths with spaces are not supported. - Rate limits. Clones and raw HEAD requests need no token, but GitHub throttles heavy use.
Keep
--jobsand the collect parallelism modest (8 to 24).api.github.comis not used. If you add API calls of your own, read the token fromGITHUB_TOKENand never print it. - Speed. Blobs are fetched in batches, so the README example (12 files, about 1,200 versions) took 3.4 seconds on 2026-10-11. Blob-less clones still download every commit and tree, so huge monorepos can take minutes each.
- Licences. Collecting public files to study them is one thing. Redistributing their text is another. Only republish file contents (including these TSVs) for repos whose licence allows it.
Credits
- GH Archive (https://www.gharchive.org), by Ilya Grigorik: its free hourly files of public
GitHub events are what
gharchive_census.pyreads to build a census of active repos. Nothing from it ships with this skill. Its front page states no licence for the data (checked 2026-10-11); the events are GitHub's public timeline, under GitHub's terms. - git partial clone (
git clone --filter=blob:none, git 2.22+): blob-less clones, with only the needed blobs fetched in batches, are what make many repos cheap. git (GPL-2.0) is called as an installed program, not bundled. - File contents collected belong to their repos' authors, under each repo's licence (see Limits).

