Skip to content

Instantly share code, notes, and snippets.

@patkepa
Created June 30, 2025 10:22
Show Gist options
  • Select an option

  • Save patkepa/8a014cf9ee7d935261c27fd9f100b79b to your computer and use it in GitHub Desktop.

Select an option

Save patkepa/8a014cf9ee7d935261c27fd9f100b79b to your computer and use it in GitHub Desktop.
Shorts tree like structure for submodules in repositories.
#!/bin/bash
# Git Submodule Dependency Tree Generator
# Usage: ./git-submodule-tree.sh <github-repo-url>
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print usage
usage() {
echo "Usage: $0 <github-repo-url>"
echo "Example: $0 https://github.com/user/repo.git"
exit 1
}
# Check if URL is provided
if [ $# -eq 0 ]; then
usage
fi
REPO_URL="$1"
TEMP_DIR=$(mktemp -d)
VISITED_REPOS=()
# Clean up on exit
cleanup() {
rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
# Function to check if repo was already visited
is_visited() {
local repo="$1"
for visited in "${VISITED_REPOS[@]}"; do
if [ "$visited" == "$repo" ]; then
return 0
fi
done
return 1
}
# Function to extract repo info from .gitmodules
parse_gitmodules() {
local gitmodules_file="$1"
local indent="$2"
if [ ! -f "$gitmodules_file" ]; then
return
fi
# Parse .gitmodules file
while IFS= read -r line; do
if [[ $line =~ ^\[submodule[[:space:]]+\"(.+)\"\] ]]; then
local name="${BASH_REMATCH[1]}"
local path=""
local url=""
local branch=""
# Read submodule details
while IFS= read -r subline && ! [[ $subline =~ ^\[submodule ]]; do
if [[ $subline =~ path[[:space:]]*=[[:space:]]*(.+) ]]; then
path="${BASH_REMATCH[1]}"
elif [[ $subline =~ url[[:space:]]*=[[:space:]]*(.+) ]]; then
url="${BASH_REMATCH[1]}"
elif [[ $subline =~ branch[[:space:]]*=[[:space:]]*(.+) ]]; then
branch="${BASH_REMATCH[1]}"
fi
done
if [ -n "$url" ]; then
# Print submodule info
echo -e "${indent}├── ${GREEN}${name}${NC}"
echo -e "${indent}│ └── ${BLUE}${url}${NC}"
# Check for commit hash or branch
if [ -n "$path" ] && [ -d "$(dirname "$gitmodules_file")/$path" ]; then
cd "$(dirname "$gitmodules_file")/$path" 2>/dev/null || true
local commit=$(git rev-parse HEAD 2>/dev/null || echo "")
if [ -n "$commit" ]; then
echo -e "${indent}│ └── ${YELLOW}commit: ${commit:0:8}${NC}"
fi
if [ -n "$branch" ]; then
echo -e "${indent}│ └── ${YELLOW}branch: ${branch}${NC}"
fi
cd - > /dev/null 2>&1 || true
fi
# Mark as visited
VISITED_REPOS+=("$url")
# Recursively process submodule if not already visited
if ! is_visited "$url"; then
process_repo "$url" "${indent}│ "
fi
fi
fi
done < "$gitmodules_file"
}
# Function to process a repository
process_repo() {
local repo_url="$1"
local indent="${2:-}"
# Skip if already visited
if is_visited "$repo_url"; then
return
fi
# Create temporary directory for this repo
local repo_dir=$(mktemp -d -p "$TEMP_DIR")
# Clone the repository (shallow clone for speed)
echo -e "${indent}${RED}Cloning: ${repo_url}${NC}" >&2
if ! git clone --depth 1 --recurse-submodules "$repo_url" "$repo_dir" 2>/dev/null; then
echo -e "${indent}${RED}Failed to clone: ${repo_url}${NC}" >&2
return
fi
# Process .gitmodules if it exists
if [ -f "$repo_dir/.gitmodules" ]; then
parse_gitmodules "$repo_dir/.gitmodules" "$indent"
fi
# Clean up this repo
rm -rf "$repo_dir"
}
# Main execution
echo -e "${GREEN}Git Submodule Dependency Tree${NC}"
echo -e "${GREEN}==============================${NC}"
echo -e "${BLUE}${REPO_URL}${NC}"
# Mark main repo as visited
VISITED_REPOS+=("$REPO_URL")
# Process the main repository
process_repo "$REPO_URL" ""
echo -e "\n${GREEN}Complete!${NC}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment