Skip to content

Instantly share code, notes, and snippets.

@darcyparker
Last active January 27, 2026 16:27
Show Gist options
  • Select an option

  • Save darcyparker/6eb3625fa02e1b072d65d5b53356aac1 to your computer and use it in GitHub Desktop.

Select an option

Save darcyparker/6eb3625fa02e1b072d65d5b53356aac1 to your computer and use it in GitHub Desktop.
git-prlog: Git log with expanded PR commits for squash-merged repositories

git-prlog / git-prls

Git log with expanded PR commits. For repositories using squash merges, this shows the original PR commits indented under each squash-merged commit on main.

Features

  • Shows main branch commits with their associated PR commits expanded below
  • Works with squash merges ((#123) format) and merge commits (Merge pull request #123)
  • Full commit SHAs shown for PR commits (so you can git checkout <sha> to inspect pre-squash commits)
  • Colored output matching git's color scheme
  • Uses git's configured pager (core.pager, e.g., delta, less)
  • Falls back gracefully if gh CLI is unavailable

Requirements

Installation

View the gist: git-prlog

  1. Clone the gist:

    git clone git@gist.github.com:6eb3625fa02e1b072d65d5b53356aac1.git ~/src/git-prlog
  2. Symlink to a directory in your $PATH (e.g., ~/bin or ~/.local/bin):

    ln -s ~/src/git-prlog/git-prlog ~/bin/git-prlog
    ln -s ~/src/git-prlog/git-prlog ~/bin/git-prls
  3. Add aliases to your ~/.gitconfig:

    [alias]
      prlog = "!git-prlog"
      prls = "!git-prls"
  4. (Optional) Add tig integration to your ~/.tigrc:

    # Show PR commits for squash-merged commit (extracts PR# from commit message)
    # Shows in terminal, press Enter to return to tig
    bind main    P !sh -c "git-prls -1 %(commit)"
    bind diff    P !sh -c "git-prls -1 %(commit)"
    bind log     P !sh -c "git-prls -1 %(commit)"
    

Usage

Command Description
git prls Short form - one line per commit + indented PR commits
git prls -10 Last 10 commits
git prlog Long form - full commit details + boxed PR commits
git prlog main~5..main Specific commit range
git prls --no-pager Disable pager
git prls --debug Show diagnostic info

All standard git log options work.

Checking Out PR Commits

The full commit SHAs shown for PR commits can be fetched and checked out to inspect the original code before squashing:

# Fetch and checkout a PR commit in one command
git fetch origin <commit> && git checkout FETCH_HEAD

# Or separately
git fetch origin f6g7h8i
git checkout f6g7h8i

This is useful for reviewing the original commit history, bisecting issues, or cherry-picking specific changes.

Example Output

Short form (git prls):

a1b2c3d4e5 (HEAD -> main) feat(auth): add user authentication (#42) [Alice]
  ├─ f6g7h8i feat(auth): add JWT token validation [Alice]
  ├─ j9k0l1m feat(auth): add login endpoint [Alice]
  └─ n2o3p4q feat(auth): add user model [Alice]
b5c6d7e8f9 fix(db): resolve connection timeout (#41) [Bob]
  ├─ r5s6t7u fix(db): increase connection pool size [Bob]
  └─ v8w9x0y fix(db): add retry logic [Bob]

Long form (git prlog):

commit a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 (HEAD -> main)
Author: Alice
Date:   Mon Jan 15 10:30:00 2024 -0500

    feat(auth): add user authentication (#42)

    Implement JWT-based authentication with login endpoint and token
    validation. Includes user model with secure password hashing.

    Closes #38

  ┌─ PR #42 commits:
  │  commit f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5
  │  Author: Alice
  │  Date:   Mon Jan 15 09:00:00 2024 -0500
  │
  │      feat(auth): add JWT token validation
  │
  │  commit j9k0l1m2n3o4p5q6r7s8t9u0v1w2x3y4z5a6b7
  │  Author: Alice
  │  Date:   Sun Jan 14 16:30:00 2024 -0500
  │
  │      feat(auth): add login endpoint
  │
  │  commit n2o3p4q5r6s7t8u9v0w1x2y3z4a5b6c7d8e9f0
  │  Author: Alice
  │  Date:   Sun Jan 14 14:00:00 2024 -0500
  │
  │      feat(auth): add user model
  │
  └─────────────────

Author

Darcy Parker - GitHub

Vibe programmed with Claude

#!/usr/bin/env bash
# git-prlog: Git log with expanded PR commits
# For squash-merged PRs, shows the original PR commits indented under each main commit
# https://gist.github.com/darcyparker/6eb3625fa02e1b072d65d5b53356aac1
#
# Usage: git prlog [git-log-options]
# git prls [git-log-options] (short form, via symlink or alias)
#
# Examples:
# git prlog -10 # Last 10 commits with PR details
# git prlog main~5..main # Range of commits
# git prls # Short form output
# Detect if we're in short mode (prls) or long mode (prlog)
SCRIPT_NAME=$(basename "$0")
SHORT_MODE=false
if [[ "$SCRIPT_NAME" == "git-prls" ]]; then
SHORT_MODE=true
fi
# Parse options - check for our custom flags first
GIT_ARGS=()
USE_PAGER=true
DEBUG_MODE=false
while [[ $# -gt 0 ]]; do
case "$1" in
--short | -s)
SHORT_MODE=true
shift
;;
--long | -l)
SHORT_MODE=false
shift
;;
--no-pager)
USE_PAGER=false
shift
;;
--debug)
DEBUG_MODE=true
USE_PAGER=false
shift
;;
*)
GIT_ARGS+=("$1")
shift
;;
esac
done
# Use git's configured pager (respects core.pager, $GIT_PAGER, $PAGER)
GIT_PAGER=$(git var GIT_PAGER 2>/dev/null || echo "${PAGER:-less}")
# Only use pager if stdout is a terminal
if [[ ! -t 1 ]]; then
USE_PAGER=false
fi
# Colors (matching git's color scheme)
C_YELLOW='\033[33m'
C_RED='\033[31m'
C_BLUE='\033[34m'
C_CYAN='\033[36m'
C_GREEN='\033[32m'
C_DIM='\033[2m'
C_RESET='\033[0m'
# Disable colors if not using pager and not a terminal
# (pagers like delta/less -R handle colors fine)
if [[ ! -t 1 ]] && [[ "$USE_PAGER" == "false" ]]; then
C_YELLOW=''
C_RED=''
C_BLUE=''
C_CYAN=''
C_GREEN=''
C_DIM=''
C_RESET=''
fi
# Check if gh is available and authenticated (called once at startup, before any pipes)
gh_available() {
command -v gh &>/dev/null && gh auth status &>/dev/null 2>&1
}
# Check gh availability early, before entering any pipe context
GH_AVAILABLE="false"
if gh_available; then
GH_AVAILABLE="true"
fi
# Debug output
if $DEBUG_MODE; then
echo "=== DEBUG INFO ===" >&2
echo "GH_AVAILABLE: $GH_AVAILABLE" >&2
echo "SHORT_MODE: $SHORT_MODE" >&2
echo "USE_PAGER: $USE_PAGER" >&2
echo "GIT_PAGER: $(git var GIT_PAGER 2>/dev/null)" >&2
echo "GIT_ARGS: ${GIT_ARGS[*]}" >&2
echo "stdout is tty: $([[ -t 1 ]] && echo yes || echo no)" >&2
echo "gh path: $(command -v gh)" >&2
echo "=================" >&2
fi
# Extract PR number from commit message
# Looks for patterns like "(#123)" or "Merge pull request #123"
extract_pr_number() {
local message="$1"
# Match (#123) at end of first line (squash merge default)
if [[ "$message" =~ \(#([0-9]+)\) ]]; then
echo "${BASH_REMATCH[1]}"
return
fi
# Match "Merge pull request #123" (merge commit)
if [[ "$message" =~ Merge\ pull\ request\ #([0-9]+) ]]; then
echo "${BASH_REMATCH[1]}"
return
fi
}
# Get PR commits using gh API
get_pr_commits() {
local pr_number="$1"
# Returns: sha|author_name|date|message (one per line)
gh api "repos/{owner}/{repo}/pulls/${pr_number}/commits" \
--jq '.[] | "\(.sha)|\(.commit.author.name)|\(.commit.author.date)|\(.commit.message | split("\n")[0])"' \
2>/dev/null || true
}
# Format a single PR commit (short form)
format_pr_commit_short() {
local sha="$1"
local author="$2"
local message="$3"
local prefix="$4"
local short_sha="${sha:0:7}"
printf " ${C_DIM}%s${C_RESET} ${C_CYAN}%s${C_RESET} %s ${C_BLUE}[%s]${C_RESET}\n" \
"$prefix" "$short_sha" "$message" "$author"
}
# Format a single PR commit (long form)
format_pr_commit_long() {
local sha="$1"
local author="$2"
local date="$3"
local message="$4"
local formatted_date
formatted_date=$(date -d "$date" "+%a %b %d %H:%M:%S %Y %z" 2>/dev/null || echo "$date")
printf " ${C_DIM}│${C_RESET} ${C_CYAN}commit %s${C_RESET}\n" "$sha"
printf " ${C_DIM}│${C_RESET} Author: %s\n" "$author"
printf " ${C_DIM}│${C_RESET} Date: %s\n" "$formatted_date"
printf " ${C_DIM}│${C_RESET}\n"
printf " ${C_DIM}│${C_RESET} %s\n" "$message"
printf " ${C_DIM}│${C_RESET}\n"
}
# Process a single commit
process_commit() {
local hash="$1"
local short_hash="$2"
local author="$3"
local date="$4"
local decorations="$5"
local subject="$6"
local gh_ok="$7"
if $SHORT_MODE; then
# Short format (like git ls)
printf "${C_YELLOW}%s${C_RED}%s${C_RESET} %s ${C_BLUE}[%s]${C_RESET}\n" \
"$short_hash" "$decorations" "$subject" "$author"
else
# Long format (like git log)
printf "${C_YELLOW}commit %s${C_RED}%s${C_RESET}\n" "$hash" "$decorations"
printf "Author: %s\n" "$author"
local formatted_date
formatted_date=$(date -d "$date" "+%a %b %d %H:%M:%S %Y %z" 2>/dev/null || echo "$date")
printf "Date: %s\n" "$formatted_date"
printf "\n %s\n\n" "$subject"
fi
# Try to expand PR commits
if [[ "$gh_ok" == "true" ]]; then
local pr_number
pr_number=$(extract_pr_number "$subject")
if [[ -n "$pr_number" ]]; then
local pr_commits
pr_commits=$(get_pr_commits "$pr_number")
if [[ -n "$pr_commits" ]]; then
local commit_count
commit_count=$(echo "$pr_commits" | wc -l)
local i=0
if $SHORT_MODE; then
while IFS='|' read -r pr_sha pr_author pr_date pr_message; do
[[ -z "$pr_sha" ]] && continue
((i++)) || true
local prefix="├─"
[[ $i -eq $commit_count ]] && prefix="└─"
format_pr_commit_short "$pr_sha" "$pr_author" "$pr_message" "$prefix"
done <<<"$pr_commits"
else
printf " ${C_DIM}┌─ PR #%s commits:${C_RESET}\n" "$pr_number"
while IFS='|' read -r pr_sha pr_author pr_date pr_message; do
[[ -z "$pr_sha" ]] && continue
format_pr_commit_long "$pr_sha" "$pr_author" "$pr_date" "$pr_message"
done <<<"$pr_commits"
printf " ${C_DIM}└─────────────────${C_RESET}\n\n"
fi
fi
fi
fi
}
# Main processing
main() {
# Use pre-computed GH_AVAILABLE (checked before entering pipe context)
local gh_ok="$GH_AVAILABLE"
if [[ "$gh_ok" != "true" ]]; then
echo -e "${C_DIM}# Note: gh not available or not authenticated; PR commits won't be expanded${C_RESET}" >&2
fi
# Use NUL as record separator to handle all edge cases
# Format: hash<NUL>short_hash<NUL>author<NUL>date<NUL>decorations<NUL>subject<NUL>
local log_format="%H%x00%h%x00%an%x00%aI%x00%d%x00%s%x00"
# Read commits separated by NUL characters
local hash short_hash author date decorations subject
while IFS= read -r -d '' hash &&
IFS= read -r -d '' short_hash &&
IFS= read -r -d '' author &&
IFS= read -r -d '' date &&
IFS= read -r -d '' decorations &&
IFS= read -r -d '' subject; do
process_commit "$hash" "$short_hash" "$author" "$date" "$decorations" "$subject" "$gh_ok"
done < <(git log --pretty=format:"$log_format" "${GIT_ARGS[@]}")
}
# Run main, optionally through pager
if [[ "$USE_PAGER" == "true" ]]; then
main | $GIT_PAGER
else
main
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment