From cd1b681657917feae5e48e87faf8c553786da5ae Mon Sep 17 00:00:00 2001 From: Mark Randall Havens Date: Wed, 11 Jun 2025 19:40:20 -0500 Subject: [PATCH] rad-info.sh mostly working now --- bin/rad-info.sh | 485 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 397 insertions(+), 88 deletions(-) diff --git a/bin/rad-info.sh b/bin/rad-info.sh index 9bdec71..e802990 100755 --- a/bin/rad-info.sh +++ b/bin/rad-info.sh @@ -1,104 +1,413 @@ #!/bin/bash -# Exit on error -set -e +# rad-info.sh: Retrieve repository information for Radicle or centralized Git hosting. +# Complies with RIGOR principles: Reproducible, Interoperable, Generalizable, Open, Robust. +# Dependencies: git, rad (Radicle CLI). Optional: curl, jq (for centralized Git or JSON output). +# Version: 1.6.0 +# License: MIT -# Check if we're in a Git repository -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "Error: Not inside a Git repository" - exit 1 -fi +# Default settings +REMOTE_NAME="" +OUTPUT_FORMAT="text" +DETAILED_MODE=false +ALL_REMOTES=false +VERBOSE=false +CONFIG_FILE="$HOME/.rad-info.rc" +PLUGIN_DIR="$HOME/.rad-info/plugins" -# Check for required tools -for cmd in git rad jq; do - if ! command -v "$cmd" >/dev/null 2>&1; then - echo "Error: $cmd is required but not installed" - exit 1 +# Function to check for tools +check_tools() { + local tools=("$@") + for cmd in "${tools[@]}"; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Error: $cmd is required but not installed" >&2 + return 1 + fi + done +} + +# Function to log verbose output +log_verbose() { + [ "$VERBOSE" = true ] && echo "[DEBUG] $*" >&2 +} + +# Function to find Radicle remote +find_radicle_remote() { + local remote + remote=$(git remote -v | grep 'rad://' | awk '{print $1}' | sort -u | head -n1 2>/dev/null || echo "") + log_verbose "Found Radicle remote: $remote (exit code: $?)" + echo "$remote" +} + +# Function to check Radicle version +check_rad_version() { + if ! check_tools rad; then + echo "Error: Radicle CLI (rad) is not installed" >&2 + return 1 fi + local version + version=$(rad --version | awk '{print $2}' 2>/dev/null || echo "unknown") + log_verbose "Radicle CLI version: $version (exit code: $?)" + if [[ "$version" == "unknown" || ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Warning: Could not determine Radicle CLI version. Some features may not work." >&2 + fi + echo "$version" +} + +# Parse command-line options +while getopts "r:jdav" opt; do + case $opt in + r) REMOTE_NAME="$OPTARG" ;; + j) OUTPUT_FORMAT="json" ;; + d) DETAILED_MODE=true ;; + a) ALL_REMOTES=true ;; + v) VERBOSE=true ;; + *) echo "Usage: $0 [-r remote] [-j] [-d] [-a] [-v]" + echo " -r: Specify remote name (default: auto-detect)" + echo " -j: Output in JSON format" + echo " -d: Include detailed Radicle info (peers, issues, patches, identity revisions)" + echo " -a: Summarize all remotes" + echo " -v: Enable verbose logging" + exit 0 ;; + esac done -# Ensure rad is initialized for the repository -if ! rad inspect >/dev/null 2>&1; then - echo "Error: This repository is not initialized with Radicle. Run 'rad init' first." +# Load config file if exists +if [ -f "$CONFIG_FILE" ]; then + log_verbose "Loading config from $CONFIG_FILE" + source "$CONFIG_FILE" +fi + +# Load plugins if directory exists +if [ -d "$PLUGIN_DIR" ]; then + for plugin in "$PLUGIN_DIR"/*.sh; do + if [ -f "$plugin" ]; then + log_verbose "Loading plugin: $plugin" + source "$plugin" + fi + done +fi + +# Check if in a Git repository +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "Error: Not inside a Git repository" >&2 exit 1 fi -# Get repository details using rad commands -REPO_RID=$(rad inspect --rid 2>/dev/null | grep -o 'rad:[a-zA-Z0-9]\+' || echo "N/A") -if [ "$REPO_RID" == "N/A" ]; then - echo "Error: Could not retrieve Repository ID (RID)" - exit 1 +# Check core tools +check_tools git || exit 1 + +# Initialize variables +RID="" +NODE_ID="" +FULL_NAME="" +DEFAULT_BRANCH="" +CURRENT_BRANCH="" +CURRENT_COMMIT="" +REPO_STATUS="" +HOST="" +SEED_NODES="" +VISIBILITY="" +PEERS="" +ISSUES="" +PATCHES="" +IDENTITY_REVISIONS="" +REMOTES=() + +# Function to get local Git details +get_local_git_details() { + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "N/A") + CURRENT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "N/A") + local status_count + status_count=$(git status --porcelain 2>/dev/null | wc -l | xargs) + REPO_STATUS=$([ "$status_count" -eq 0 ] && echo "Clean" || echo "Dirty ($status_count uncommitted changes)") + DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main") + log_verbose "Local Git details: branch=$CURRENT_BRANCH, commit=$CURRENT_COMMIT, status=$REPO_STATUS, default=$DEFAULT_BRANCH (exit code: $?)" +} + +# Function to handle Radicle repositories +handle_radicle() { + check_rad_version >/dev/null || return 1 + + # Check Radicle node status and connectivity + local node_status node_stderr + node_status=$(rad node status 2>/tmp/rad_node_stderr) || { + node_stderr=$(cat /tmp/rad_node_stderr) + log_verbose "rad node status failed: $node_stderr (exit code: $?)" + echo "Error: Radicle node is not running. Start it with 'rad node start'" >&2 + return 1 + } + log_verbose "Node status: $node_status (exit code: $?)" + local peer_count + peer_count=$(echo "$node_status" | grep -c 'connected' || echo "0") + if [ "$peer_count" -eq 0 ]; then + echo "Warning: Radicle node is running but not connected to peers. Check seed node configuration." >&2 + fi + log_verbose "Connected peers: $peer_count" + + # Check repository initialization + local payload_output payload_stderr + payload_output=$(rad inspect --payload 2>/tmp/rad_payload_stderr) || { + payload_stderr=$(cat /tmp/rad_payload_stderr) + log_verbose "rad inspect --payload failed: $payload_stderr (exit code: $?)" + echo "Warning: Repository not fully initialized with Radicle. Run 'rad init' to set metadata." >&2 + } + log_verbose "rad inspect --payload output: $payload_output (exit code: $?)" + + # Get RID + local inspect_output inspect_stderr + inspect_output=$(rad inspect 2>/tmp/rad_inspect_stderr) || { + inspect_stderr=$(cat /tmp/rad_inspect_stderr) + log_verbose "rad inspect failed: $inspect_stderr (exit code: $?)" + echo "Warning: Not a Radicle repository. Falling back to Git details." >&2 + return 1 + } + RID=$(echo "$inspect_output" | grep -o 'rad:[^ ]*' || echo "") + [ -z "$RID" ] && { echo "Error: Could not retrieve Radicle Repository ID (RID)" >&2; return 1; } + log_verbose "RID: $RID (exit code: $?)" + + # Get Node ID + local self_output self_stderr + self_output=$(rad self 2>/tmp/rad_self_stderr || echo "") + self_stderr=$(cat /tmp/rad_self_stderr) + NODE_ID=$(echo "$self_output" | grep 'Node ID (NID)' | awk '{print $NF}' || echo "N/A") + log_verbose "Node ID: $NODE_ID, stderr: $self_stderr (exit code: $?)" + + # Get repository metadata from payload + if [ -n "$payload_output" ] && check_tools jq; then + FULL_NAME=$(echo "$payload_output" | jq -r '.["xyz.radicle.project"].name // "N/A"' 2>/dev/null || echo "N/A") + VISIBILITY=$(echo "$payload_output" | jq -r '.visibility // "unknown"' 2>/dev/null || echo "unknown") + DEFAULT_BRANCH=$(echo "$payload_output" | jq -r '.["xyz.radicle.project"].default_branch // "main"' 2>/dev/null || echo "main") + else + FULL_NAME=$(git config rad.name 2>/dev/null || basename "$(git rev-parse --show-toplevel)" 2>/dev/null || echo "N/A") + VISIBILITY="unknown" + DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/rad/HEAD 2>/dev/null | sed 's@^refs/remotes/rad/@@' || echo "main") + echo "Warning: Repository metadata not found or jq not installed. Using directory name: $FULL_NAME. Run 'rad id update --payload xyz.radicle.project name \"\"' to set a name." >&2 + fi + if [ "$FULL_NAME" = "N/A" ]; then + FULL_NAME=$(basename "$(git rev-parse --show-toplevel)" 2>/dev/null || echo "N/A") + echo "Warning: Repository name not found. Using directory name: $FULL_NAME. Run 'rad id update --payload xyz.radicle.project name \"\"' to set a name." >&2 + fi + log_verbose "Full Name: $FULL_NAME, Visibility: $VISIBILITY, Default Branch: $DEFAULT_BRANCH" + + # Get seed nodes + local config_output config_stderr + config_output=$(rad config 2>/tmp/rad_config_stderr || echo "") + config_stderr=$(cat /tmp/rad_config_stderr) + log_verbose "rad config output: $config_output, stderr: $config_stderr (exit code: $?)" + if [ -n "$config_output" ] && echo "$config_output" | grep -q '{'; then + if check_tools jq; then + SEED_NODES=$(echo "$config_output" | jq -r '.preferredSeeds[]' 2>/dev/null | paste -sd, - || echo "N/A") + else + SEED_NODES=$(echo "$config_output" | grep -oE '[^ ]+@[^ ]+:8776' | paste -sd, - || echo "N/A") + echo "Warning: jq not installed; using fallback for seed nodes parsing" >&2 + fi + else + SEED_NODES="N/A" + echo "Warning: Failed to parse rad config for seed nodes" >&2 + fi + log_verbose "Seed Nodes: $SEED_NODES" + + # Detailed mode: peers, issues, patches, identity revisions + if [ "$DETAILED_MODE" = true ]; then + PEERS=$(rad peer ls 2>/dev/null | wc -l | xargs || echo "N/A") + ISSUES=$(rad issue ls 2>/dev/null | wc -l | xargs || echo "N/A") + PATCHES=$(rad patch ls 2>/dev/null | wc -l | xargs || echo "N/A") + IDENTITY_REVISIONS=$(rad id list 2>/dev/null | grep -c 'accepted' || echo "N/A") + log_verbose "Peers: $PEERS, Issues: $ISSUES, Patches: $PATCHES, Identity Revisions: $IDENTITY_REVISIONS (exit code: $?)" + fi + + HOST="radicle" + get_local_git_details +} + +# Function to handle centralized Git +handle_centralized() { + local remote_url=$1 + if [[ "$remote_url" =~ ^rad:// ]]; then + echo "Error: Radicle URL ($remote_url) cannot be processed as centralized Git. Ensure repository is initialized with 'rad init'." >&2 + return 1 + fi + local parsed_url="$remote_url" + if [[ "$remote_url" =~ ^git@ ]]; then + parsed_url=$(echo "$remote_url" | sed -E 's/git@([^:]+):(.+)\.git$/https:\/\/\1\/\2/') + fi + + if [[ "$parsed_url" =~ https?://([^/]+)/([^/]+)/([^/]+) ]]; then + HOST="${BASH_REMATCH[1]}" + FULL_NAME="${BASH_REMATCH[2]}/${BASH_REMATCH[3]%.git}" + else + echo "Error: Could not parse remote URL: $remote_url" >&2 + return 1 + fi + log_verbose "Centralized Git: Host=$HOST, Full Name=$FULL_NAME (exit code: $?)" + + # Optional API-based metadata with retry + if check_tools curl jq; then + local api_url="" + local curl_opts=(--silent --fail) + if [ -n "$GITHUB_TOKEN" ] && [ "$HOST" = "github.com" ]; then + curl_opts+=(--header "Authorization: token $GITHUB_TOKEN") + fi + case "$HOST" in + github.com) + api_url="https://api.github.com/repos/$FULL_NAME" + ;; + gitlab.com) + api_url="https://gitlab.com/api/v4/projects/$(echo -n "$FULL_NAME" | xxd -p | tr -d '\n')" + ;; + bitbucket.org) + api_url="https://api.bitbucket.org/2.0/repositories/$FULL_NAME" + ;; + esac + if [ -n "$api_url" ]; then + local repo_details="" + for attempt in 1 2 3; do + repo_details=$(curl "${curl_opts[@]}" "$api_url" 2>/dev/null || echo "") + log_verbose "API attempt $attempt response: $repo_details (exit code: $?)" + [ -n "$repo_details" ] && break + sleep 1 + done + if [ -n "$repo_details" ]; then + FULL_NAME=$(echo "$repo_details" | jq -r '.full_name // .path_with_namespace // ""' || echo "$FULL_NAME") + DEFAULT_BRANCH=$(echo "$repo_details" | jq -r '.default_branch // .mainbranch.name // "main"' || echo "main") + else + echo "Warning: Failed to fetch API metadata for $HOST after retries" >&2 + fi + fi + else + echo "Warning: curl or jq not installed; skipping API metadata for $HOST" >&2 + fi + + get_local_git_details +} + +# Function to summarize all remotes +summarize_all_remotes() { + local remotes + mapfile -t remotes < <(git remote) + REMOTES=() + for remote in "${remotes[@]}"; do + local url + url=$(git remote get-url "$remote" 2>/dev/null || echo "N/A") + local type="centralized" + [[ "$url" =~ ^rad:// ]] && type="radicle" + REMOTES+=("$remote: $type ($url)") + log_verbose "Remote: $remote, Type: $type, URL: $url (exit code: $?)" + done +} + +# Main logic +if [ "$ALL_REMOTES" = true ]; then + summarize_all_remotes +elif [ -z "$REMOTE_NAME" ]; then + REMOTE_NAME=$(find_radicle_remote) + if [ -z "$REMOTE_NAME" ] && rad inspect >/dev/null 2>&1; then + REMOTE_NAME="rad" + fi fi -# Get repository name and visibility -REPO_INFO=$(rad inspect --json 2>/dev/null | jq -r '.name, .visibility.type' || echo "N/A N/A") -read REPO_NAME VISIBILITY <<< "$REPO_INFO" -if [ "$REPO_NAME" == "N/A" ]; then - echo "Error: Could not retrieve repository name" - exit 1 -fi - -# Get user identity (DID and NID) -USER_INFO=$(rad self --json 2>/dev/null | jq -r '.did, .nid' || echo "N/A N/A") -read DID NID <<< "$USER_INFO" -if [ "$DID" == "N/A" ]; then - echo "Error: Could not retrieve user identity (DID)" - exit 1 -fi - -# Get preferred seed nodes from config -PREFERRED_SEEDS=$(rad config --json 2>/dev/null | jq -r '.preferredSeeds[]' | tr '\n' ',' | sed 's/,$//') -if [ -z "$PREFERRED_SEEDS" ]; then - PREFERRED_SEEDS="None configured" -fi - -# Get local repository details -DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main") -CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "N/A") -CURRENT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "N/A") -REPO_STATUS=$(git status --porcelain 2>/dev/null | wc -l | xargs) -if [ "$REPO_STATUS" -eq 0 ]; then - REPO_STATUS="Clean" +if [ -n "$REMOTE_NAME" ]; then + REMOTE_URL=$(git remote get-url "$REMOTE_NAME" 2>/dev/null || echo "") + log_verbose "Processing remote: $REMOTE_NAME ($REMOTE_URL) (exit code: $?)" + if [[ "$REMOTE_URL" =~ ^rad:// ]] || rad inspect >/dev/null 2>&1; then + handle_radicle || { + echo "Warning: Failed to process Radicle repository. Ensure 'rad init' has been run and metadata is set with 'rad id update'." >&2 + return 1 + } + else + handle_centralized "$REMOTE_URL" || { + echo "Warning: Failed to process centralized Git repository." >&2 + return 1 + } + fi else - REPO_STATUS="Dirty ($REPO_STATUS uncommitted changes)" -fi - -# Get sync status (peers seeding the repo) -SYNC_STATUS=$(rad sync status --json 2>/dev/null | jq -r '.peers[]?.node' | tr '\n' ',' | sed 's/,$//' || echo "No peers found") -if [ -z "$SYNC_STATUS" ]; then - SYNC_STATUS="No peers found" -fi - -# Get web view URL (if public) -if [ "$VISIBILITY" == "public" ]; then - PUBLIC_EXPLORER=$(rad config --json | jq -r '.publicExplorer' | sed "s@\\\$host@seed.radicle.garden@g;s@\\\$rid@$REPO_RID@g;s@\\\$path@@g") -else - PUBLIC_EXPLORER="N/A (Private repository)" + echo "Warning: No Radicle remote found. Processing as centralized Git or use -r to specify." >&2 + REMOTE_NAME="origin" + REMOTE_URL=$(git remote get-url "$REMOTE_NAME" 2>/dev/null || echo "") + [ -z "$REMOTE_URL" ] && { echo "Error: No remote URL found" >&2; exit 1; } + handle_centralized "$REMOTE_URL" || { + echo "Warning: Failed to process centralized Git repository." >&2 + return 1 + } fi # Output repository information -cat <&2 + OUTPUT_FORMAT="text" + else + jq -n --arg host "$HOST" \ + --arg full_name "$FULL_NAME" \ + --arg rid "$RID" \ + --arg node_id "$NODE_ID" \ + --arg default_branch "$DEFAULT_BRANCH" \ + --arg current_branch "$CURRENT_BRANCH" \ + --arg current_commit "$CURRENT_COMMIT" \ + --arg repo_status "$REPO_STATUS" \ + --arg seed_nodes "$SEED_NODES" \ + --arg visibility "$VISIBILITY" \ + --arg peers "$PEERS" \ + --arg issues "$ISSUES" \ + --arg patches "$PATCHES" \ + --arg identity_revisions "$IDENTITY_REVISIONS" \ + --argjson remotes "$(printf '%s\n' "${REMOTES[@]}" | jq -R . | jq -s .)" \ + '{ + hosting_service: $host, + full_name: $full_name, + repo_id: $rid, + node_id: $node_id, + default_branch: $default_branch, + current_branch: $current_branch, + current_commit: $current_commit, + repo_status: $repo_status, + seed_nodes: $seed_nodes, + visibility: $visibility, + peers: $peers, + issues: $issues, + patches: $patches, + identity_revisions: $identity_revisions, + remotes: $remotes + }' + fi +fi + +if [ "$OUTPUT_FORMAT" = "text" ]; then + echo + echo "Repository Information:" + echo "----------------------" + echo "Hosting Service: $HOST" + echo "Full Name: ${FULL_NAME:-N/A}" + echo "Repository ID: ${RID:-N/A}" + echo "Node ID: ${NODE_ID:-N/A}" + echo "Default Branch: $DEFAULT_BRANCH" + echo "Current Branch: $CURRENT_BRANCH" + echo "Current Commit: $CURRENT_COMMIT" + echo "Repository Status: $REPO_STATUS" + [ -n "$SEED_NODES" ] && echo "Seed Nodes: $SEED_NODES" + [ -n "$VISIBILITY" ] && echo "Visibility: $VISIBILITY" + [ "$DETAILED_MODE" = true ] && [ -n "$PEERS" ] && echo "Peers: $PEERS" + [ "$DETAILED_MODE" = true ] && [ -n "$ISSUES" ] && echo "Issues: $ISSUES" + [ "$DETAILED_MODE" = true ] && [ -n "$PATCHES" ] && echo "Patches: $PATCHES" + [ "$DETAILED_MODE" = true ] && [ -n "$IDENTITY_REVISIONS" ] && echo "Accepted Identity Revisions: $IDENTITY_REVISIONS" + if [ "$ALL_REMOTES" = true ]; then + echo "Remotes:" + for remote in "${REMOTES[@]}"; do + echo " $remote" + done + fi + echo "----------------------" + if [ "$HOST" = "radicle" ]; then + echo "Clone Command: rad clone $RID" + echo "Sync Command: rad sync" + echo "Note: Ensure Radicle node is running ('rad node start') and configured with seed nodes." + echo "See 'rad help' for more commands and https://radicle.xyz for documentation." + else + echo "Clone Command: git clone $REMOTE_URL" + echo "Sync Command: git fetch $REMOTE_NAME && git pull $REMOTE_NAME $DEFAULT_BRANCH" + fi +fi + +# Clean up temporary files +rm -f /tmp/rad_*_stderr