Git workflow guide
Git log to JSON without hand-rolled escaping
Git prints its normal history, jc-rs parses each commit into a JSON array, and jq shapes the report. Commit subjects, names, and multi-line messages are serialized as data instead of being spliced into a JSON template.
By Oleg Sotnikov · Published · Examples exercised against a real repository and the Git parser fixture suite
The direct conversion
Run this inside a repository. --no-decorate keeps branch and tag labels out of the commit hash field; jc-rs reads the standard commit blocks and writes one JSON object per commit.
git log -n 20 --no-decorate |
jc-rs --git-log > commits.jsonA default record includes the full commit hash, author name and email, the displayed date, the complete commit message, and parsed epoch fields. Empty author names and email addresses remain null instead of disappearing.
commitFull commit hash from the commit header
stringauthor / author_emailIdentity from the Author line
string | nulldateGit's displayed author date
stringmessageSubject and body, with line breaks preserved
stringepoch / epoch_utcParsed timestamp values when available
number | nullYou can also use jc-rs magic syntax: jc-rs git log -n 20 --no-decorate. The explicit pipe is easier to extend with Git options and makes the data boundary obvious in a script.
Why JSON-shaped pretty formats are brittle
A common shortcut asks Git to print text that resembles a JSON object:
git log --format='{"commit":"%H","subject":"%s"}'Git substitutes the subject directly into that template. A subject containing a double quote, backslash, tab, or control character needs JSON escaping that the format string does not provide. Multi-line bodies add another delimiter problem, and joining the objects into an array introduces comma handling.
git log --no-decorate |
jc-rs --git-log |
jq 'map({commit, author, date, message})'jc-rs creates valid JSON from Git's record grammar. jq filters that JSON. Neither commit text nor a shell variable is evaluated as jq or JSON source code.
Use a Git format the parser understands
The parser covers Git's familiar log styles and stat blocks. Choose the least verbose style that contains the fields you need.
git log --no-decorateCommit, author, date, and message
Best default
git log --format=fuller --no-decorateCommitter identity and commit date
Adds commit_by fields
git log --stat --no-decorateChanged files, insertions, and deletions
Adds nested stats
git log --oneline --no-abbrev-commitFull hash and subject only
Full 40-character hash is required
git log -n 10 --format=fuller --no-decorate |
jc-rs --git-log |
jq 'map({commit, author, commit_by, date, commit_by_date})'Avoid arbitrary custom --format strings here. The parser needs recognizable commit, identity, date, message, and stat lines; it cannot infer the meaning of an unrelated delimiter scheme.
Practical Git log to JSON recipes
A compact release-note feed
git log v1.4.0..HEAD --no-merges --no-decorate |
jc-rs --git-log |
jq 'map({
commit: .commit[0:12],
author,
subject: (.message | split("\n")[0])
})'Commit counts by author
git log --since='2026-01-01' --no-decorate |
jc-rs --git-log |
jq 'group_by(.author)
| map({author: .[0].author, commits: length})
| sort_by(-.commits)'Changed-line totals from stat output
git log -n 50 --stat --no-decorate |
jc-rs --git-log |
jq 'map({
commit: .commit[0:12],
files: (.stats.files_changed // 0),
lines: ((.stats.insertions // 0) + (.stats.deletions // 0))
})'Only commits touching one path
git log --no-decorate -- website/src/ |
jc-rs --git-log |
jq 'map({commit, author, message})'Let Git perform revision, merge, author, date, and path selection. It can walk its own graph more efficiently than jq can filter an unnecessarily large history afterward.
Treat an empty history and a failed command separately
An empty revision range is valid and becomes an empty JSON array. A bad revision or a failed parser is an error. Bash needs pipefail to preserve failures from the left side of the pipeline; jq can enforce a non-empty result when the job requires one.
#!/usr/bin/env bash
set -Eeuo pipefail
range=${1:-HEAD~10..HEAD}
if ! git log "$range" --no-decorate |
jc-rs --git-log |
jq -e '
if length > 0
then map({commit, author, message})
else error("revision range contains no commits")
end
' >commits.json
then
printf 'could not build commit report for %s\n' "$range" >&2
exit 1
fiThe range is passed to Git as one quoted argument. It is never interpolated into the jq program.
Boundaries worth knowing
- This parses log text, not the Git object database. Git remains responsible for revision walking, path filters, mailmap behavior, and date selection.
- Disable decorations for stable hashes. Branch and tag labels are presentation text; keep them out of the commit field unless they are intentionally part of your report.
- Oneline input needs full hashes. Add
--no-abbrev-commit. The abbreviated form is not enough for the parser's oneline record boundary. - Large histories are batch output. The regular Git parser returns one array after input ends. Limit the range in Git rather than converting years of history for a ten-commit report.
Check one real record before reusing the filter
The parser page shows a Git fixture beside its exact JSON output. Confirm the fields you plan to query; the Bash guide covers the failure handling used in the script above.