git Einsteiger 4 Min. Lesezeit

Git-Repos im Überblick: Alle Repositories auf einen Blick

Problem

Wer mehrere Git-Repositories auf einem Rechner hat, verliert schnell den Überblick: Wo gibt es uncommitted Changes? Welche Branches wurden noch nicht gepusht? Jedes Repo einzeln zu prüfen kostet Zeit.

Lösung

git-status-report.sh durchsucht ein Verzeichnis nach Git-Repos und zeigt Branch, uncommitted Changes, untracked Files und unpushed Commits — farbkodiert auf einen Blick.

Getestet auf
  • Ubuntu 24.04
  • Debian 12
  • macOS 14
Benötigt
  • bash
  • git

Auf einem Entwickler-Rechner oder Homelab-Server sammeln sich schnell viele Git-Repositories an: eigene Projekte, geklonte Tools, Konfigurationsdateien unter Versionskontrolle. Die Frage Habe ich irgendwo noch uncommitted Changes? lässt sich nicht auf einen Blick beantworten — außer mit diesem Script.

git-status-report.sh durchsucht ein Verzeichnis rekursiv nach Git-Repositories und zeigt für jedes den aktuellen Branch, uncommitted Changes, untracked Files und lokale Commits die noch nicht gepusht wurden. Alles farbkodiert, alles auf einen Blick.

Das Problem

Wer regelmäßig mit Git arbeitet, kennt das: Man hat ein halbes Dutzend Repos auf dem Rechner und irgendwo liegen Änderungen die nie committed wurden. Oder ein Feature-Branch der nie gepusht wurde. git status hilft — aber nur für ein Repo gleichzeitig.

Die Lösung

Das Script nutzt find um alle .git-Verzeichnisse zu lokalisieren, und fragt dann für jedes Repository über git status --porcelain und git rev-list den aktuellen Zustand ab. Die Ausgabe ist tabellarisch: grün = alles sauber, gelb = Änderungen vorhanden.

Voraussetzungen

Nur bash und git werden benötigt. Funktioniert auf Linux und macOS.

Verwendung

# Alle Repos im Home-Verzeichnis prüfen
bash git-status-report.sh

# Bestimmtes Verzeichnis durchsuchen
bash git-status-report.sh ~/projekte

# Nur Repos MIT Änderungen anzeigen
bash git-status-report.sh --dirty-only

# Suchtiefe begrenzen (Standard: 4)
bash git-status-report.sh --depth 2

Zeile für Zeile erklärt

set -euo pipefail Bricht bei Fehler ab; ungesetzte Variablen und Pipeline-Fehler werden abgefangen
find "$SEARCH_DIR" -maxdepth "$MAX_DEPTH" -name ".git" -type d Findet alle .git-Verzeichnisse — das Elternverzeichnis ist das Repository
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?') Liest den aktuellen Branch-Namen, '?' wenn kein gültiges Repo
uncommitted=$(git status --porcelain | awk '/^[^?]/{n++} END{print n+0}') Zählt geänderte/staged Dateien; awk statt grep damit pipefail nicht auslöst
untracked=$(git status --porcelain | awk '/^\?\?/{n++} END{print n+0}') Zählt untracked Files separat — oft vergessene neue Dateien
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo '') Prüft ob ein Remote-Tracking-Branch gesetzt ist
unpushed=$(git rev-list "${upstream}..HEAD" --count 2>/dev/null || echo 0) Zählt lokale Commits die noch nicht gepusht wurden
[[ $DIRTY_ONLY -eq 1 && $is_dirty -eq 0 ]] && continue Mit --dirty-only: saubere Repos überspringen

Komplettes Script git-status-report.sh

#!/usr/bin/env bash
# git-status-report.sh — Alle Git-Repos im angegebenen Verzeichnis mit Status-Übersicht
# Zeigt: uncommitted changes, untracked files, unpushed commits — rein lesend.
set -euo pipefail

if [[ -t 1 ]]; then
  R=$'\e[31m'; G=$'\e[32m'; Y=$'\e[33m'; C=$'\e[36m'
  B=$'\e[1m'; D=$'\e[2m'; N=$'\e[0m'
else
  R=""; G=""; Y=""; C=""; B=""; D=""; N=""
fi

usage() {
  cat <<EOF
${B}git-status-report.sh${N} — findet alle Git-Repos und zeigt ihren Status.
Rein lesend, ändert nichts.

Nutzung: bash git-status-report.sh [VERZEICHNIS] [--depth N] [--dirty-only] [--help]
  VERZEICHNIS   Startverzeichnis (Standard: \$HOME)
  --depth N     Suchtiefe in Unterverzeichnissen (Standard: 4)
  --dirty-only  Nur Repos mit Änderungen oder unpushed Commits anzeigen
  --help        Diese Hilfe anzeigen
EOF
}

SEARCH_DIR="$HOME"
MAX_DEPTH=4
DIRTY_ONLY=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --help|-h)    usage; exit 0 ;;
    --dirty-only) DIRTY_ONLY=1; shift ;;
    --depth)      MAX_DEPTH="${2:?--depth braucht eine Zahl}"; shift 2 ;;
    -*)           echo "${R}Unbekannte Option: $1${N}" >&2; usage >&2; exit 2 ;;
    *)            SEARCH_DIR="$1"; shift ;;
  esac
done

command -v git >/dev/null || { echo "${R}git nicht gefunden.${N}" >&2; exit 1; }
[[ -d "$SEARCH_DIR" ]] || { echo "${R}Verzeichnis nicht gefunden: $SEARCH_DIR${N}" >&2; exit 1; }

echo "${B}Git-Repository-Report${N} ${D}— $(date '+%d.%m.%Y %H:%M')${N}"
echo "${D}Suche in: $SEARCH_DIR (Tiefe: $MAX_DEPTH)${N}"
echo

total=0; dirty=0; clean=0

while IFS= read -r gitdir; do
  repo="${gitdir%/.git}"
  name="${repo##*/}"

  # Status ermitteln
  cd "$repo" 2>/dev/null || continue
  branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "?")

  # Uncommitted (staged + unstaged)
  uncommitted=$(git status --porcelain 2>/dev/null | awk '/^[^?]/{n++} END{print n+0}')
  # Untracked files
  untracked=$(git status --porcelain 2>/dev/null | awk '/^\?\?/{n++} END{print n+0}')
  # Unpushed commits
  upstream=$(git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>/dev/null || echo "")
  if [[ -n "$upstream" ]]; then
    unpushed=$(git rev-list "${upstream}..HEAD" --count 2>/dev/null || echo 0)
  else
    unpushed="–"
  fi

  is_dirty=0
  [[ "$uncommitted" -gt 0 ]] 2>/dev/null && is_dirty=1
  [[ "$untracked" -gt 0 ]] 2>/dev/null && is_dirty=1
  [[ "$unpushed" =~ ^[1-9] ]] && is_dirty=1

  total=$((total + 1))
  [[ $is_dirty -eq 1 ]] && dirty=$((dirty + 1)) || clean=$((clean + 1))

  [[ $DIRTY_ONLY -eq 1 && $is_dirty -eq 0 ]] && continue

  # Ausgabe
  if [[ $is_dirty -eq 1 ]]; then
    printf '%s%-36s%s %s%-10s%s' "$Y" "${name:0:36}" "$N" "$C" "(${branch:0:10})" "$N"
  else
    printf '%s%-36s%s %s%-10s%s' "$G" "${name:0:36}" "$N" "$D" "(${branch:0:10})" "$N"
  fi

  [[ "$uncommitted" -gt 0 ]] && printf '  %s%d geändert%s' "$Y" "$uncommitted" "$N" \
                               || printf '  %s✓ sauber%s' "$G" "$N"
  [[ "$untracked" -gt 0 ]]   && printf '  %s%d untracked%s' "$D" "$untracked" "$N"
  [[ "$unpushed" =~ ^[1-9] ]] && printf '  %s↑%s unpushed%s' "$R" "$unpushed" "$N"
  printf '\n'

done < <(find "$SEARCH_DIR" -maxdepth "$MAX_DEPTH" -name ".git" -type d 2>/dev/null | sort)

echo
printf '%s%d Repos gefunden%s — %s%d sauber%s · %s%d mit Änderungen%s\n' \
  "$B" "$total" "$N" "$G" "$clean" "$N" "$Y" "$dirty" "$N"
[[ $dirty -gt 0 ]] && echo "Tipp: Details mit ${C}git -C <pfad> status${N} ansehen."
exit 0