rad-info.sh mostly working now

This commit is contained in:
Mark Randall Havens 2025-06-11 19:40:20 -05:00
parent ead9aa61de
commit cd1b681657

View file

@ -1,104 +1,413 @@
#!/bin/bash #!/bin/bash
# Exit on error # rad-info.sh: Retrieve repository information for Radicle or centralized Git hosting.
set -e # 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 # Default settings
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then REMOTE_NAME=""
echo "Error: Not inside a Git repository" OUTPUT_FORMAT="text"
exit 1 DETAILED_MODE=false
fi ALL_REMOTES=false
VERBOSE=false
CONFIG_FILE="$HOME/.rad-info.rc"
PLUGIN_DIR="$HOME/.rad-info/plugins"
# Check for required tools # Function to check for tools
for cmd in git rad jq; do check_tools() {
local tools=("$@")
for cmd in "${tools[@]}"; do
if ! command -v "$cmd" >/dev/null 2>&1; then if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Error: $cmd is required but not installed" echo "Error: $cmd is required but not installed" >&2
exit 1 return 1
fi fi
done done
}
# Ensure rad is initialized for the repository # Function to log verbose output
if ! rad inspect >/dev/null 2>&1; then log_verbose() {
echo "Error: This repository is not initialized with Radicle. Run 'rad init' first." [ "$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
# 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 exit 1
fi fi
# Get repository details using rad commands # Check core tools
REPO_RID=$(rad inspect --rid 2>/dev/null | grep -o 'rad:[a-zA-Z0-9]\+' || echo "N/A") check_tools git || exit 1
if [ "$REPO_RID" == "N/A" ]; then
echo "Error: Could not retrieve Repository ID (RID)"
exit 1
fi
# Get repository name and visibility # Initialize variables
REPO_INFO=$(rad inspect --json 2>/dev/null | jq -r '.name, .visibility.type' || echo "N/A N/A") RID=""
read REPO_NAME VISIBILITY <<< "$REPO_INFO" NODE_ID=""
if [ "$REPO_NAME" == "N/A" ]; then FULL_NAME=""
echo "Error: Could not retrieve repository name" DEFAULT_BRANCH=""
exit 1 CURRENT_BRANCH=""
fi CURRENT_COMMIT=""
REPO_STATUS=""
HOST=""
SEED_NODES=""
VISIBILITY=""
PEERS=""
ISSUES=""
PATCHES=""
IDENTITY_REVISIONS=""
REMOTES=()
# Get user identity (DID and NID) # Function to get local Git details
USER_INFO=$(rad self --json 2>/dev/null | jq -r '.did, .nid' || echo "N/A N/A") get_local_git_details() {
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_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") CURRENT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "N/A")
REPO_STATUS=$(git status --porcelain 2>/dev/null | wc -l | xargs) local status_count
if [ "$REPO_STATUS" -eq 0 ]; then status_count=$(git status --porcelain 2>/dev/null | wc -l | xargs)
REPO_STATUS="Clean" 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 else
REPO_STATUS="Dirty ($REPO_STATUS uncommitted changes)" 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 \"<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 \"<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 fi
# Get sync status (peers seeding the repo) HOST="radicle"
SYNC_STATUS=$(rad sync status --json 2>/dev/null | jq -r '.peers[]?.node' | tr '\n' ',' | sed 's/,$//' || echo "No peers found") get_local_git_details
if [ -z "$SYNC_STATUS" ]; then }
SYNC_STATUS="No peers found"
# 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 fi
# Get web view URL (if public) if [[ "$parsed_url" =~ https?://([^/]+)/([^/]+)/([^/]+) ]]; then
if [ "$VISIBILITY" == "public" ]; then HOST="${BASH_REMATCH[1]}"
PUBLIC_EXPLORER=$(rad config --json | jq -r '.publicExplorer' | sed "s@\\\$host@seed.radicle.garden@g;s@\\\$rid@$REPO_RID@g;s@\\\$path@@g") FULL_NAME="${BASH_REMATCH[2]}/${BASH_REMATCH[3]%.git}"
else else
PUBLIC_EXPLORER="N/A (Private repository)" 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
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
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 fi
# Output repository information # Output repository information
cat <<EOF if [ "$OUTPUT_FORMAT" = "json" ]; then
Radicle Repository Information: if ! check_tools jq; then
------------------------------ echo "Error: jq is required for JSON output. Falling back to text output." >&2
Repository Name: $REPO_NAME OUTPUT_FORMAT="text"
Repository ID (RID): $REPO_RID else
Visibility: $VISIBILITY jq -n --arg host "$HOST" \
User DID: $DID --arg full_name "$FULL_NAME" \
Node ID (NID): $NID --arg rid "$RID" \
Preferred Seed Nodes: $PREFERRED_SEEDS --arg node_id "$NODE_ID" \
Web View URL: $PUBLIC_EXPLORER --arg default_branch "$DEFAULT_BRANCH" \
Default Branch: $DEFAULT_BRANCH --arg current_branch "$CURRENT_BRANCH" \
Current Branch: $CURRENT_BRANCH --arg current_commit "$CURRENT_COMMIT" \
Current Commit: $CURRENT_COMMIT --arg repo_status "$REPO_STATUS" \
Repository Status: $REPO_STATUS --arg seed_nodes "$SEED_NODES" \
Seeding Peers: $SYNC_STATUS --arg visibility "$VISIBILITY" \
------------------------------ --arg peers "$PEERS" \
Clone Command: rad clone $REPO_RID --arg issues "$ISSUES" \
Sync Command: rad sync $REPO_RID --arg patches "$PATCHES" \
Initialize Command (if not cloned): rad init --name $REPO_NAME --default-branch $DEFAULT_BRANCH --visibility $VISIBILITY --arg identity_revisions "$IDENTITY_REVISIONS" \
------------------------------ --argjson remotes "$(printf '%s\n' "${REMOTES[@]}" | jq -R . | jq -s .)" \
Notes: '{
- To clone, ensure Radicle is installed (see https://radicle.xyz). hosting_service: $host,
- For private repositories, ensure you have the necessary cryptographic keys. full_name: $full_name,
- Syncing requires at least one seeding peer to be online. repo_id: $rid,
EOF 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