gitfield-local
1 #!/bin/bash 2 set -euo pipefail 3 IFS=$'\n\t' 4 5 # ╭─────────────────────────────────────╮ 6 # │ gitfield-local │ 7 # ╰─────────────────────────────────────╯ 8 # Manages a local bare Git repository as a sacred push target for redundancy. 9 # Creates and maintains a bare repository in ~/git-local-repos/git-sigil.git 10 # Generates rich metadata in .gitfield/local.sigil.md and updates .gitfield/push_log.json 11 # Commands: configure, status, push 12 13 # ╭─────────────────────────────────────╮ 14 # │ CONFIGURATION │ 15 # ╰─────────────────────────────────────╯ 16 GIT_REMOTE_NAME="local" 17 REPO_NAME=$(basename "$(pwd)") || REPO_NAME="Unknown" 18 DEFAULT_NAME="Mark Randall Havens" 19 DEFAULT_EMAIL="mark.r.havens@gmail.com" 20 LOCAL_REPO="$HOME/git-local-repos/git-sigil.git" 21 REPO_PATH=$(git rev-parse --show-toplevel 2>/dev/null) || { echo -e "\e[1;31m[ERROR]\e[0m Not inside a Git repository" >&2; exit 1; } 22 MARKDOWN_FILE="$REPO_PATH/.gitfield/local.sigil.md" 23 PUSH_LOG="$REPO_PATH/.gitfield/push_log.json" 24 SCRIPT_VERSION="1.0" 25 26 # ╭─────────────────────────────────────╮ 27 # │ LOGGING UTILS │ 28 # ╰─────────────────────────────────────╯ 29 info() { echo -e "\e[1;34m[INFO]\e[0m $*"; } 30 warn() { echo -e "\e[1;33m[WARN]\e[0m $*"; } 31 error() { echo -e "\e[1;31m[ERROR]\e[0m $*" >&2; exit 1; } 32 33 # ╭─────────────────────────────────────╮ 34 # │ TOOLCHAIN SETUP │ 35 # ╰─────────────────────────────────────╯ 36 info "Checking for required tools..." 37 if ! command -v git &>/dev/null; then 38 info "Installing Git..." 39 sudo apt update -qq 2>/dev/null || warn "apt update failed, continuing..." 40 sudo apt install -y git 2>/dev/null || error "Git install failed" 41 fi 42 43 if ! command -v jq &>/dev/null; then 44 info "Installing jq for JSON processing..." 45 sudo apt install -y jq 2>/dev/null || warn "jq install failed, push_log.json updates may fail" 46 fi 47 48 # ╭─────────────────────────────────────╮ 49 # │ AUTH + IDENTITY │ 50 # ╰─────────────────────────────────────╯ 51 info "Setting Git identity..." 52 git config --global user.name "$DEFAULT_NAME" 2>/dev/null || warn "Failed to set git user name" 53 git config --global user.email "$DEFAULT_EMAIL" 2>/dev/null || warn "Failed to set git user email" 54 info "Git identity set to: $DEFAULT_NAME <$DEFAULT_EMAIL>" 55 56 # ╭─────────────────────────────────────╮ 57 # │ GIT INIT (IF NEEDED) │ 58 # ╰─────────────────────────────────────╯ 59 if [ ! -d "$REPO_PATH/.git" ]; then 60 info "Initializing Git repository..." 61 git -C "$REPO_PATH" init 2>/dev/null || warn "Git init failed, continuing..." 62 git -C "$REPO_PATH" add . 2>/dev/null || warn "Nothing to add" 63 git -C "$REPO_PATH" commit -m "Initial commit" 2>/dev/null || warn "Nothing to commit" 64 fi 65 66 # ╭─────────────────────────────────────╮ 67 # │ LOCAL REPO CONFIGURATION │ 68 # ╰─────────────────────────────────────╯ 69 configure() { 70 info "Configuring local bare repository..." 71 72 # Create and verify local bare repository 73 if [[ ! -d "$LOCAL_REPO" ]]; then 74 info "Creating local bare repository: $LOCAL_REPO" 75 mkdir -p "$LOCAL_REPO" || error "Failed to create $LOCAL_REPO" 76 git -C "$LOCAL_REPO" init --bare 2>/dev/null || error "Failed to initialize bare repository" 77 fi 78 79 if ! git -C "$LOCAL_REPO" rev-parse --is-bare-repository >/dev/null 2>&1; then 80 warn "Local repository $LOCAL_REPO is not a valid bare repository. Reinitializing..." 81 rm -rf "$LOCAL_REPO" || error "Failed to remove invalid $LOCAL_REPO" 82 mkdir -p "$LOCAL_REPO" || error "Failed to create $LOCAL_REPO" 83 git -C "$LOCAL_REPO" init --bare 2>/dev/null || error "Failed to reinitialize bare repository" 84 fi 85 86 # Set permissions 87 chmod -R u+rwX "$LOCAL_REPO" 2>/dev/null || warn "Failed to set permissions on $LOCAL_REPO" 88 89 # Configure local remote 90 REMOTE_URL="file://$LOCAL_REPO" 91 if ! git -C "$REPO_PATH" remote get-url "$GIT_REMOTE_NAME" &>/dev/null; then 92 info "Adding local remote: $REMOTE_URL" 93 git -C "$REPO_PATH" remote add "$GIT_REMOTE_NAME" "$REMOTE_URL" 2>/dev/null || error "Failed to add local remote" 94 else 95 current_url=$(git -C "$REPO_PATH" remote get-url "$GIT_REMOTE_NAME") 96 if [[ "$current_url" != "$REMOTE_URL" ]]; then 97 warn "Local remote URL is incorrect ($current_url). Updating to $REMOTE_URL" 98 git -C "$REPO_PATH" remote set-url "$GIT_REMOTE_NAME" "$REMOTE_URL" 2>/dev/null || error "Failed to update local remote URL" 99 fi 100 fi 101 102 # Set upstream for current branch 103 DEFAULT_BRANCH=$(git -C "$REPO_PATH" symbolic-ref --short HEAD 2>/dev/null || echo "main") 104 if ! git -C "$REPO_PATH" rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then 105 info "Setting upstream for $DEFAULT_BRANCH to $GIT_REMOTE_NAME/$DEFAULT_BRANCH" 106 git -C "$REPO_PATH" push --set-upstream "$GIT_REMOTE_NAME" "$DEFAULT_BRANCH" 2>/dev/null || error "Failed to set upstream" 107 fi 108 109 info "Local bare repository configured successfully." 110 } 111 112 # ╭─────────────────────────────────────╮ 113 # │ STATUS CHECK │ 114 # ╰─────────────────────────────────────╯ 115 status() { 116 info "Checking local repository status..." 117 118 # Verify local bare repository 119 if [[ -d "$LOCAL_REPO" && $(git -C "$LOCAL_REPO" rev-parse --is-bare-repository 2>/dev/null) == "true" ]]; then 120 info "Local bare repository: $LOCAL_REPO" 121 latest_commit=$(git -C "$LOCAL_REPO" log -1 --format="%h %s (%cr)" 2>/dev/null || echo "No commits") 122 info "Latest commit: $latest_commit" 123 else 124 warn "Local bare repository not found or invalid: $LOCAL_REPO" 125 fi 126 127 # Check remote configuration 128 if git -C "$REPO_PATH" remote | grep -q "^$GIT_REMOTE_NAME$"; then 129 remote_url=$(git -C "$REPO_PATH" remote get-url "$GIT_REMOTE_NAME") 130 info "Local remote URL: $remote_url" 131 else 132 warn "Local remote not configured." 133 fi 134 135 # Check working repository status 136 info "Working repository: $REPO_PATH" 137 git -C "$REPO_PATH" status --short 2>/dev/null || warn "Failed to get repository status" 138 } 139 140 # ╭─────────────────────────────────────╮ 141 # │ GIT METADATA SNAPSHOT │ 142 # ╰─────────────────────────────────────╯ 143 generate_metadata() { 144 info "Generating metadata: $MARKDOWN_FILE" 145 mkdir -p "$(dirname "$MARKDOWN_FILE")" 2>/dev/null || warn "Failed to create .gitfield directory" 146 147 TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "Unknown") 148 DEFAULT_BRANCH=$(git -C "$REPO_PATH" symbolic-ref --short HEAD 2>/dev/null || echo "Unknown") 149 LATEST_SHA=$(git -C "$REPO_PATH" rev-parse HEAD 2>/dev/null || echo "Unknown") 150 LAST_COMMIT_MSG=$(git -C "$REPO_PATH" log -1 --pretty=format:"%s" 2>/dev/null || echo "Unknown") 151 LAST_COMMIT_DATE=$(git -C "$REPO_PATH" log -1 --pretty=format:"%ad" 2>/dev/null || echo "Unknown") 152 LAST_COMMIT_AUTHOR=$(git -C "$REPO_PATH" log -1 --pretty=format:"%an <%ae>" 2>/dev/null || echo "Unknown") 153 TOTAL_COMMITS=$(git -C "$REPO_PATH" rev-list --count HEAD 2>/dev/null || echo "Unknown") 154 TRACKED_FILES=$(git -C "$REPO_PATH" ls-files 2>/dev/null | wc -l 2>/dev/null || echo "Unknown") 155 UNCOMMITTED=$(if ! git -C "$REPO_PATH" diff --quiet 2>/dev/null || ! git -C "$REPO_PATH" diff --cached --quiet 2>/dev/null; then echo "Yes"; else echo "No"; fi) 156 LATEST_TAG=$(git -C "$REPO_PATH" describe --tags --abbrev=0 2>/dev/null || echo "None") 157 HOSTNAME=$(hostname 2>/dev/null || echo "Unknown") 158 CURRENT_USER=$(whoami 2>/dev/null || echo "Unknown") 159 TIMEZONE=$(date +%Z 2>/dev/null || echo "Unknown") 160 OS_NAME=$(uname -s 2>/dev/null || echo "Unknown") 161 KERNEL_VERSION=$(uname -r 2>/dev/null || echo "Unknown") 162 ARCHITECTURE=$(uname -m 2>/dev/null || echo "Unknown") 163 OS_PRETTY_NAME=$(grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"' || echo "Unknown") 164 DOCKER_CHECK=$(grep -qE '/docker|/lxc' /proc/1/cgroup 2>/dev/null && echo "Yes" || echo "No") 165 WSL_CHECK=$(grep -qi microsoft /proc/version 2>/dev/null && echo "Yes" || echo "No") 166 VM_CHECK=$(systemd-detect-virt 2>/dev/null || echo "Unknown") 167 UPTIME=$(uptime -p 2>/dev/null || echo "Unknown") 168 MAC_ADDR=$(ip link 2>/dev/null | awk '/ether/ {print $2}' | head -n 1 2>/dev/null || echo "Unknown") 169 LOCAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}' 2>/dev/null || echo "Unknown") 170 CPU_MODEL=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2 | sed 's/^ //' 2>/dev/null || echo "Unknown") 171 RAM_GB=$(awk '/MemTotal/ {printf "%.2f", $2/1024/1024}' /proc/meminfo 2>/dev/null || echo "Unknown") 172 WEB_LINK="file://$LOCAL_REPO" 173 174 cat > "$MARKDOWN_FILE" <<EOF 175 # 🔗 Local Repository Link 176 177 - **Repo Name**: \`$REPO_NAME\` 178 - **Local User**: \`$CURRENT_USER\` 179 - **Remote URL**: \`$WEB_LINK\` 180 - **Local Repo Path**: \`$REPO_PATH\` 181 - **Remote Label**: \`$GIT_REMOTE_NAME\` 182 - **Default Branch**: \`$DEFAULT_BRANCH\` 183 - **Repo Created**: \`$TIMESTAMP\` 184 185 --- 186 187 ## 📦 Commit Info 188 189 - **This Commit Timestamp**: \`$TIMESTAMP\` 190 - **Last Commit SHA**: \`$LATEST_SHA\` 191 - **Last Commit Message**: \`$LAST_COMMIT_MSG\` 192 - **Last Commit Author**: \`$LAST_COMMIT_AUTHOR\` 193 - **Last Commit Date**: \`$LAST_COMMIT_DATE\` 194 - **This Commit URL**: \`$WEB_LINK\` 195 196 --- 197 198 ## 📊 Repo Status 199 200 - **Total Commits**: \`$TOTAL_COMMITS\` 201 - **Tracked Files**: \`$TRACKED_FILES\` 202 - **Uncommitted Changes**: \`$UNCOMMITTED\` 203 - **Latest Tag**: \`$LATEST_TAG\` 204 205 --- 206 207 ## 🧭 Environment 208 209 - **Host Machine**: \`$HOSTNAME\` 210 - **Current User**: \`$CURRENT_USER\` 211 - **Time Zone**: \`$TIMEZONE\` 212 - **Script Version**: \`v$SCRIPT_VERSION\` 213 214 --- 215 216 ## 🧬 Hardware & OS Fingerprint 217 218 - **OS Name**: \`$OS_NAME\` 219 - **OS Version**: \`$OS_PRETTY_NAME\` 220 - **Kernel Version**: \`$KERNEL_VERSION\` 221 - **Architecture**: \`$ARCHITECTURE\` 222 - **Running in Docker**: \`$DOCKER_CHECK\` 223 - **Running in WSL**: \`$WSL_CHECK\` 224 - **Virtual Machine**: \`$VM_CHECK\` 225 - **System Uptime**: \`$UPTIME\` 226 - **MAC Address**: \`$MAC_ADDR\` 227 - **Local IP**: \`$LOCAL_IP\` 228 - **CPU Model**: \`$CPU_MODEL\` 229 - **Total RAM (GB)**: \`$RAM_GB\` 230 231 --- 232 233 _Auto-generated by \`gitfield-local\` push script._ 234 EOF 235 236 # Update push_log.json 237 if command -v jq >/dev/null 2>&1; then 238 jq --arg ts "$TIMESTAMP" \ 239 --arg branch "$DEFAULT_BRANCH" \ 240 --arg commit "$LATEST_SHA" \ 241 --arg msg "$LAST_COMMIT_MSG" \ 242 '.local += [{"timestamp": $ts, "branch": $branch, "commit": $commit, "message": $msg}]' \ 243 "$PUSH_LOG" > "$PUSH_LOG.tmp" && mv "$PUSH_LOG.tmp" "$PUSH_LOG" 2>/dev/null || warn "Failed to update $PUSH_LOG" 244 info "Updated push log: $PUSH_LOG" 245 else 246 warn "jq not installed. Skipping $PUSH_LOG update." 247 fi 248 } 249 250 # ╭─────────────────────────────────────╮ 251 # │ PUSH TO LOCAL │ 252 # ╰─────────────────────────────────────╯ 253 push() { 254 info "Pushing to local bare repository..." 255 256 # Ensure remote is configured 257 if ! git -C "$REPO_PATH" remote | grep -q "^$GIT_REMOTE_NAME$"; then 258 warn "Local remote not configured. Running configure..." 259 configure 260 fi 261 262 # Generate metadata 263 generate_metadata 264 265 # Commit metadata 266 set +e 267 info "Committing metadata file..." 268 git -C "$REPO_PATH" add "$MARKDOWN_FILE" 2>/dev/null || warn "Failed to add metadata file" 269 git -C "$REPO_PATH" commit -m "Local metadata link commit at $TIMESTAMP — $WEB_LINK" 2>/dev/null || warn "No changes to commit" 270 set -e 271 272 # Push to local remote 273 DEFAULT_BRANCH=$(git -C "$REPO_PATH" symbolic-ref --short HEAD 2>/dev/null || echo "main") 274 set +e 275 info "Pushing to $GIT_REMOTE_NAME/$DEFAULT_BRANCH..." 276 if ! git -C "$REPO_PATH" push "$GIT_REMOTE_NAME" "$DEFAULT_BRANCH" 2>/dev/null; then 277 warn "Push failed. Attempting to recover..." 278 configure 279 git -C "$REPO_PATH" push "$GIT_REMOTE_NAME" "$DEFAULT_BRANCH" 2>/dev/null || error "Failed to push to $GIT_REMOTE_NAME/$DEFAULT_BRANCH after recovery" 280 fi 281 set -e 282 283 info "✅ Local push complete." 284 echo -e "\n🔗 Local repository: $WEB_LINK\n" 285 } 286 287 # ╭─────────────────────────────────────╮ 288 # │ MAIN EXECUTION │ 289 # ╰─────────────────────────────────────╯ 290 main() { 291 case "${1:-push}" in 292 configure) 293 configure 294 ;; 295 status) 296 status 297 ;; 298 push) 299 push 300 ;; 301 *) 302 error "Usage: $0 {configure|status|push}" 303 ;; 304 esac 305 } 306 307 main "$@"