Parsing git status porcelain output Jump to heading
Hooks, prompts, editor integrations and CI guards all need to know the state of a working tree: is it clean, what is staged, are there conflicts, is the branch ahead of its upstream. git status answers all of that for people, in a format that changes between versions and follows the userβs language. git status --porcelain=v2 answers it for scripts, in a documented format that will not change, with one line per entry and a type character at the start that says exactly what kind of entry it is. Combined with -z, every path arrives intact, whatever characters it contains. This page explains the format and builds a small reader for it, within scripting Git with plumbing commands.
When to use this approach Jump to heading
- A script parses
git statusorgit status --shortoutput and breaks on some machines. - You need staged versus unstaged state, conflict detection or branch tracking information in a script.
- File names in your repositories include spaces, non-ASCII characters or worse.
- You are writing a hook that must refuse to run on a dirty or conflicted tree, as in handling partially staged files.
Step 1 β Request the right format Jump to heading
--porcelain=v2 gives the detailed, stable format. -z terminates entries with NUL instead of newline and turns off path quoting. --branch adds header lines about the current branch and its upstream.
git status --porcelain=v2 -z --branch | tr '\0' '\n' # tr only to make it readable here
# # branch.oid 8b3e4f2cβ¦
# # branch.head feature/export
# # branch.upstream origin/feature/export
# # branch.ab +2 -0
# 1 .M N... 100644 100644 100644 1d3f⦠1d3f⦠src/export/schedule.py
# 1 M. N... 100644 100644 100644 9e2a⦠4b77⦠src/export/api.py
# u UU N... 100644 100644 100644 100644 a1β¦ b2β¦ c3β¦ src/billing/totals.py
# ? notes/todo.md Step 2 β Read the XY status of changed entries Jump to heading
Ordinary entries (1) and renames (2) carry a two-character XY field: X is the staged (index) status, Y the unstaged (working tree) status, with . meaning unchanged.
1 .M β¦ modified in the working tree, not staged
1 M. β¦ modified and staged
1 MM β¦ staged, then modified again (partially staged)
1 A. β¦ added to the index
1 .D β¦ deleted in the working tree, not staged
2 R. β¦ R100 new\0old renamed in the index (path, then original path) Step 3 β Parse the stream safely in shell Jump to heading
Because entries are NUL-terminated and rename entries contain a second NUL-terminated path, a line-oriented while read is not enough in POSIX shell. A small awk program with the record separator set to NUL handles it, including the extra path after renames.
#!/bin/sh
# scripts/status-summary.sh β counts and lists from porcelain v2
git status --porcelain=v2 -z --branch | awk 'BEGIN { RS="\0" }
skip { skip=0; next } # original path after a rename
/^# branch.head / { head=substr($0, 15) }
/^# branch.ab / { split($0, ab, " "); ahead=ab[3]; behind=ab[4] }
/^1 / { xy=substr($0,3,2); path=$0; sub(/^([^ ]+ ){8}/, "", path)
if (substr(xy,1,1)!=".") staged++; if (substr(xy,2,1)!=".") unstaged++ }
/^2 / { xy=substr($0,3,2); if (substr(xy,1,1)!=".") staged++; if (substr(xy,2,1)!=".") unstaged++; skip=1 }
/^u / { conflicted++ }
/^\? / { untracked++ }
END { printf "branch=%s ahead=%s behind=%s staged=%d unstaged=%d conflicted=%d untracked=%d\n",
head, ahead, behind, staged, unstaged, conflicted, untracked }' The sub(/^([^ ]+ ){8}/, "") removes the eight fixed fields before the path on a type-1 line, leaving the path intact even if it contains spaces. Type-2 lines have nine fields before the path, and type-u lines have ten.
# Verification: works with hostile names
printf 'x' > 'name with space.txt'; printf 'y' > 'tab here.txt'
sh scripts/status-summary.sh # untracked=2, no broken output Step 4 β Turn the summary into decisions Jump to heading
Most scripts need a yes-or-no answer. Build small predicates on the parser, and use exit codes for the simplest cases.
# Fail a release script on any uncommitted change, conflicts or untracked files
summary=$(sh scripts/status-summary.sh)
case "$summary" in
*conflicted=[1-9]*) echo "resolve conflicts first"; exit 1 ;;
*staged=0\ unstaged=0\ conflicted=0\ untracked=0) : ;;
*) echo "working tree not clean: $summary"; exit 1 ;;
esac
# Cheapest possible checks, no parsing at all
git diff --quiet || echo "unstaged changes"
git diff --cached --quiet || echo "staged changes"
test -z "$(git ls-files --others --exclude-standard -z | head -c1)" || echo "untracked files" Step 5 β Keep v1 for simple, legacy cases only Jump to heading
--porcelain without a version is v1, which matches git status --short without colour: two status characters, a space, the path. It is stable, but lacks branch tracking detail and submodule state. Use v2 for new scripts; v1 is fine where an existing script already depends on it and only needs paths and XY codes.
git status --porcelain=v1 -z | tr '\0' '\n' | head -3
# M src/export/schedule.py
# M src/export/api.py
# ?? notes/todo.md Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Is porcelain output affected by the userβs configuration? Jump to heading
Porcelain formats ignore colour and translation settings. Some options still matter β status.showUntrackedFiles=no hides untracked entries β so pass --untracked-files=all explicitly when your script needs them.
How do I detect submodule changes? Jump to heading
In v2, the third field of type-1 and 2 entries is a four-character submodule state: N... for a normal file, S followed by flags for submodules. Check it if your repository uses submodules.
Is git status slow on large repositories? Jump to heading
It can be, because it scans the working tree. For hooks on very large repositories, enable fsmonitor and the untracked cache, as described in using fsmonitor and the untracked cache.
Related Jump to heading
- Scripting Git with Plumbing Commands β the parent topic.
- Iterating Refs with for-each-ref β the same discipline for refs.
- Diagnosing a Slow git status β when the command itself is the bottleneck.
- Testing Git Hooks Before Sharing Them β fixtures for awkward working trees.