TimurGrenda/git-diff-rss

★ 0Forks 0TypeScriptGitHub ↗Compare

README

git-diff-rss

Turn the commit history of a hand-picked list of GitHub repos into Atom feeds where every entry contains the complete inline unified diff of the commit — the actual diff --git … @@ … +/- hunks, HTML-escaped in a <pre> block — not just the message and a link like GitHub's native commits.atom.

It runs once and exits. Point it at a config, schedule it (cron / systemd timer), and let FreshRSS poll the static .xml files (served by nginx). No server, no database, no git clone — pure HTTP against the GitHub REST API.

Requirements

  • Node.js 20+ (uses the built-in global fetch; zero runtime dependencies).
  • A GitHub token (optional for public repos but strongly recommended — it raises the rate limit from 60 to 5000 requests/hour and is required for private repos).

Install

npm install        # installs tsx (runner) + typescript (typecheck) as dev deps

Configure

Copy the example and edit it:

cp config.example.json config.json
{
  "outputDir": "./feeds",
  "cacheDir": "./cache",
  "maxCommits": 25,
  "maxDiffBytes": 200000,
  "repos": [
    { "owner": "owner1", "repo": "repo1", "branch": "main", "title": "Friendly name" },
    { "owner": "owner2", "repo": "private-repo" }
  ]
}
  • outputDir — where the .xml feeds are written (one file per repo, {owner}-{repo}.xml).
  • cacheDir — per-SHA diff cache; each commit's diff is downloaded exactly once, ever.
  • maxCommits — most recent N commits per repo (1 page, max 100).
  • maxDiffBytes — diffs larger than this are truncated with a "see commit" notice. The full diff is still cached, so raising this limit later does not re-download anything.
  • repos[].branch — optional; omit to use the repo's default branch.
  • repos[].title — optional friendly feed title; defaults to owner/repo.

The token comes from the GITHUB_TOKEN environment variable — never from the config file, never hardcoded. A fine-grained token with read-only "Contents" access is enough.

Run once

export GITHUB_TOKEN=ghp_xxx
npm start                 # uses ./config.json
# or point at another config:
npx tsx src/index.ts /etc/git-diff-rss/config.json

Type-check and self-test:

npm run typecheck
npm test

Node 23.6+ can run the TypeScript directly with no dev deps: node src/index.ts config.json.

Schedule with systemd

/etc/systemd/system/git-diff-rss.service (oneshot — runs and exits):

[Unit]
Description=Generate GitHub commit-diff Atom feeds
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
WorkingDirectory=/opt/git-diff-rss
Environment=GITHUB_TOKEN=ghp_xxx
ExecStart=/usr/bin/npx tsx src/index.ts /opt/git-diff-rss/config.json

/etc/systemd/system/git-diff-rss.timer (every 30 minutes):

[Unit]
Description=Run git-diff-rss every 30 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=30min
Persistent=true

[Install]
WantedBy=timers.target

Enable it:

sudo systemctl daemon-reload
sudo systemctl enable --now git-diff-rss.timer
systemctl list-timers git-diff-rss.timer      # check next run
journalctl -u git-diff-rss.service            # check logs

Keep the token out of the unit file by using EnvironmentFile=/etc/git-diff-rss.env (chmod 600) with a single GITHUB_TOKEN=ghp_xxx line instead of Environment=.

Serve the feeds with nginx

Point a location at your outputDir:

location /feeds/ {
    alias /opt/git-diff-rss/feeds/;
    types { application/atom+xml xml; }
    default_type application/atom+xml;
    autoindex on;          # optional: browse the list of feeds
}

Now https://your-host/feeds/owner1-repo1.xml serves the Atom file.

Add to FreshRSS

  1. In FreshRSS: Subscription management → Add → a feed.
  2. Paste the feed URL, e.g. https://your-host/feeds/owner1-repo1.xml.
  3. FreshRSS auto-detects Atom and starts polling. Each article body shows the commit subject/message followed by the full diff in a <pre> block.

To keep long diffs readable, FreshRSS's default article view already renders the <pre> monospaced; no extra configuration is needed.

How it works

  1. GET /repos/{owner}/{repo}/commits?sha={branch}&per_page={maxCommits} → commit metadata.
  2. For each commit, the diff comes from cacheDir/{sha}.diff if present, otherwise GET /repos/{owner}/{repo}/commits/{sha} with Accept: application/vnd.github.diff, then it's written to the cache.
  3. Each entry's diff is HTML-escaped, wrapped in <pre><code>, and the whole HTML body is escaped again for Atom content type="html".
  4. One .xml file is written per repo.

Resilience: a repo that errors (network / 404 / rate limit) is logged and skipped; the others still generate. When GitHub reports X-RateLimit-Remaining: 0, the run stops fetching new diffs and finishes from cache — uncached commits are picked up on the next run.

Contributors

TimurGrenda

Issues