--- /dev/null
+#!/bin/bash
+#
+# git-grouped-log - generates a grouped list of commits based on subsystems
+# prefix in commit messages.
+#
+# Copyright (C) 2025 Karel Zak <kzak@redhat.com>
+#
+# This file is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# Ensure we're in a Git repository
+git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
+ echo "Error: Not inside a Git repository." >&2
+ exit 1
+}
+
+# Display help message
+if [[ "$1" == "--help" ]]; then
+ echo "Usage: $(basename $0) [--start <commit_id>] [--end <commit_id>]"
+ echo
+ echo "Generates a grouped list of commits based on subsystems."
+ exit 0
+fi
+
+declare -A commits
+declare -A subsystems
+
+start_commit="HEAD"
+end_commit=""
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --start)
+ start_commit="$2"
+ shift 2
+ ;;
+ --end)
+ end_commit="$2"
+ shift 2
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ exit 1
+ ;;
+ esac
+done
+
+# Verify end and start
+if ! git rev-parse --verify "$start_commit" >/dev/null 2>&1; then
+ echo "Error: Start commit '$start_commit' not found." >&2
+ exit 1
+fi
+if [[ -n "$end_commit" ]] && ! git rev-parse --verify "$end_commit" >/dev/null 2>&1; then
+ echo "Error: End commit '$end_commit' not found." >&2
+ exit 1
+fi
+
+# Construct git log range
+if [[ -n "$end_commit" ]]; then
+ range="$end_commit..$start_commit"
+else
+ range="$start_commit"
+fi
+
+while IFS='|' read -r subj author; do
+ # Handle "Revert" commits separately
+ if [[ "$subj" =~ ^Revert\ \"([^:]+):.* ]]; then
+ subsys="${BASH_REMATCH[1]}"
+ desc="(reverted) "$(echo "$subj" | sed -E 's/^Revert \"[^:]+: //;s/\"$//')
+ else
+ # Extract subsystem name (everything before the first colon)
+ subsys=$(echo "$subj" | awk -F': ' '{print $1}')
+ desc=$(echo "$subj" | awk -F': ' '{$1=""; sub(/^ /, ""); print}')
+ fi
+
+ # If no subsystem is detected, categorize under "Misc"
+ if [[ -z "$desc" ]]; then
+ subsys="Misc"
+ desc="$subj"
+ fi
+
+ key=$(echo "$subsys" | sed 's|[^a-zA-Z0-9_]|_|g')
+
+ subsystems["$key"]="$subsys"
+ commits["$key"]+=" - $desc (by $author)
+"
+done < <(git log --no-merges --pretty=format:'%s|%an' "$range" --)
+
+misc=""
+for key in "${!commits[@]}"; do
+ if [[ "${subsystems[$key]}" == "Misc" ]]; then
+ misc="${subsystems[$key]}:\n${commits[$key]}"
+ else
+ echo "${subsystems[$key]}:"
+ echo -e "${commits[$key]}"
+ fi
+done
+
+if [[ -n "$misc" ]]; then
+ echo -e "$misc"
+fi