Skip to main content
Published / updated

AI in the Git Workflow

Before you start

You need: Git (Track 16) and Articles 01–06.

Time: about 45 minutes, at the keyboard.

Learning objective

Use Git so that any AI-assisted change is reviewable, reversible and attributable.

Topics

  • Why Git matters more with AI
  • Commit before generating
  • Reading the diff
  • Commit boundaries
  • Commit messages and attribution
  • Pull requests with generated code
  • Recovering from an agent
  • Automated checks

Why Git matters more with AI

AI produces more code, faster, and it produces code you did not write. Both raise the value of everything Git provides:

Git gives youWhy it matters more now
A restore pointUndoing a bad generation costs one command
A diffThe only reliable account of what changed
Small commitsThe unit you can actually review
Blame and historyWhy a line exists, when nobody remembers writing it
BisectFinding which generated change broke something

Without commit discipline, an agent editing files leaves you unable to tell what changed. With it, every generation is a diff you can read, accept or discard.

Commit before generating

git status # must be clean
git commit -am "Add fee summary endpoint"
# now generate

A clean working directory before generating is the single most useful habit in this article.

With it:

git diff # exactly what the AI changed, nothing else
git restore . # undo everything it did

Without it, your own uncommitted work and the AI's changes are mixed in one diff, and git restore destroys both.

Stash if you cannot commit:

git stash -u # -u includes untracked files
# generate
git stash pop

Reading the diff

git diff # unstaged
git diff --staged
git diff --stat # which files, how many lines
git diff -- src/Data/ # one folder
git add -p # review hunk by hunk while staging

Read the diff, not the AI's summary of what it did. A summary is a description; the diff is what happened. They differ more often than you would expect — an agent asked to change one method may reformat the file, add a using directive, or touch a second file it decided was related.

git diff --stat first. If you asked for one method and five files changed, stop and look at why before reading any content.

src/Data/FeeRepository.cs | 24 ++++++--
src/Services/FeeService.cs | 18 ++++--
src/Core/FeeAccount.cs | 3 +- ← you did not ask for this
appsettings.json | 2 +- ← or this

git add -p is the best review tool for generated code, because it forces a decision on every hunk: y to stage, n to skip, d to skip the rest of the file. Accept the good hunks, reject the rest.

Commit boundaries

One commit per verified change, as always — but with generated code there is a second rule:

Never mix generated and hand-written changes in one commit.

Bad: Add fee defaulters feature
[generated repository + your controller changes + a refactor]

Good: Add GetFeeDefaultersAsync to FeeRepository
Add fee defaulters endpoint
Extract due-date logic into FeePolicy

When the middle one turns out to be wrong, reverting it leaves the other two intact.

Refactor and behaviour change stay separate, exactly as in the Git track. Generated refactors change behaviour more often than they announce, so the separation matters more here.

Commit messages and attribution

Add fee defaulter query to FeeRepository

Returns fee accounts where the balance is above zero and the due date has
passed, for one school. Filters by SchoolId from the auth claim and
excludes soft-deleted students.

Generated with Claude, reviewed against the multi-tenant checklist and
tested against the seed data including a student with two accounts.

The "why" is unchanged. A diff cannot say why a rule exists, whoever wrote the lines.

Attribution is a team convention. Some teams require a trailer or a note; some consider it irrelevant since the committer is responsible either way. Follow whatever the team agreed, and if there is no convention, ask rather than inventing one.

Recording what you verified is worth more than recording the tool. "Tested against a student with two accounts" tells a reviewer which risk you already covered.

Never write a message describing code you have not read. A message that misdescribes the change is worse than none.

Pull requests with generated code

The description should say what you verified:

## What
Adds the fee defaulters report — students with an outstanding balance
past the due date.

## Why
The accounts team currently exports the full fee list and filters it
in Excel. #153.

## How
- FeeRepository.GetFeeDefaultersAsync — single query, joined to Student
- FeeController.GetDefaulters — Admin and Principal only
- React page reusing the existing table component

## AI assistance
The repository method and the SQL were generated with Claude.
Reviewed against the security and multi-tenant checklists.
The controller and the frontend are hand-written.

## Testing
- Unit tests: no defaulters, one, many, a student with two fee accounts
- Verified SchoolId comes from the claim: signing in as School 1 and
requesting School 2 returns 403
- Verified soft-deleted students are excluded
- Checked money types are decimal end to end
- Row count verified against a manual SSMS query: 23 defaulters

## Notes
Not paginated. 23 rows today, capped at 500. #158 covers pagination.

Saying what was generated is not an admission — it directs the reviewer's attention. A reviewer who knows the SQL was generated checks the tenant filter first, which is exactly where the risk is.

"Testing" carries the weight. It shows the silent failures were considered: tenant isolation, soft delete, money types, multiple accounts.

Review generated code with more care than hand-written code, not less, because there is no author to ask why.

Recovering from an agent

Tools that edit the repository directly (Claude Code, Cursor, Copilot Workspace) can change more than you intended.

SituationRecovery
Uncommitted changes are wronggit restore .
One file is wronggit restore src/Services/FeeService.cs
Committed but not pushedgit reset --soft HEAD~1 to keep the changes, --hard to discard
Pushedgit revert <hash>
Untracked files createdgit clean -n, read the list, then git clean -fd
Lost work after a bad resetgit reflog

git clean -n before git clean -fd, always. An agent's new files sit alongside your .env and local certificates, and -fdx would take all of them.

git status --short # what changed, including untracked
git diff --stat # scale
git diff # content

Those three commands, in that order, after every agent run.

Keep the working directory clean between generations so each run's changes are isolated. A generation on top of an unreviewed generation is two changes you can no longer separate.

Automated checks

Make the mechanical failures impossible to merge:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- run: dotnet restore
- run: dotnet build --no-restore -warnaserror
- run: dotnet test --no-build --verbosity normal
- run: dotnet list package --vulnerable --include-transitive

-warnaserror catches CS4014 — a task not awaited — which is a warning nobody reads and a bug that silently discards exceptions.

A pre-commit hook for the checks a build cannot do:

#!/bin/sh
# .git/hooks/pre-commit

if git diff --cached | grep -qE "^\+.*(Password\s*=\s*\"|ApiKey\s*=\s*\"|Server=.*User Id=)"; then
echo "Possible secret in staged changes. Review before committing."
exit 1
fi

if git diff --cached | grep -qE "^\+.*(<<<<<<<|>>>>>>>)"; then
echo "Conflict markers in staged changes."
exit 1
fi

if git diff --cached --name-only | grep -qE "^src/Data/.*\.cs$"; then
if ! git diff --cached -- src/Data/ | grep -q "SchoolId"; then
echo "Data-layer change with no SchoolId reference. Confirm the tenant filter."
exit 1
fi
fi

The last check is deliberately crude — it will fire on legitimate changes. That is acceptable for the multi-tenant rule, because the failure it guards against is a data breach with no error message, and confirming a false positive costs seconds.

Secret scanning is worth enabling at the server too (GitHub → Settings → Code security), since a hook only runs on machines that have it installed.

Errors you will hit

The mistakeConsequence
Generating with uncommitted work presentYour changes and the tool's are inseparable
Reading the summary instead of the diffFiles you did not expect are committed
Not checking --statFive files changed when you asked for one
Mixing generated and hand-written in one commitReverting one loses the other
git clean -fd without -n.env and local certificates gone
No CIMechanical failures reach review

A clean working directory before generating is the single most useful habit here — git diff then shows exactly what the tool did.

Common mistakes

  • Generating with uncommitted work present
  • Reading the summary instead of the diff
  • Not checking --stat for unexpected files
  • One commit containing generated and hand-written changes
  • A commit message describing code you have not read
  • Not saying in the PR which parts were generated
  • Reviewing generated code less carefully
  • git clean -fd without -n
  • No CI, so mechanical failures reach review
  • Building on top of an unreviewed generation

Practice

The course exercise is use Git as the safety net.

  1. Generate a change with uncommitted work present, then try to separate the two diffs.
  2. Commit first, generate, then use git diff to see exactly what changed.
  3. Run git diff --stat after a generation and check whether any unexpected file appears.
  4. Use git add -p to accept some hunks of a generated change and reject others.
  5. Compare an AI's summary of its change with the actual diff.
  6. Generate a change, dislike it, and undo it with git restore ..
  7. Commit a generated change, then undo it with reset --soft and with --hard. Note the difference.
  8. Push a generated change and undo it with revert.
  9. Let an agent create untracked files, then use git clean -n before removing them.
  10. Write a commit message recording both the why and what you verified.
  11. Write a PR description with an "AI assistance" section and a testing section listing the silent failures you checked.
  12. Set up the CI workflow and confirm -warnaserror fails on a missing await.
  13. Install the pre-commit hook and try to commit a fake connection string.
  14. Trigger the data-layer SchoolId check with a legitimate change, and decide whether the false positive is worth it.
  15. Run two generations without reviewing the first, then try to separate them.

Exercises 1 and 15 both demonstrate the same thing: without commit boundaries, generated changes become inseparable.

You can now

  • Commit before generating
  • Read the diff, not the summary
  • Keep generated and hand-written changes in separate commits
  • Recover from an agent that changed too much
  • Automate the checks that catch mechanical failures

Review questions

  1. Why commit before generating?
  2. Why read the diff rather than the AI's summary?
  3. Why should generated and hand-written changes be in separate commits?
  4. What does saying "this part was generated" give a reviewer?

Next: AI-assisted feature under review