• Toggle Theme
  • Search Site
  • View as Mobile

Most automation starts with one useful question:

What changed?

Not "a commit exists, run the whole pipeline." What changed?

Which packages changed? Was it runtime code or docs? Did a config file move? Did package.json change? Did markdown update without touching anything that ships?

That question is why I built git-er-done, a small JavaScript utility for querying git history from scripts.

My first pass was the usual tiny helper:

const execa = require('execa')

async function getChangedFiles() {
  const result = await execa.command('git diff --name-only')
  return result.stdout.split('\n').filter(Boolean)
}

That works until the next question shows up.

Was the file created, modified, deleted, or renamed? Which branch are we comparing against? What about uncommitted working tree changes?

Then the questions keep stacking up. I need to match packages/*/src/** but ignore tests. I need commit metadata, line counts, file timestamps, or GitHub Actions outputs.

At that point you either keep stacking shell commands and string parsing, or you give git a real API.

git-er-done is the API version.

The basic shape

Install it:

npm install git-er-done

Then ask for git details:

const { gitDetails } = require('git-er-done')

const git = await gitDetails({
  base: 'main',
  head: 'HEAD'
})

console.log('Modified files:', git.modifiedFiles)
console.log('Created files:', git.createdFiles)
console.log('Deleted files:', git.deletedFiles)

The stable object is what matters. Once git history has a shape, automation can make decisions around it.

const { gitDetails } = require('git-er-done')

const git = await gitDetails({ base: 'main' })

const source = git.fileMatch('src/**/*.js', '!**/*.test.js')
const docs = git.fileMatch('**/*.md')
const packages = git.fileMatch('packages/*/package.json')

if (source.edited) {
  console.log('Run tests')
}

if (packages.edited) {
  console.log('Reinstall dependencies and run package checks')
}

if (docs.edited && !source.edited) {
  console.log('Skip runtime test suite')
}

Git history should be queryable.

Why this exists

I first started reaching for this while working on build tooling. Netlify Build Plugins exposed git utilities so plugins could ask small questions before doing expensive work:

  • Did HTML change?
  • Did markdown change?
  • Did CSS get deleted?
  • Can this build exit early?
  • Can this expensive step be skipped?

I wrote about that in Netlify year two, and the earlier raw version lives in the snippet Get files changed from Git history.

The pattern kept coming back. CI systems are good at responding to events: a push happened, a pull request opened, a workflow was dispatched. They are weaker at understanding what the event means.

git-er-done sits in that gap.

It turns "a commit happened" into "these files changed, these packages are affected, and these are the cheapest useful next steps."

File matching is the money

The fileMatch helper is the part I use constantly.

It takes glob patterns and returns status-specific results:

const configFiles = git.fileMatch([
  '**/.env*',
  '**/config.js',
  '**/config.json',
  '**/*.config.js',
  '**/.github/workflows/**'
])

if (configFiles.edited) {
  console.log('Configuration changed')
  console.log(configFiles.modifiedFiles)
  console.log(configFiles.createdFiles)
  console.log(configFiles.deletedFiles)
}

The object tells you whether matched files were modified, created, deleted, or edited. That distinction matters. A deleted CSS file is not the same as a modified CSS file. A created package is not the same as a modified package. A changed workflow file is not the same as a changed README.

Most CI pipelines collapse those cases into one answer: run everything. Wasteful.

Smarter CI

A monorepo example:

const { gitDetails } = require('git-er-done')

const git = await gitDetails({
  base: process.env.GITHUB_BASE_REF || 'main',
  head: 'HEAD'
})

const changedFiles = [
  ...git.modifiedFiles,
  ...git.createdFiles,
  ...git.deletedFiles
]

const changedPackages = new Set()

for (const file of changedFiles) {
  const match = file.match(/^packages\/([^/]+)/)
  if (match) {
    changedPackages.add(match[1])
  }
}

console.log([...changedPackages].join(','))

Once you have that package list, CI can stop guessing.

- uses: actions/checkout@v4
  with:
    fetch-depth: 0

- name: Detect changed packages
  id: detect
  run: node scripts/detect-changed-packages.js

- name: Test changed packages
  if: steps.detect.outputs.count > 0
  run: pnpm test --filter "${{ steps.detect.outputs.packages }}"

The boring but critical detail is fetch-depth: 0.

If CI does not fetch enough history, git cannot answer history questions. Shallow clones are fast until the job needs to compare against a base branch, previous commit, or merge base.

Working tree checks

The package can also compare against uncommitted local changes:

const { gitDetails } = require('git-er-done')

const git = await gitDetails({
  base: 'main',
  includeWorkingChanges: true
})

const tests = git.fileMatch('**/*.test.js', '**/*.spec.js')

if (!tests.edited && git.fileMatch('src/**').edited) {
  console.log('Source changed without test changes')
}

Useful for local scripts, pre-commit checks, release prep, or agent workflows where you want to inspect the diff before asking another tool to act on it.

Commit metadata and line counts

gitDetails also exposes commit metadata and a line-count helper:

const { gitDetails } = require('git-er-done')

const git = await gitDetails({
  base: 'main',
  head: 'feature-branch'
})

for (const commit of git.commits) {
  console.log(commit.sha)
  console.log(commit.author.name)
  console.log(commit.subject)
}

const changedLines = await git.linesOfCode()
console.log('Lines changed:', changedLines)

That is enough signal for release notes, code review helpers, risk scoring, or "this PR is too large, split it up" checks.

There are also helpers for the current branch, git root, remotes, file contents at a commit, file created/modified timestamps, and specific commit lookups. Subpath exports are available when a script only needs one helper.

A practical automation pattern

The pattern:

  1. Query git once.
  2. Classify the change.
  3. Decide the smallest useful action.
  4. Save or print the result in a format CI can use.

For example:

const { gitDetails } = require('git-er-done')

const git = await gitDetails({
  base: process.env.GITHUB_EVENT_BEFORE || 'main',
  head: process.env.GITHUB_SHA || 'HEAD'
})

const plan = {
  testRuntime: git.fileMatch('src/**', 'packages/**/src/**').edited,
  testDocs: git.fileMatch('**/*.md').edited,
  reviewInfra: git.fileMatch(
    '**/.github/workflows/**',
    '**/Dockerfile*',
    '**/serverless.yml',
    '**/netlify.toml'
  ).edited,
  reinstall: git.fileMatch('**/package.json', '**/pnpm-lock.yaml').edited
}

console.log(JSON.stringify(plan, null, 2))

That plan can drive a workflow:

  • Runtime changed: run tests and build.
  • Docs only: run markdown checks and skip expensive integration tests.
  • Infrastructure changed: require a different reviewer.
  • Dependencies changed: install from scratch and clear caches.
  • Nothing relevant changed: exit early.

This is how CI gets cheaper and less noisy: run the right checks for the actual change.

Where this gets more interesting

Git change detection is useful outside CI too:

  • Release note generation.
  • Package publishing decisions.
  • Docs automation.
  • Build cache invalidation.
  • Monorepo task routing.
  • Agent handoffs.
  • Security review targeting.
  • "What should I look at first?" summaries.

Agents make this more valuable. If an agent is about to review a PR, the first context it should receive is not the whole repo. Give it the changed files, changed packages, commit subjects, line counts, and high-risk categories.

That lets the agent start with the shape of the change instead of wandering the filesystem.

The bigger lesson

Git is already the source of truth for how a codebase changes over time.

Most automation treats it like a trigger.

git-er-done treats it like data.

That shift opens up a lot:

  • Skip work confidently.
  • Run narrower checks.
  • Route review to the right person or agent.
  • Publish only what changed.
  • Generate better release notes.
  • Explain why a pipeline did or did not run.

The package is small, but the pattern is durable: ask git what changed, convert the answer into structured data, and let automation make a better decision.

Links: