diff --git a/utils/uci-git-backup/Makefile b/utils/uci-git-backup/Makefile new file mode 100644 index 00000000000000..a2f87f1cd98564 --- /dev/null +++ b/utils/uci-git-backup/Makefile @@ -0,0 +1,61 @@ +# +# Copyright (C) 2026 Mathias Rangger +# +# This is free software, licensed under the GNU General Public License v2. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=uci-git-backup +PKG_VERSION:=1.2.1 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=Mathias Rangger +PKG_LICENSE:=GPL-2.0-only + +include $(INCLUDE_DIR)/package.mk + +define Package/uci-git-backup + SECTION:=utils + CATEGORY:=Utilities + TITLE:=Automatically back up UCI configuration to Git + DEPENDS:=+git +git-http +openssh-client +endef + +define Package/uci-git-backup/description + Automatically commits and pushes watched UCI configuration to a remote Git + repository whenever a watched config is committed. Includes manual connection + testing and restore helpers for LuCI integration. +endef + +define Package/uci-git-backup/conffiles +/etc/config/uci_git_backup +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/uci-git-backup/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/uci_git_backup.config $(1)/etc/config/uci_git_backup + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/uci-git-backup.init $(1)/etc/init.d/uci-git-backup + + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_BIN) ./files/99-uci-git-backup $(1)/etc/uci-defaults/99-uci-git-backup + + $(INSTALL_DIR) $(1)/etc/uci-git-backup + + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/uci-git-backup $(1)/usr/bin/uci-git-backup + $(INSTALL_BIN) ./files/uci-git-test $(1)/usr/bin/uci-git-test + $(INSTALL_BIN) ./files/uci-git-list $(1)/usr/bin/uci-git-list + $(INSTALL_BIN) ./files/uci-git-restore $(1)/usr/bin/uci-git-restore +endef + +$(eval $(call BuildPackage,uci-git-backup)) diff --git a/utils/uci-git-backup/files/99-uci-git-backup b/utils/uci-git-backup/files/99-uci-git-backup new file mode 100644 index 00000000000000..9aac06b4f958b5 --- /dev/null +++ b/utils/uci-git-backup/files/99-uci-git-backup @@ -0,0 +1,8 @@ +#!/bin/sh +# Runs once after package installation (on next boot or via uci-defaults trigger). +# Enables and starts the uci-git-backup service. + +/etc/init.d/uci-git-backup enable +/etc/init.d/uci-git-backup start + +exit 0 diff --git a/utils/uci-git-backup/files/uci-git-backup b/utils/uci-git-backup/files/uci-git-backup new file mode 100644 index 00000000000000..981098a81e8c8f --- /dev/null +++ b/utils/uci-git-backup/files/uci-git-backup @@ -0,0 +1,252 @@ +#!/bin/sh +# +# uci-git-backup - copy /etc/config/ into a local git repo and push to remote. +# +# Called by the procd reload trigger whenever a watched UCI config changes. +# Can also be called manually or from LuCI. +# + +LOCK_FILE="/var/run/uci-git-backup.lock" +LOCK_PID_FILE="$LOCK_FILE/pid" +SSH_KEY_FILE="/etc/uci-git-backup/id_rsa" + +get_local_fqdn() { + LOCAL_HOSTNAME="$(uci -q get system.@system[0].hostname 2>/dev/null || cat /proc/sys/kernel/hostname 2>/dev/null || echo openwrt)" + LOCAL_DOMAIN="$(uci -q get dhcp.@dnsmasq[0].domain 2>/dev/null || true)" + + case "$LOCAL_DOMAIN" in + ''|lan) + printf '%s\n' "$LOCAL_HOSTNAME" + ;; + *) + printf '%s.%s\n' "$LOCAL_HOSTNAME" "$LOCAL_DOMAIN" + ;; + esac +} + +default_author_name() { + printf 'OpenWrt (%s)\n' "$(get_local_fqdn)" +} + +log() { + logger -t uci-git-backup "$*" +} + +die() { + log "ERROR: $*" + rm -rf "$LOCK_FILE" + exit 1 +} + +explain_git_error() { + case "$1" in + *'error in libcrypto: unsupported'*) + printf '%s\n%s\n' \ + "$1" \ + 'Router SSH client could not load the private key. Use an unencrypted PEM RSA/ECDSA key, or convert an existing key with: ssh-keygen -p -m PEM -f ' + ;; + *) + printf '%s\n' "$1" + ;; + esac +} + +run_with_timeout() { + RUN_TIMEOUT_SECS="$1" + shift + RUN_TIMEOUT_MARKER="/tmp/uci-git-backup-timeout.$$.$(date +%s)" + rm -f "$RUN_TIMEOUT_MARKER" + + "$@" & + RUN_TIMEOUT_CMD_PID=$! + ( + sleep "$RUN_TIMEOUT_SECS" + printf '1\n' > "$RUN_TIMEOUT_MARKER" + kill -TERM "$RUN_TIMEOUT_CMD_PID" 2>/dev/null || true + sleep 1 + kill -KILL "$RUN_TIMEOUT_CMD_PID" 2>/dev/null || true + ) & + RUN_TIMEOUT_WATCHDOG_PID=$! + + wait "$RUN_TIMEOUT_CMD_PID" + RUN_TIMEOUT_STATUS=$? + kill "$RUN_TIMEOUT_WATCHDOG_PID" 2>/dev/null || true + wait "$RUN_TIMEOUT_WATCHDOG_PID" 2>/dev/null || true + + if [ -f "$RUN_TIMEOUT_MARKER" ]; then + rm -f "$RUN_TIMEOUT_MARKER" + return 124 + fi + + rm -f "$RUN_TIMEOUT_MARKER" + return "$RUN_TIMEOUT_STATUS" +} + +# Prevent concurrent runs (procd may fire multiple triggers in rapid succession) +if ! mkdir "$LOCK_FILE" 2>/dev/null; then + if [ -f "$LOCK_PID_FILE" ]; then + LOCK_PID="$(cat "$LOCK_PID_FILE" 2>/dev/null || true)" + if [ -n "$LOCK_PID" ] && ! kill -0 "$LOCK_PID" 2>/dev/null; then + log "Removing stale lock from PID $LOCK_PID" + rm -rf "$LOCK_FILE" + fi + elif [ -d "$LOCK_FILE" ]; then + log "Removing stale lock without PID metadata" + rm -rf "$LOCK_FILE" + fi + + if ! mkdir "$LOCK_FILE" 2>/dev/null; then + log "Another instance is already running, skipping" + exit 0 + fi +fi +printf '%s\n' "$$" > "$LOCK_PID_FILE" +trap 'rm -rf "$LOCK_FILE"' EXIT INT TERM + +# Load configuration + +. /lib/functions.sh +config_load uci_git_backup + +config_get ENABLED config enabled '0' +config_get REMOTE_URL config remote_url '' +config_get BRANCH config branch 'main' +config_get AUTH_TYPE config auth_type 'password' +config_get USERNAME config username '' +config_get PASSWORD config password '' +config_get REPO_PATH config repo_path '/etc/uci-git-backup/repo' +config_get AUTHOR_NAME config author_name 'OpenWrt' +config_get AUTHOR_EMAIL config author_email 'openwrt@localhost' + +[ -n "$AUTHOR_NAME" ] || AUTHOR_NAME="$(default_author_name)" +[ "$AUTHOR_NAME" = 'OpenWRT' ] && AUTHOR_NAME="$(default_author_name)" +[ "$AUTHOR_NAME" = 'OpenWrt' ] && AUTHOR_NAME="$(default_author_name)" + +[ "$ENABLED" = "1" ] || { log "Backup is disabled (set enabled=1 in LuCI to activate)"; exit 0; } +[ -n "$REMOTE_URL" ] || die "No remote repository URL configured" + +# Git identity + +export GIT_AUTHOR_NAME="$AUTHOR_NAME" +export GIT_AUTHOR_EMAIL="$AUTHOR_EMAIL" +export GIT_COMMITTER_NAME="$AUTHOR_NAME" +export GIT_COMMITTER_EMAIL="$AUTHOR_EMAIL" + +# Suppress interactive prompts +export GIT_TERMINAL_PROMPT=0 + +# Build authenticated remote URL + +case "$AUTH_TYPE" in + password) + [ -n "$USERNAME" ] || [ -z "$PASSWORD" ] || \ + die "HTTPS password/token auth requires a username. For PAT login, use your Git username plus the token as the password." + + if [ -n "$USERNAME" ]; then + PROTO="${REMOTE_URL%%://*}://" + REST="${REMOTE_URL#*://}" + REST="${REST#*@}" + if [ -n "$PASSWORD" ]; then + ENC_PASS=$(printf '%s' "$PASSWORD" | sed 's/@/%40/g; s/:/%3A/g') + AUTH_URL="${PROTO}${USERNAME}:${ENC_PASS}@${REST}" + else + AUTH_URL="${PROTO}${USERNAME}@${REST}" + fi + else + AUTH_URL="$REMOTE_URL" + fi + ;; + ssh) + AUTH_URL="$REMOTE_URL" + if [ -f "$SSH_KEY_FILE" ]; then + export GIT_SSH_COMMAND="ssh -i $SSH_KEY_FILE \ + -o StrictHostKeyChecking=no \ + -o BatchMode=yes \ + -o ConnectTimeout=30" + else + die "SSH key file not found at $SSH_KEY_FILE. Upload your key in LuCI." + fi + ;; + *) + AUTH_URL="$REMOTE_URL" + ;; +esac + +# Initialise local repository + +if [ ! -d "$REPO_PATH/.git" ]; then + mkdir -p "$REPO_PATH" || die "Cannot create repo directory: $REPO_PATH" + git init "$REPO_PATH" 2>&1 | while IFS= read -r line; do log "$line"; done + git -C "$REPO_PATH" config user.name "$AUTHOR_NAME" + git -C "$REPO_PATH" config user.email "$AUTHOR_EMAIL" + git -C "$REPO_PATH" remote add origin "$AUTH_URL" + log "Initialised new git repository at $REPO_PATH" +else + git -C "$REPO_PATH" remote set-url origin "$AUTH_URL" 2>/dev/null +fi + +# Sync /etc/config/ into the repo + +CONFIG_DEST="$REPO_PATH/config" +mkdir -p "$CONFIG_DEST" + +find /etc/config -maxdepth 1 -type f ! -name '*.apk-new' | while read -r f; do + base="${f##*/}" + cp -f "$f" "$CONFIG_DEST/$base" +done + +find "$CONFIG_DEST" -type f | while read -r f; do + rel="${f#$CONFIG_DEST/}" + case "$rel" in + *.apk-new) + rm -f "$f" + continue + ;; + esac + + [ -f "/etc/config/$rel" ] || rm -f "$f" +done + +uci export > "$REPO_PATH/uci-export.txt" 2>/dev/null + +# Commit if there are changes + +git -C "$REPO_PATH" add -A + +if git -C "$REPO_PATH" diff --cached --quiet; then + log "No changes detected, nothing to commit" + exit 0 +fi + +HOSTNAME="$(cat /proc/sys/kernel/hostname 2>/dev/null || echo openwrt)" +TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')" + +git -C "$REPO_PATH" commit -m "UCI backup from $HOSTNAME at $TIMESTAMP" \ + || die "git commit failed" + +# Push to remote + +PUSH_LOG="/tmp/uci-git-backup-push.log" + +if run_with_timeout 20 git -C "$REPO_PATH" -c http.connectTimeout=15 push origin "HEAD:$BRANCH" >"$PUSH_LOG" 2>&1; then + log "Backup pushed successfully -> $REMOTE_URL ($BRANCH)" +else + PUSH_STATUS=$? + if [ "$PUSH_STATUS" -eq 124 ]; then + die "Push failed: remote connection timed out after 20 seconds" + fi + + log "Normal push rejected (remote is ahead), retrying with --force-with-lease" + if run_with_timeout 20 git -C "$REPO_PATH" -c http.connectTimeout=15 push --force-with-lease origin "HEAD:$BRANCH" >"$PUSH_LOG" 2>&1; then + log "Force-with-lease push succeeded" + else + PUSH_STATUS=$? + if [ "$PUSH_STATUS" -eq 124 ]; then + die "Push failed: remote connection timed out after 20 seconds" + fi + + PUSH_ERR=$(cat "$PUSH_LOG") + PUSH_ERR=$(explain_git_error "$PUSH_ERR") + die "Push failed: $PUSH_ERR" + fi +fi diff --git a/utils/uci-git-backup/files/uci-git-backup.init b/utils/uci-git-backup/files/uci-git-backup.init new file mode 100644 index 00000000000000..c6ffd79bd5d21e --- /dev/null +++ b/utils/uci-git-backup/files/uci-git-backup.init @@ -0,0 +1,45 @@ +#!/bin/sh /etc/rc.common +# +# UCI Git Backup - procd init script +# +# This service has no persistent daemon process. It uses procd's +# reload-trigger mechanism: whenever a listed UCI config is committed +# (e.g. via LuCI Save & Apply or `uci commit`), procd calls +# reload_service() which runs the backup. +# + +USE_PROCD=1 +START=99 +STOP=01 + +start_service() { + # Register a dummy oneshot instance so procd tracks this service + # and evaluates our service_triggers() declaration. + procd_open_instance + procd_set_param command /bin/true + procd_set_param oneshot 1 + procd_close_instance +} + +service_triggers() { + # Built-in set of standard OpenWrt UCI configs to watch + local cfg + for cfg in \ + network wireless firewall dhcp system \ + dropbear uhttpd rpcd openvpn \ + sqm qos nlbwmon luci + do + procd_add_reload_trigger "$cfg" + done + + # User-configured extra triggers (list option in UCI) + local extras + extras=$(uci -q get uci_git_backup.config.extra_triggers 2>/dev/null) + for cfg in $extras; do + procd_add_reload_trigger "$cfg" + done +} + +reload_service() { + /usr/bin/uci-git-backup +} diff --git a/utils/uci-git-backup/files/uci-git-list b/utils/uci-git-backup/files/uci-git-list new file mode 100644 index 00000000000000..4f3181ec0aa01a --- /dev/null +++ b/utils/uci-git-backup/files/uci-git-list @@ -0,0 +1,16 @@ +#!/bin/sh + +. /lib/functions.sh +config_load uci_git_backup + +config_get REPO_PATH config repo_path '/etc/uci-git-backup/repo' + +[ -d "$REPO_PATH/.git" ] || { + echo "No local backup repository found. Run a backup first." >&2 + exit 1 +} + +git -C "$REPO_PATH" log --pretty=format:'%H %ai %s' -30 2>/dev/null || { + echo "Failed to read backup history from $REPO_PATH" >&2 + exit 1 +} diff --git a/utils/uci-git-backup/files/uci-git-restore b/utils/uci-git-backup/files/uci-git-restore new file mode 100644 index 00000000000000..5f4d468724ed31 --- /dev/null +++ b/utils/uci-git-backup/files/uci-git-restore @@ -0,0 +1,98 @@ +#!/bin/sh +# +# uci-git-restore +# +# Checks out the config/ tree from the given commit, copies it to /etc/config/, +# then triggers a service reload. A new "restore" commit is pushed so the +# restore event is recorded in the git history. +# + +SHA="$1" + +get_local_fqdn() { + LOCAL_HOSTNAME="$(uci -q get system.@system[0].hostname 2>/dev/null || cat /proc/sys/kernel/hostname 2>/dev/null || echo openwrt)" + LOCAL_DOMAIN="$(uci -q get dhcp.@dnsmasq[0].domain 2>/dev/null || true)" + + case "$LOCAL_DOMAIN" in + ''|lan) + printf '%s\n' "$LOCAL_HOSTNAME" + ;; + *) + printf '%s.%s\n' "$LOCAL_HOSTNAME" "$LOCAL_DOMAIN" + ;; + esac +} + +default_author_name() { + printf 'OpenWrt (%s)\n' "$(get_local_fqdn)" +} + +log() { + logger -t uci-git-restore "$*" + echo "$*" +} + +die() { + log "ERROR: $*" + exit 1 +} + +[ -n "$SHA" ] || die "Usage: $0 <40-char-commit-sha>" +echo "$SHA" | grep -qE '^[0-9a-f]{40}$' || die "Invalid commit SHA: $SHA" + +# Load config + +. /lib/functions.sh +config_load uci_git_backup + +config_get REPO_PATH config repo_path '/etc/uci-git-backup/repo' +config_get AUTHOR_NAME config author_name 'OpenWrt' +config_get AUTHOR_EMAIL config author_email 'openwrt@localhost' + +[ -n "$AUTHOR_NAME" ] || AUTHOR_NAME="$(default_author_name)" +[ "$AUTHOR_NAME" = 'OpenWRT' ] && AUTHOR_NAME="$(default_author_name)" +[ "$AUTHOR_NAME" = 'OpenWrt' ] && AUTHOR_NAME="$(default_author_name)" + +[ -d "$REPO_PATH/.git" ] || die "No local backup repository found at $REPO_PATH. Run a backup first." + +export GIT_AUTHOR_NAME="$AUTHOR_NAME" +export GIT_AUTHOR_EMAIL="$AUTHOR_EMAIL" +export GIT_COMMITTER_NAME="$AUTHOR_NAME" +export GIT_COMMITTER_EMAIL="$AUTHOR_EMAIL" +export GIT_TERMINAL_PROMPT=0 + +# Restore config files from the chosen commit + +log "Restoring config from commit $SHA" + +git -C "$REPO_PATH" checkout "$SHA" -- config/ \ + || die "Failed to restore commit $SHA" + +cp -r "$REPO_PATH/config/." /etc/config/ \ + || die "Failed to copy config files to /etc/config/" + +log "Config files restored from $SHA" + +# Reload services (in background so this script can exit first) + +(sleep 2 && reload_config) & + +log "Services will reload in 2 seconds" + +# Push a restore-tracking commit +# The procd trigger will also detect the /etc/config/ change and call +# uci-git-backup, but we record the restore intent explicitly here. + +HOSTNAME="$(cat /proc/sys/kernel/hostname 2>/dev/null || echo openwrt)" +TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')" +SHORT_SHA="$(echo "$SHA" | cut -c1-8)" + +git -C "$REPO_PATH" add -A 2>/dev/null +git -C "$REPO_PATH" diff --cached --quiet || \ + git -C "$REPO_PATH" commit \ + -m "Restore from $SHORT_SHA on $HOSTNAME at $TIMESTAMP" 2>/dev/null + +# Best-effort push (uci-git-backup will retry if this fails) +/usr/bin/uci-git-backup 2>/dev/null + +log "Done" diff --git a/utils/uci-git-backup/files/uci-git-test b/utils/uci-git-backup/files/uci-git-test new file mode 100644 index 00000000000000..25e643a55601db --- /dev/null +++ b/utils/uci-git-backup/files/uci-git-test @@ -0,0 +1,178 @@ +#!/bin/sh + +SSH_KEY_FILE="/etc/uci-git-backup/id_rsa" +TMP_PREFIX="/tmp/uci-git-test.$$" + +cleanup() { + rm -f "$TMP_PREFIX".out "$TMP_PREFIX".err "$TMP_PREFIX"-branch.out "$TMP_PREFIX"-branch.err +} + +die() { + printf 'ERROR: %s\n' "$*" >&2 + cleanup + exit 1 +} + +explain_git_error() { + case "$1" in + *'error in libcrypto: unsupported'*) + printf '%s\n%s\n' \ + "$1" \ + 'Router SSH client could not load the private key. Use an unencrypted PEM RSA/ECDSA key, or convert an existing key with: ssh-keygen -p -m PEM -f ' + ;; + *) + printf '%s\n' "$1" + ;; + esac +} + +run_with_timeout() { + RUN_TIMEOUT_SECS="$1" + shift + RUN_TIMEOUT_MARKER="/tmp/uci-git-test-timeout.$$.$(date +%s)" + rm -f "$RUN_TIMEOUT_MARKER" + + "$@" & + RUN_TIMEOUT_CMD_PID=$! + ( + sleep "$RUN_TIMEOUT_SECS" + printf '1\n' > "$RUN_TIMEOUT_MARKER" + kill -TERM "$RUN_TIMEOUT_CMD_PID" 2>/dev/null || true + sleep 1 + kill -KILL "$RUN_TIMEOUT_CMD_PID" 2>/dev/null || true + ) & + RUN_TIMEOUT_WATCHDOG_PID=$! + + wait "$RUN_TIMEOUT_CMD_PID" + RUN_TIMEOUT_STATUS=$? + kill "$RUN_TIMEOUT_WATCHDOG_PID" 2>/dev/null || true + wait "$RUN_TIMEOUT_WATCHDOG_PID" 2>/dev/null || true + + if [ -f "$RUN_TIMEOUT_MARKER" ]; then + rm -f "$RUN_TIMEOUT_MARKER" + return 124 + fi + + rm -f "$RUN_TIMEOUT_MARKER" + return "$RUN_TIMEOUT_STATUS" +} + +trap cleanup EXIT INT TERM + +CLI_REMOTE_URL_SET=0 +CLI_BRANCH_SET=0 +CLI_AUTH_TYPE_SET=0 +CLI_USERNAME_SET=0 +CLI_PASSWORD_SET=0 + +while [ "$#" -gt 0 ]; do + case "$1" in + --remote-url) + CLI_REMOTE_URL_SET=1 + CLI_REMOTE_URL="${2-}" + shift 2 + ;; + --branch) + CLI_BRANCH_SET=1 + CLI_BRANCH="${2-}" + shift 2 + ;; + --auth-type) + CLI_AUTH_TYPE_SET=1 + CLI_AUTH_TYPE="${2-}" + shift 2 + ;; + --username) + CLI_USERNAME_SET=1 + CLI_USERNAME="${2-}" + shift 2 + ;; + --password) + CLI_PASSWORD_SET=1 + CLI_PASSWORD="${2-}" + shift 2 + ;; + *) + die "Unknown argument: $1" + ;; + esac +done + +. /lib/functions.sh +config_load uci_git_backup + +config_get REMOTE_URL config remote_url '' +config_get BRANCH config branch 'main' +config_get AUTH_TYPE config auth_type 'password' +config_get USERNAME config username '' +config_get PASSWORD config password '' + +[ "$CLI_REMOTE_URL_SET" -eq 0 ] || REMOTE_URL="$CLI_REMOTE_URL" +[ "$CLI_BRANCH_SET" -eq 0 ] || BRANCH="$CLI_BRANCH" +[ "$CLI_AUTH_TYPE_SET" -eq 0 ] || AUTH_TYPE="$CLI_AUTH_TYPE" +[ "$CLI_USERNAME_SET" -eq 0 ] || USERNAME="$CLI_USERNAME" +[ "$CLI_PASSWORD_SET" -eq 0 ] || PASSWORD="$CLI_PASSWORD" + +[ -n "$REMOTE_URL" ] || die "No remote repository URL configured" + +export GIT_TERMINAL_PROMPT=0 + +case "$AUTH_TYPE" in + password) + [ -n "$USERNAME" ] || [ -z "$PASSWORD" ] || \ + die "HTTPS password/token auth requires a username. For PAT login, use your Git username plus the token as the password." + + if [ -n "$USERNAME" ]; then + PROTO="${REMOTE_URL%%://*}://" + REST="${REMOTE_URL#*://}" + REST="${REST#*@}" + if [ -n "$PASSWORD" ]; then + ENC_PASS=$(printf '%s' "$PASSWORD" | sed 's/@/%40/g; s/:/%3A/g') + AUTH_URL="${PROTO}${USERNAME}:${ENC_PASS}@${REST}" + else + AUTH_URL="${PROTO}${USERNAME}@${REST}" + fi + else + AUTH_URL="$REMOTE_URL" + fi + ;; + ssh) + AUTH_URL="$REMOTE_URL" + if [ -f "$SSH_KEY_FILE" ]; then + export GIT_SSH_COMMAND="ssh -i $SSH_KEY_FILE \ + -o StrictHostKeyChecking=no \ + -o BatchMode=yes \ + -o ConnectTimeout=15" + else + die "SSH key file not found at $SSH_KEY_FILE. Upload your key in LuCI." + fi + ;; + *) + die "Unsupported authentication method: $AUTH_TYPE" + ;; +esac + +printf 'Testing remote reachability for %s\n' "$REMOTE_URL" +printf 'Configured branch: %s\n' "$BRANCH" + +if run_with_timeout 20 git -c http.connectTimeout=15 ls-remote "$AUTH_URL" >"$TMP_PREFIX".out 2>"$TMP_PREFIX".err; then + printf 'Remote reachable and authentication succeeded.\n' + if run_with_timeout 20 git -c http.connectTimeout=15 ls-remote --heads "$AUTH_URL" "$BRANCH" >"$TMP_PREFIX"-branch.out 2>"$TMP_PREFIX"-branch.err; then + if [ -s "$TMP_PREFIX"-branch.out ]; then + printf 'Configured branch exists on remote: %s\n' "$BRANCH" + else + printf 'Configured branch does not exist on remote yet: %s\n' "$BRANCH" + fi + fi + exit 0 +fi + +TEST_STATUS=$? + +if [ "$TEST_STATUS" -eq 124 ]; then + die "Connection test failed: remote connection timed out after 20 seconds" +fi + +ERR_MSG=$(cat "$TMP_PREFIX".err 2>/dev/null || true) +ERR_MSG=$(explain_git_error "${ERR_MSG:-git ls-remote failed}") +die "Connection test failed: $ERR_MSG" diff --git a/utils/uci-git-backup/files/uci_git_backup.config b/utils/uci-git-backup/files/uci_git_backup.config new file mode 100644 index 00000000000000..230c3a9819bde2 --- /dev/null +++ b/utils/uci-git-backup/files/uci_git_backup.config @@ -0,0 +1,11 @@ +config uci_git_backup 'config' + option enabled '0' + option remote_url '' + option branch 'main' + option auth_type 'password' + option username '' + option password '' + option repo_path '/etc/uci-git-backup/repo' + option author_name 'OpenWrt' + option author_email 'openwrt@localhost' + list extra_triggers ''