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.

Revisions

  1. patkepa created this gist Jun 30, 2025.
    146 changes: 146 additions & 0 deletions git-submodule-tree.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,146 @@
    #!/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}"