Skip to content
jc-rs
GitHub

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 · Published · Examples exercised against a real repository and the Git parser fixture suite

Historygit log
Parsejc-rs --git-log
DataJSON array
Reportjq

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.

Bash
git log -n 20 --no-decorate |
  jc-rs --git-log > commits.json

A 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.

commit

Full commit hash from the commit header

string
author / author_email

Identity from the Author line

string | null
date

Git's displayed author date

string
message

Subject and body, with line breaks preserved

string
epoch / epoch_utc

Parsed timestamp values when available

number | null

You 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:

Fragile: output only looks like JSON
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.

Structured: serialization happens after parsing
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-decorate

Commit, author, date, and message

Best default

git log --format=fuller --no-decorate

Committer identity and commit date

Adds commit_by fields

git log --stat --no-decorate

Changed files, insertions, and deletions

Adds nested stats

git log --oneline --no-abbrev-commit

Full hash and subject only

Full 40-character hash is required

Committer as well as author
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

Bash
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

Bash
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

Bash
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

Bash
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.

Bash script
#!/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
fi

The 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.

Install jc-rs