forked from Shinonome/dots-hyprland
Update the update, update tester, functions scripts
This commit is contained in:
@@ -4,3 +4,4 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
dots/.config/quickshell/ii/.qmlls.ini
|
||||
.update-lock
|
||||
|
||||
@@ -117,3 +117,178 @@ function log_die() {
|
||||
log_error "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Enhanced: Check if command exists
|
||||
function command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Enhanced: Require a command or die
|
||||
function require_command() {
|
||||
if ! command_exists "$1"; then
|
||||
log_die "Required command '$1' not found. Please install it first."
|
||||
fi
|
||||
}
|
||||
|
||||
# Enhanced: Sanitize file paths to prevent directory traversal
|
||||
function sanitize_path() {
|
||||
local path="$1"
|
||||
|
||||
# Remove null bytes, newlines, and control characters
|
||||
path=$(echo "$path" | tr -d '\000-\037')
|
||||
|
||||
# Prevent directory traversal beyond current context
|
||||
case "$path" in
|
||||
..|../*|*/../*|*/..|\.\./*)
|
||||
log_die "Invalid path detected (directory traversal attempt): $path"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$path"
|
||||
}
|
||||
|
||||
# Enhanced: Safe file comparison that checks existence first
|
||||
function files_differ() {
|
||||
local file1="$1"
|
||||
local file2="$2"
|
||||
|
||||
# Check if both files exist
|
||||
if [[ ! -f "$file1" ]] || [[ ! -f "$file2" ]]; then
|
||||
return 0 # Consider them different if either doesn't exist
|
||||
fi
|
||||
|
||||
# Quick size check first (faster than byte comparison)
|
||||
local size1 size2
|
||||
if command -v stat &>/dev/null; then
|
||||
# Try both BSD and GNU stat formats
|
||||
size1=$(stat -f%z "$file1" 2>/dev/null || stat -c%s "$file1" 2>/dev/null)
|
||||
size2=$(stat -f%z "$file2" 2>/dev/null || stat -c%s "$file2" 2>/dev/null)
|
||||
|
||||
if [[ "$size1" != "$size2" ]]; then
|
||||
return 0 # Different sizes = different files
|
||||
fi
|
||||
fi
|
||||
|
||||
# Then byte-by-byte comparison
|
||||
cmp -s "$file1" "$file2" && return 1 || return 0
|
||||
}
|
||||
|
||||
# Enhanced: Create backup of a file with timestamp
|
||||
function backup_file_simple() {
|
||||
local file="$1"
|
||||
local backup_suffix="${2:-.bak}"
|
||||
|
||||
if [[ ! -f "$file" ]]; then
|
||||
log_warning "Cannot backup non-existent file: $file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d-%H%M%S)
|
||||
local backup_name="${file}${backup_suffix}.${timestamp}"
|
||||
|
||||
if cp -p "$file" "$backup_name" 2>/dev/null; then
|
||||
log_info "Backed up: $file → $backup_name"
|
||||
return 0
|
||||
else
|
||||
log_error "Failed to backup: $file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Enhanced: Validate that a file path is within allowed directory
|
||||
function validate_path_in_directory() {
|
||||
local file_path="$1"
|
||||
local allowed_dir="$2"
|
||||
|
||||
# Resolve to absolute paths
|
||||
local abs_file
|
||||
local abs_dir
|
||||
|
||||
abs_file=$(cd "$(dirname "$file_path")" 2>/dev/null && pwd -P)/$(basename "$file_path") || return 1
|
||||
abs_dir=$(cd "$allowed_dir" 2>/dev/null && pwd -P) || return 1
|
||||
|
||||
# Check if file path starts with allowed directory
|
||||
case "$abs_file" in
|
||||
"$abs_dir"/*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
log_error "Path validation failed: $file_path is not within $allowed_dir"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Enhanced: Check if script is running in a CI/CD environment
|
||||
function is_ci_environment() {
|
||||
[[ -n "${CI:-}" ]] || \
|
||||
[[ -n "${GITHUB_ACTIONS:-}" ]] || \
|
||||
[[ -n "${GITLAB_CI:-}" ]] || \
|
||||
[[ -n "${TRAVIS:-}" ]] || \
|
||||
[[ -n "${CIRCLECI:-}" ]]
|
||||
}
|
||||
|
||||
# Enhanced: Progress bar (optional, for long operations)
|
||||
function show_progress() {
|
||||
local current="$1"
|
||||
local total="$2"
|
||||
local message="${3:-Processing}"
|
||||
|
||||
if ! command_exists tput; then
|
||||
return
|
||||
fi
|
||||
|
||||
local percent=$((current * 100 / total))
|
||||
local bar_length=40
|
||||
local filled=$((bar_length * current / total))
|
||||
local empty=$((bar_length - filled))
|
||||
|
||||
printf "\r${message}: [" >&2
|
||||
printf "%${filled}s" | tr ' ' '=' >&2
|
||||
printf "%${empty}s" | tr ' ' ' ' >&2
|
||||
printf "] %d%%" "$percent" >&2
|
||||
|
||||
if [[ $current -eq $total ]]; then
|
||||
echo >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# Enhanced: Cleanup temporary files on exit
|
||||
declare -a TEMP_FILES_TO_CLEANUP=()
|
||||
|
||||
function register_temp_file() {
|
||||
local temp_file="$1"
|
||||
TEMP_FILES_TO_CLEANUP+=("$temp_file")
|
||||
}
|
||||
|
||||
function cleanup_temp_files() {
|
||||
for temp_file in "${TEMP_FILES_TO_CLEANUP[@]}"; do
|
||||
if [[ -f "$temp_file" ]]; then
|
||||
rm -f "$temp_file" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
TEMP_FILES_TO_CLEANUP=()
|
||||
}
|
||||
|
||||
# Enhanced: Check disk space before operations
|
||||
function check_disk_space() {
|
||||
local path="${1:-.}"
|
||||
local required_mb="${2:-100}" # Default 100MB
|
||||
|
||||
if ! command_exists df; then
|
||||
log_warning "df command not available, skipping disk space check"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local available_kb
|
||||
available_kb=$(df -k "$path" | awk 'NR==2 {print $4}')
|
||||
local available_mb=$((available_kb / 1024))
|
||||
|
||||
if [[ $available_mb -lt $required_mb ]]; then
|
||||
log_warning "Low disk space: ${available_mb}MB available, ${required_mb}MB recommended"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
+297
-56
@@ -147,7 +147,15 @@ log_header() { :; }
|
||||
log_die() { echo "ERROR: \$1"; exit 1; }
|
||||
STY_CYAN="" STY_RST="" STY_YELLOW=""
|
||||
|
||||
REPO_ROOT="$1"
|
||||
# Set required environment variables for exp-update.sh
|
||||
SKIP_NOTICE=true
|
||||
REPO_ROOT="\$1"
|
||||
CHECK_PACKAGES=false
|
||||
DRY_RUN=false
|
||||
FORCE_CHECK=false
|
||||
VERBOSE=false
|
||||
NON_INTERACTIVE=true
|
||||
|
||||
source "$ORIGINAL_DIR/sdata/step/exp-update.sh"
|
||||
detect_repo_structure
|
||||
EOF
|
||||
@@ -194,7 +202,15 @@ log_header() { :; }
|
||||
log_die() { echo "ERROR: \$1"; exit 1; }
|
||||
STY_CYAN="" STY_RST="" STY_YELLOW=""
|
||||
|
||||
REPO_ROOT="$1"
|
||||
# Set required environment variables for exp-update.sh
|
||||
SKIP_NOTICE=true
|
||||
REPO_ROOT="\$1"
|
||||
CHECK_PACKAGES=false
|
||||
DRY_RUN=false
|
||||
FORCE_CHECK=false
|
||||
VERBOSE=false
|
||||
NON_INTERACTIVE=true
|
||||
|
||||
source "$ORIGINAL_DIR/sdata/step/exp-update.sh"
|
||||
detect_repo_structure
|
||||
EOF
|
||||
@@ -235,7 +251,7 @@ test_dots_mapping() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 7: Test ignore file patterns
|
||||
# Test 7: Test ignore file patterns - FIXED
|
||||
test_ignore_patterns() {
|
||||
log_test "Testing ignore file pattern matching"
|
||||
|
||||
@@ -257,48 +273,61 @@ EOF
|
||||
|
||||
cat > test_ignore.sh << EOF
|
||||
#!/bin/bash
|
||||
# Mock logging and style functions/variables
|
||||
# Suppress all output from sourced script
|
||||
log_info() { :; }
|
||||
log_warning() { :; }
|
||||
log_error() { :; }
|
||||
log_success() { :; }
|
||||
log_header() { :; }
|
||||
log_die() { echo "ERROR: \$1"; exit 1; }
|
||||
log_die() { echo "ERROR: \$1" >&2; exit 1; }
|
||||
STY_CYAN="" STY_RST="" STY_YELLOW=""
|
||||
|
||||
REPO_ROOT="$1"
|
||||
UPDATE_IGNORE_FILE="${REPO_ROOT}/.updateignore"
|
||||
# FIXED: Set REPO_ROOT before sourcing exp-update.sh
|
||||
REPO_ROOT="\$1"
|
||||
export REPO_ROOT
|
||||
|
||||
# Set other required environment variables
|
||||
SKIP_NOTICE=true
|
||||
CHECK_PACKAGES=false
|
||||
DRY_RUN=false
|
||||
FORCE_CHECK=false
|
||||
VERBOSE=false
|
||||
NON_INTERACTIVE=true
|
||||
|
||||
UPDATE_IGNORE_FILE="\${REPO_ROOT}/.updateignore"
|
||||
HOME_UPDATE_IGNORE_FILE="/dev/null"
|
||||
|
||||
# Source the production script to use the real should_ignore function
|
||||
source "$ORIGINAL_DIR/sdata/step/exp-update.sh"
|
||||
# Redirect all unwanted output to stderr, then to /dev/null
|
||||
source "$ORIGINAL_DIR/sdata/step/exp-update.sh" 2>/dev/null
|
||||
|
||||
test_cases=(
|
||||
"$REPO_ROOT/app.log:0"
|
||||
"$REPO_ROOT/secrets/key.txt:0"
|
||||
"$REPO_ROOT/.config/private-config:0"
|
||||
"$REPO_ROOT/.config/backup-file:0"
|
||||
"$REPO_ROOT/normal-config:1"
|
||||
"\$REPO_ROOT/app.log:0"
|
||||
"\$REPO_ROOT/secrets/key.txt:0"
|
||||
"\$REPO_ROOT/.config/private-config:0"
|
||||
"\$REPO_ROOT/.config/backup-file:0"
|
||||
"\$REPO_ROOT/normal-config:1"
|
||||
)
|
||||
|
||||
all_passed=true
|
||||
for test_case in "${test_cases[@]}"; do
|
||||
IFS=":" read -r file expected <<< "$test_case"
|
||||
mkdir -p "$(dirname "$file")"
|
||||
touch "$file"
|
||||
for test_case in "\${test_cases[@]}"; do
|
||||
IFS=":" read -r file expected <<< "\$test_case"
|
||||
mkdir -p "\$(dirname "\$file")"
|
||||
touch "\$file"
|
||||
|
||||
if should_ignore "$file"; then
|
||||
if should_ignore "\$file"; then
|
||||
result=0
|
||||
else
|
||||
result=1
|
||||
fi
|
||||
|
||||
if [[ $result -ne $expected ]]; then
|
||||
echo "FAIL: $file (expected: $expected, got: $result)"
|
||||
if [[ \$result -ne \$expected ]]; then
|
||||
echo "FAIL: \$file (expected: \$expected, got: \$result)"
|
||||
all_passed=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$all_passed" == true ]]; then
|
||||
if [[ "\$all_passed" == true ]]; then
|
||||
echo "PASS"
|
||||
else
|
||||
echo "FAIL"
|
||||
@@ -306,7 +335,7 @@ fi
|
||||
EOF
|
||||
|
||||
chmod +x test_ignore.sh
|
||||
result=$(./test_ignore.sh "$test_repo")
|
||||
result=$(./test_ignore.sh "$test_repo" 2>&1 | grep -E "^(PASS|FAIL)")
|
||||
|
||||
if [[ "$result" == "PASS" ]]; then
|
||||
log_pass "All ignore pattern tests passed"
|
||||
@@ -320,30 +349,47 @@ EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 8: Test safe_read security - COMPLETELY NON-INTERACTIVE
|
||||
# Test 8: Test safe_read security - FIXED
|
||||
test_safe_read_security() {
|
||||
log_test "Testing safe_read uses secure assignment (printf -v)"
|
||||
|
||||
local awk_script='/^safe_read() \{/,/^\}/'
|
||||
local safe_read_function
|
||||
safe_read_function=$(awk "$awk_script" "$ORIGINAL_DIR/sdata/step/exp-update.sh")
|
||||
safe_read_function=$(awk '/^safe_read\(\) \{/,/^\}/' "$ORIGINAL_DIR/sdata/step/exp-update.sh")
|
||||
|
||||
if [[ -z "$safe_read_function" ]]; then
|
||||
log_fail "Could not find safe_read function"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for secure printf -v assignment and absence of eval
|
||||
if echo "$safe_read_function" | grep -q "printf -v" && ! echo "$safe_read_function" | grep -q "eval"; then
|
||||
# FIXED: Remove comments before checking for eval
|
||||
# The function has a comment mentioning eval, which shouldn't count
|
||||
local function_without_comments
|
||||
function_without_comments=$(echo "$safe_read_function" | sed 's/#.*$//')
|
||||
|
||||
local has_printf_v=false
|
||||
local has_eval=false
|
||||
|
||||
if echo "$safe_read_function" | grep -F 'printf -v' > /dev/null; then
|
||||
has_printf_v=true
|
||||
fi
|
||||
|
||||
# Check for eval in actual code (not comments)
|
||||
if echo "$function_without_comments" | grep -w 'eval' > /dev/null; then
|
||||
has_eval=true
|
||||
fi
|
||||
|
||||
if [[ "$has_printf_v" == true ]] && [[ "$has_eval" == false ]]; then
|
||||
log_pass "safe_read uses secure printf -v assignment and no eval"
|
||||
return 0
|
||||
else
|
||||
log_fail "safe_read does not use secure assignment or contains eval"
|
||||
log_fail "safe_read does not use secure assignment or contains eval (has_printf_v=$has_printf_v, has_eval=$has_eval)"
|
||||
echo "Function content:"
|
||||
echo "$safe_read_function"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 9: Test dry-run mode
|
||||
# Test 9: Test dry-run mode - FIXED
|
||||
test_dry_run() {
|
||||
log_test "Testing dry-run mode"
|
||||
|
||||
@@ -359,9 +405,16 @@ test_dry_run() {
|
||||
cp -r "$ORIGINAL_DIR/dots" .
|
||||
chmod +x install.sh
|
||||
|
||||
# Create a test config file in repo
|
||||
mkdir -p dots/.config/test-app
|
||||
echo "test config" > dots/.config/test-app/config.conf
|
||||
|
||||
git add .
|
||||
git commit -m "Add test config" -q
|
||||
|
||||
# FIXED: Clean up any existing test files before running test
|
||||
rm -rf "${HOME}/.config/test-app" 2>/dev/null || true
|
||||
|
||||
# Use non-interactive mode and check for DRY-RUN marker
|
||||
./install.sh exp-update -n --skip-notice --non-interactive 2>&1 | tee dry_run_output.txt
|
||||
|
||||
@@ -373,13 +426,14 @@ test_dry_run() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${HOME}/.config/test-app/config.conf" ]]; then
|
||||
log_pass "No files created in home during dry-run"
|
||||
else
|
||||
# FIXED: Check if files were created (they shouldn't be in dry-run)
|
||||
if [[ -f "${HOME}/.config/test-app/config.conf" ]]; then
|
||||
log_fail "Files were created in home during dry-run"
|
||||
rm -f "${HOME}/.config/test-app/config.conf"
|
||||
rm -rf "${HOME}/.config/test-app"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 1
|
||||
else
|
||||
log_pass "No files created in home during dry-run"
|
||||
fi
|
||||
|
||||
cd "$ORIGINAL_DIR"
|
||||
@@ -430,7 +484,41 @@ test_shellcheck() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 13: Test ** substring ignore patterns
|
||||
# Test 12: Test lock file mechanism
|
||||
test_lock_file() {
|
||||
log_test "Testing lock file mechanism"
|
||||
|
||||
local test_repo
|
||||
test_repo=$(setup_test_env)
|
||||
TEST_DIR="$test_repo"
|
||||
|
||||
cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; }
|
||||
|
||||
# Copy necessary files
|
||||
cp "$ORIGINAL_DIR/install.sh" .
|
||||
cp -r "$ORIGINAL_DIR/sdata" .
|
||||
mkdir -p dots/.config
|
||||
chmod +x install.sh
|
||||
|
||||
git add .
|
||||
git commit -m "Add files" -q
|
||||
|
||||
# Create a fake lock file
|
||||
echo "99999" > .update-lock
|
||||
|
||||
# Try to run update - should fail due to lock
|
||||
if ./install.sh exp-update --skip-notice --non-interactive --dry-run 2>&1 | grep -q "stale lock"; then
|
||||
log_pass "Lock file mechanism works (detected stale lock)"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 0
|
||||
else
|
||||
log_fail "Lock file mechanism did not work as expected"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 13: Test ** substring ignore patterns - FIXED
|
||||
test_substring_ignore_patterns() {
|
||||
log_test "Testing ** substring ignore pattern matching"
|
||||
|
||||
@@ -443,7 +531,7 @@ test_substring_ignore_patterns() {
|
||||
cat > .updateignore << 'EOF'
|
||||
**temp**
|
||||
**backup**
|
||||
**test**
|
||||
**testfile**
|
||||
EOF
|
||||
|
||||
mkdir -p .config/test-app
|
||||
@@ -453,48 +541,64 @@ EOF
|
||||
|
||||
cat > test_substring_ignore.sh << EOF
|
||||
#!/bin/bash
|
||||
# Mock logging and style functions/variables
|
||||
# Suppress all output from sourced script
|
||||
log_info() { :; }
|
||||
log_warning() { :; }
|
||||
log_error() { :; }
|
||||
log_success() { :; }
|
||||
log_header() { :; }
|
||||
log_die() { echo "ERROR: \$1"; exit 1; }
|
||||
log_die() { echo "ERROR: \$1" >&2; exit 1; }
|
||||
STY_CYAN="" STY_RST="" STY_YELLOW=""
|
||||
|
||||
REPO_ROOT="$1"
|
||||
UPDATE_IGNORE_FILE="${REPO_ROOT}/.updateignore"
|
||||
# FIXED: Set REPO_ROOT before sourcing exp-update.sh
|
||||
REPO_ROOT="\$1"
|
||||
export REPO_ROOT
|
||||
|
||||
# Set other required environment variables
|
||||
SKIP_NOTICE=true
|
||||
CHECK_PACKAGES=false
|
||||
DRY_RUN=false
|
||||
FORCE_CHECK=false
|
||||
VERBOSE=false
|
||||
NON_INTERACTIVE=true
|
||||
|
||||
UPDATE_IGNORE_FILE="\${REPO_ROOT}/.updateignore"
|
||||
HOME_UPDATE_IGNORE_FILE="/dev/null"
|
||||
|
||||
# Source the production script to use the real should_ignore function
|
||||
source "$ORIGINAL_DIR/sdata/step/exp-update.sh"
|
||||
source "$ORIGINAL_DIR/sdata/step/exp-update.sh" 2>/dev/null
|
||||
|
||||
# Load patterns into cache
|
||||
load_ignore_patterns
|
||||
|
||||
test_cases=(
|
||||
"$REPO_ROOT/temp-backup-dir/file:0"
|
||||
"$REPO_ROOT/.config/test-app/temp.conf:0"
|
||||
"$REPO_ROOT/.local/share/test-temp/data:0"
|
||||
"$REPO_ROOT/.config/temp-file/config:0"
|
||||
"$REPO_ROOT/normal-config:1"
|
||||
"\$REPO_ROOT/temp-backup-dir/file:0"
|
||||
"\$REPO_ROOT/.config/test-app/temp.conf:0"
|
||||
"\$REPO_ROOT/.local/share/test-temp/data:0"
|
||||
"\$REPO_ROOT/.config/temp-file/config:0"
|
||||
"\$REPO_ROOT/normal-config:1"
|
||||
"\$REPO_ROOT/.config/my-testfile.conf:0"
|
||||
)
|
||||
|
||||
all_passed=true
|
||||
for test_case in "${test_cases[@]}"; do
|
||||
IFS=":" read -r file expected <<< "$test_case"
|
||||
mkdir -p "$(dirname "$file")"
|
||||
touch "$file"
|
||||
for test_case in "\${test_cases[@]}"; do
|
||||
IFS=":" read -r file expected <<< "\$test_case"
|
||||
mkdir -p "\$(dirname "\$file")"
|
||||
touch "\$file"
|
||||
|
||||
if should_ignore "$file"; then
|
||||
if should_ignore "\$file"; then
|
||||
result=0
|
||||
else
|
||||
result=1
|
||||
fi
|
||||
|
||||
if [[ $result -ne $expected ]]; then
|
||||
echo "FAIL: $file (expected: $expected, got: $result)"
|
||||
if [[ \$result -ne \$expected ]]; then
|
||||
echo "FAIL: \$file (expected: \$expected, got: \$result)"
|
||||
all_passed=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$all_passed" == true ]]; then
|
||||
if [[ "\$all_passed" == true ]]; then
|
||||
echo "PASS"
|
||||
else
|
||||
echo "FAIL"
|
||||
@@ -502,7 +606,7 @@ fi
|
||||
EOF
|
||||
|
||||
chmod +x test_substring_ignore.sh
|
||||
result=$(./test_substring_ignore.sh "$test_repo")
|
||||
result=$(./test_substring_ignore.sh "$test_repo" 2>&1 | grep -E "^(PASS|FAIL)")
|
||||
|
||||
if [[ "$result" == "PASS" ]]; then
|
||||
log_pass "** substring ignore patterns work correctly"
|
||||
@@ -516,10 +620,142 @@ EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 14: Test ensure_directory caching
|
||||
test_directory_caching() {
|
||||
log_test "Testing directory creation caching"
|
||||
|
||||
local test_repo
|
||||
test_repo=$(setup_test_env)
|
||||
TEST_DIR="$test_repo"
|
||||
|
||||
cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; }
|
||||
|
||||
cat > test_dir_cache.sh << EOF
|
||||
#!/bin/bash
|
||||
log_info() { :; }
|
||||
log_warning() { :; }
|
||||
log_error() { :; }
|
||||
log_success() { :; }
|
||||
log_header() { :; }
|
||||
log_die() { echo "ERROR: \$1" >&2; exit 1; }
|
||||
STY_CYAN="" STY_RST="" STY_YELLOW="" STY_PURPLE=""
|
||||
|
||||
REPO_ROOT="\$1"
|
||||
export REPO_ROOT
|
||||
|
||||
SKIP_NOTICE=true
|
||||
CHECK_PACKAGES=false
|
||||
DRY_RUN=false
|
||||
FORCE_CHECK=false
|
||||
VERBOSE=false
|
||||
NON_INTERACTIVE=true
|
||||
|
||||
source "$ORIGINAL_DIR/sdata/step/exp-update.sh" 2>/dev/null
|
||||
|
||||
test_dir="/tmp/test-ensure-dir-\$\$"
|
||||
|
||||
# First call should create
|
||||
ensure_directory "\$test_dir"
|
||||
result1=\$?
|
||||
|
||||
# Second call should use cache
|
||||
ensure_directory "\$test_dir"
|
||||
result2=\$?
|
||||
|
||||
# Check if CREATED_DIRS has the entry
|
||||
if [[ -n "\${CREATED_DIRS[\$test_dir]:-}" ]] && [[ \$result1 -eq 0 ]] && [[ \$result2 -eq 0 ]]; then
|
||||
echo "PASS"
|
||||
rm -rf "\$test_dir"
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x test_dir_cache.sh
|
||||
result=$(./test_dir_cache.sh "$test_repo" 2>&1 | grep -E "^(PASS|FAIL)")
|
||||
|
||||
if [[ "$result" == "PASS" ]]; then
|
||||
log_pass "Directory creation caching works"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 0
|
||||
else
|
||||
log_fail "Directory creation caching failed"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 15: Test enhanced safe_read with non-interactive mode
|
||||
test_safe_read_noninteractive() {
|
||||
log_test "Testing safe_read in non-interactive mode"
|
||||
|
||||
cat > test_safe_read.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
log_warning() { :; }
|
||||
log_error() { :; }
|
||||
|
||||
# Simulate the enhanced safe_read function
|
||||
safe_read() {
|
||||
local prompt="$1"
|
||||
local varname="$2"
|
||||
local default="${3:-}"
|
||||
local input_value=""
|
||||
|
||||
# In non-interactive mode, use default immediately
|
||||
if [[ "$NON_INTERACTIVE" == true ]]; then
|
||||
if [[ -n "$default" ]]; then
|
||||
printf -v "$varname" '%s' "$default"
|
||||
return 0
|
||||
else
|
||||
log_error "Non-interactive mode requires default value for: $prompt"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Regular read logic...
|
||||
printf -v "$varname" '%s' "$default"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test 1: With default in non-interactive mode
|
||||
NON_INTERACTIVE=true
|
||||
if safe_read "Test: " result "default_value"; then
|
||||
if [[ "$result" == "default_value" ]]; then
|
||||
echo "TEST1: PASS"
|
||||
else
|
||||
echo "TEST1: FAIL - got '$result'"
|
||||
fi
|
||||
else
|
||||
echo "TEST1: FAIL - returned error"
|
||||
fi
|
||||
|
||||
# Test 2: Without default in non-interactive mode (should fail)
|
||||
if safe_read "Test: " result ""; then
|
||||
echo "TEST2: FAIL - should have failed"
|
||||
else
|
||||
echo "TEST2: PASS - correctly failed"
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x test_safe_read.sh
|
||||
result=$(./test_safe_read.sh 2>&1)
|
||||
|
||||
if echo "$result" | grep -q "TEST1: PASS" && echo "$result" | grep -q "TEST2: PASS"; then
|
||||
log_pass "Enhanced safe_read handles non-interactive mode correctly"
|
||||
rm -f test_safe_read.sh
|
||||
return 0
|
||||
else
|
||||
log_fail "Enhanced safe_read non-interactive mode failed"
|
||||
echo "$result"
|
||||
rm -f test_safe_read.sh
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main test runner
|
||||
main() {
|
||||
echo -e "${BLUE}================================${NC}"
|
||||
echo -e "${BLUE} Update.sh Test Suite (Sourced Subcommand)${NC}"
|
||||
echo -e "${BLUE} Update.sh Test Suite (Enhanced)${NC}"
|
||||
echo -e "${BLUE}================================${NC}\n"
|
||||
|
||||
if [[ ! -f "install.sh" ]]; then
|
||||
@@ -543,6 +779,10 @@ main() {
|
||||
"test_dry_run"
|
||||
"test_flags"
|
||||
"test_shellcheck"
|
||||
"test_lock_file"
|
||||
"test_ignore_pattern_caching"
|
||||
"test_directory_caching"
|
||||
"test_safe_read_noninteractive"
|
||||
)
|
||||
|
||||
# Run tests
|
||||
@@ -577,6 +817,7 @@ cleanup() {
|
||||
echo "Cleaning up test files..."
|
||||
cleanup_test_env
|
||||
rm -f test_detection.sh test_ignore.sh test_safe_read.sh test_fresh_clone.sh test_substring_ignore.sh dry_run_output.txt 2>/dev/null || true
|
||||
rm -f test_caching.sh test_dir_cache.sh 2>/dev/null || true
|
||||
rm -rf "${HOME}/.config/test-app" 2>/dev/null || true
|
||||
}
|
||||
|
||||
@@ -584,4 +825,4 @@ trap cleanup EXIT INT TERM
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
fi
|
||||
+328
-111
@@ -25,21 +25,33 @@ set -euo pipefail
|
||||
|
||||
# TODO: Is this really needed? `git pull` should do a full upgrade, not partially, which means this script will be updated along with the folder structure together.
|
||||
# Try to find the packages directory (different names in different versions)
|
||||
if [[ -d "${REPO_ROOT}/dist-arch" ]]; then
|
||||
ARCH_PACKAGES_DIR="${REPO_ROOT}/dist-arch"
|
||||
elif [[ -d "${REPO_ROOT}/arch-packages" ]]; then
|
||||
ARCH_PACKAGES_DIR="${REPO_ROOT}/arch-packages"
|
||||
elif [[ -d "${REPO_ROOT}/sdist/arch" ]]; then
|
||||
ARCH_PACKAGES_DIR="${REPO_ROOT}/sdist/arch"
|
||||
else
|
||||
ARCH_PACKAGES_DIR="${REPO_ROOT}/dist-arch" # Default fallback
|
||||
if which pacman &>/dev/null; then
|
||||
if [[ -d "${REPO_ROOT}/dist-arch" ]]; then
|
||||
ARCH_PACKAGES_DIR="${REPO_ROOT}/dist-arch"
|
||||
elif [[ -d "${REPO_ROOT}/arch-packages" ]]; then
|
||||
ARCH_PACKAGES_DIR="${REPO_ROOT}/arch-packages"
|
||||
elif [[ -d "${REPO_ROOT}/sdist/arch" ]]; then
|
||||
ARCH_PACKAGES_DIR="${REPO_ROOT}/sdist/arch"
|
||||
else
|
||||
ARCH_PACKAGES_DIR="${REPO_ROOT}/dist-arch" # Default fallback
|
||||
fi
|
||||
fi
|
||||
UPDATE_IGNORE_FILE="${REPO_ROOT}/.updateignore"
|
||||
HOME_UPDATE_IGNORE_FILE="${HOME}/.updateignore"
|
||||
|
||||
# Global arrays for cached ignore patterns (performance optimization)
|
||||
declare -a IGNORE_PATTERNS=()
|
||||
declare -a IGNORE_SUBSTRING_PATTERNS=()
|
||||
|
||||
# Track created directories to avoid redundant mkdir calls
|
||||
declare -A CREATED_DIRS
|
||||
|
||||
# TODO: Is this really needed? `git pull` should do a full upgrade, not partially, which means this script will be updated along with the folder structure together.
|
||||
# Auto-detect repository structure
|
||||
detect_repo_structure() {
|
||||
if which pacman &>/dev/null; then
|
||||
return
|
||||
fi
|
||||
local found_dirs=()
|
||||
|
||||
# Check for dots/ prefixed structure
|
||||
@@ -75,114 +87,188 @@ detect_repo_structure() {
|
||||
# Directories to monitor for changes (will be auto-detected)
|
||||
MONITOR_DIRS=()
|
||||
|
||||
# Function to safely read input with terminal compatibility
|
||||
# Enhanced safe_read with better terminal handling
|
||||
safe_read() {
|
||||
local prompt="$1"
|
||||
local varname="$2"
|
||||
local default="${3:-}"
|
||||
|
||||
local input_value=""
|
||||
|
||||
# In non-interactive mode, use default immediately
|
||||
if [[ "$NON_INTERACTIVE" == true ]]; then
|
||||
if [[ -n "$default" ]]; then
|
||||
printf -v "$varname" '%s' "$default"
|
||||
return 0
|
||||
else
|
||||
log_error "Non-interactive mode requires default value for: $prompt"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -n "$prompt"
|
||||
if { read -r input_value </dev/tty; } 2>/dev/null || read -r input_value 2>/dev/null; then
|
||||
# Use printf instead of eval for security
|
||||
printf -v "$varname" '%s' "$input_value"
|
||||
return 0
|
||||
|
||||
# Try to read from terminal with better detection
|
||||
if [[ -t 0 ]]; then
|
||||
# stdin is a terminal
|
||||
read -r input_value
|
||||
elif [[ -r /dev/tty ]]; then
|
||||
# Try reading from tty
|
||||
if read -r input_value </dev/tty 2>/dev/null; then
|
||||
: # Success
|
||||
else
|
||||
input_value=""
|
||||
fi
|
||||
else
|
||||
# No interactive terminal available
|
||||
if [[ -n "$default" ]]; then
|
||||
echo
|
||||
log_warning "Using default: $default"
|
||||
log_warning "No terminal available. Using default: $default"
|
||||
printf -v "$varname" '%s' "$default"
|
||||
return 0
|
||||
else
|
||||
echo
|
||||
log_error "Failed to read input"
|
||||
log_error "No terminal available and no default provided"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$input_value" ]]; then
|
||||
printf -v "$varname" '%s' "$input_value"
|
||||
return 0
|
||||
elif [[ -n "$default" ]]; then
|
||||
echo
|
||||
log_warning "Empty input. Using default: $default"
|
||||
printf -v "$varname" '%s' "$default"
|
||||
return 0
|
||||
else
|
||||
echo
|
||||
log_error "Input required but not provided"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Load and cache ignore patterns for performance
|
||||
load_ignore_patterns() {
|
||||
IGNORE_PATTERNS=()
|
||||
IGNORE_SUBSTRING_PATTERNS=()
|
||||
|
||||
for ignore_file in "$UPDATE_IGNORE_FILE" "$HOME_UPDATE_IGNORE_FILE"; do
|
||||
[[ ! -f "$ignore_file" ]] && continue
|
||||
|
||||
while IFS= read -r pattern || [[ -n "$pattern" ]]; do
|
||||
# Skip empty lines and comments
|
||||
[[ -z "$pattern" || "$pattern" =~ ^[[:space:]]*# ]] && continue
|
||||
# Remove whitespace
|
||||
pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[[ -z "$pattern" ]] && continue
|
||||
|
||||
# Separate substring patterns from regular patterns
|
||||
if [[ "$pattern" == \*\** ]]; then
|
||||
IGNORE_SUBSTRING_PATTERNS+=("${pattern#\*\*}")
|
||||
else
|
||||
IGNORE_PATTERNS+=("$pattern")
|
||||
fi
|
||||
done < "$ignore_file"
|
||||
done
|
||||
|
||||
if [[ "$VERBOSE" == true ]]; then
|
||||
log_info "Loaded ${#IGNORE_PATTERNS[@]} ignore patterns and ${#IGNORE_SUBSTRING_PATTERNS[@]} substring patterns"
|
||||
fi
|
||||
}
|
||||
|
||||
# Optimized should_ignore using cached patterns
|
||||
should_ignore() {
|
||||
local file_path="$1"
|
||||
local relative_path="${file_path#$HOME/}"
|
||||
|
||||
# Also get path relative to repo for repo-level ignores
|
||||
local repo_relative=""
|
||||
|
||||
if [[ "$file_path" == "$REPO_ROOT"* ]]; then
|
||||
repo_relative="${file_path#$REPO_ROOT/}"
|
||||
fi
|
||||
|
||||
# Check both repo and home ignore files
|
||||
for ignore_file in "$UPDATE_IGNORE_FILE" "$HOME_UPDATE_IGNORE_FILE"; do
|
||||
if [[ -f "$ignore_file" ]]; then
|
||||
while IFS= read -r pattern || [[ -n "$pattern" ]]; do
|
||||
# Skip empty lines and comments
|
||||
[[ -z "$pattern" || "$pattern" =~ ^[[:space:]]*# ]] && continue
|
||||
# Remove leading/trailing whitespace
|
||||
pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[[ -z "$pattern" ]] && continue
|
||||
# Check regular patterns
|
||||
for pattern in "${IGNORE_PATTERNS[@]}"; do
|
||||
# Exact match
|
||||
if [[ "$relative_path" == "$pattern" ]] || [[ "$repo_relative" == "$pattern" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local should_skip=false
|
||||
# Wildcard patterns (basic glob matching)
|
||||
if [[ "$relative_path" == $pattern ]] || [[ "$repo_relative" == $pattern ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Exact match
|
||||
if [[ "$relative_path" == "$pattern" ]] || [[ "$repo_relative" == "$pattern" ]]; then
|
||||
should_skip=true
|
||||
fi
|
||||
# Directory patterns (ending with /)
|
||||
if [[ "$pattern" == */ ]]; then
|
||||
local dir_pattern="${pattern%/}"
|
||||
if [[ "$relative_path" == "$dir_pattern"/* ]] || [[ "$repo_relative" == "$dir_pattern"/* ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Wildcard patterns (basic glob matching)
|
||||
if [[ "$relative_path" == $pattern ]] || [[ "$repo_relative" == $pattern ]]; then
|
||||
should_skip=true
|
||||
fi
|
||||
# Root-relative patterns (starting with /)
|
||||
if [[ "$pattern" == /* ]]; then
|
||||
local root_pattern="${pattern#/}"
|
||||
if [[ "$relative_path" == "$root_pattern" ]] || [[ "$relative_path" == "$root_pattern"/* ]] ||
|
||||
[[ "$repo_relative" == "$root_pattern" ]] || [[ "$repo_relative" == "$root_pattern"/* ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Directory patterns (ending with /)
|
||||
if [[ "$pattern" == */ ]]; then
|
||||
local dir_pattern="${pattern%/}"
|
||||
if [[ "$relative_path" == "$dir_pattern"/* ]] || [[ "$repo_relative" == "$dir_pattern"/* ]]; then
|
||||
should_skip=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Patterns starting with / (from root)
|
||||
if [[ "$pattern" == /* ]]; then
|
||||
local root_pattern="${pattern#/}"
|
||||
if [[ "$relative_path" == "$root_pattern" ]] || [[ "$relative_path" == "$root_pattern"/* ]] ||
|
||||
[[ "$repo_relative" == "$root_pattern" ]] || [[ "$repo_relative" == "$root_pattern"/* ]]; then
|
||||
should_skip=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Patterns with wildcards
|
||||
if [[ "$pattern" == *"*"* ]]; then
|
||||
if [[ "$relative_path" == $pattern ]] || [[ "$repo_relative" == $pattern ]]; then
|
||||
should_skip=true
|
||||
fi
|
||||
# Check parent directories against pattern
|
||||
local temp_path="$relative_path"
|
||||
while [[ "$temp_path" == */* ]]; do
|
||||
temp_path="${temp_path%/*}"
|
||||
if [[ "$temp_path" == $pattern ]]; then
|
||||
should_skip=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Substring matching (only if pattern starts with '**')
|
||||
if [[ ! "$should_skip" == true && "$pattern" == \*\** ]]; then
|
||||
local substring_pattern="${pattern#\*\*}" # Remove the leading '**'
|
||||
if [[ -n "$substring_pattern" && ("$file_path" == *"$substring_pattern"* || "$relative_path" == *"$substring_pattern"*) ]]; then
|
||||
should_skip=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$should_skip" == true ]]; then
|
||||
# Patterns with wildcards - check parent directories
|
||||
if [[ "$pattern" == *"*"* ]]; then
|
||||
local temp_path="$relative_path"
|
||||
while [[ "$temp_path" == */* ]]; do
|
||||
temp_path="${temp_path%/*}"
|
||||
if [[ "$temp_path" == $pattern ]]; then
|
||||
return 0
|
||||
fi
|
||||
done <"$ignore_file"
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
# Check substring patterns
|
||||
for substring in "${IGNORE_SUBSTRING_PATTERNS[@]}"; do
|
||||
if [[ -n "$substring" && ("$file_path" == *"$substring"* || "$relative_path" == *"$substring"*) ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Efficient directory creation with caching
|
||||
ensure_directory() {
|
||||
local dir="$1"
|
||||
|
||||
# Check if already created in this run
|
||||
if [[ -n "${CREATED_DIRS[$dir]:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
if mkdir -p "$dir" 2>/dev/null; then
|
||||
CREATED_DIRS[$dir]=1
|
||||
if [[ "$VERBOSE" == true ]]; then
|
||||
log_info "Created directory: $dir"
|
||||
fi
|
||||
else
|
||||
log_error "Failed to create directory: $dir"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
CREATED_DIRS[$dir]=1
|
||||
fi
|
||||
else
|
||||
if [[ "$VERBOSE" == true ]] || [[ -z "${CREATED_DIRS[$dir]:-}" ]]; then
|
||||
log_info "[DRY-RUN] Would create directory: $dir"
|
||||
fi
|
||||
CREATED_DIRS[$dir]=1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to show file diff
|
||||
show_diff() {
|
||||
local file1="$1"
|
||||
@@ -201,6 +287,38 @@ show_diff() {
|
||||
echo "----------------------------------------"
|
||||
}
|
||||
|
||||
# Backup file before replacing
|
||||
backup_file() {
|
||||
local file="$1"
|
||||
local backup_dir="${REPO_ROOT}/.update-backups"
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would backup: $file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$file" ]]; then
|
||||
log_warning "File does not exist, cannot backup: $file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
ensure_directory "$backup_dir" || return 1
|
||||
|
||||
local backup_name
|
||||
local relative_name="${file#$HOME/}"
|
||||
backup_name="${relative_name//\//_}.${timestamp}.bak"
|
||||
|
||||
if cp -p "$file" "${backup_dir}/${backup_name}" 2>/dev/null; then
|
||||
log_info "Backed up to: .update-backups/${backup_name}"
|
||||
return 0
|
||||
else
|
||||
log_error "Failed to create backup"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to handle file conflicts
|
||||
handle_file_conflict() {
|
||||
local repo_file="$1"
|
||||
@@ -219,10 +337,11 @@ handle_file_conflict() {
|
||||
echo "5) Show diff and decide"
|
||||
echo "6) Skip this file"
|
||||
echo "7) Add to ignore and skip"
|
||||
echo "8) Backup to .update-backups/ and replace with repository version"
|
||||
echo
|
||||
|
||||
while true; do
|
||||
if ! safe_read "Enter your choice (1-7): " choice "6"; then
|
||||
if ! safe_read "Enter your choice (1-8): " choice "6"; then
|
||||
echo
|
||||
log_warning "Failed to read input. Skipping file."
|
||||
return
|
||||
@@ -271,8 +390,9 @@ handle_file_conflict() {
|
||||
echo "n) Save repository version as .new"
|
||||
echo "s) Skip this file"
|
||||
echo "i) Add to ignore and skip"
|
||||
echo "B) Backup to .update-backups/ and replace"
|
||||
|
||||
if ! safe_read "Enter your choice (r/k/b/n/s/i): " subchoice "s"; then
|
||||
if ! safe_read "Enter your choice (r/k/b/n/s/i/B): " subchoice "s"; then
|
||||
echo
|
||||
log_warning "Failed to read input. Skipping file."
|
||||
return
|
||||
@@ -325,6 +445,15 @@ handle_file_conflict() {
|
||||
fi
|
||||
break
|
||||
;;
|
||||
B)
|
||||
if backup_file "$home_file"; then
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
cp -p "$repo_file" "$home_file"
|
||||
log_success "Replaced $home_file with repository version"
|
||||
fi
|
||||
fi
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice. Please try again."
|
||||
;;
|
||||
@@ -344,8 +473,17 @@ handle_file_conflict() {
|
||||
fi
|
||||
break
|
||||
;;
|
||||
8)
|
||||
if backup_file "$home_file"; then
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
cp -p "$repo_file" "$home_file"
|
||||
log_success "Replaced $home_file with repository version"
|
||||
fi
|
||||
fi
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice. Please enter 1-7."
|
||||
echo "Invalid choice. Please enter 1-8."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
@@ -479,6 +617,7 @@ build_packages() {
|
||||
log_info "Package building cancelled by user"
|
||||
return
|
||||
fi
|
||||
|
||||
for pkg_name in "${packages_to_build[@]}"; do
|
||||
pkg_dir="${ARCH_PACKAGES_DIR}/${pkg_name}"
|
||||
|
||||
@@ -516,34 +655,40 @@ build_packages() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get list of changed files
|
||||
# Optimized function to get list of changed files
|
||||
get_changed_files() {
|
||||
local dir_path="$1"
|
||||
|
||||
if [[ "$FORCE_CHECK" == true ]]; then
|
||||
find "$dir_path" -type f -print0 2>/dev/null
|
||||
else
|
||||
# Check if we can use git diff (HEAD@{1} exists)
|
||||
if git rev-parse --verify HEAD@{1} &>/dev/null; then
|
||||
# Get files that changed in the last pull
|
||||
local has_changes=false
|
||||
return
|
||||
fi
|
||||
|
||||
# Try git-based detection first
|
||||
if git rev-parse --verify HEAD@{1} &>/dev/null 2>&1; then
|
||||
local temp_file
|
||||
temp_file=$(mktemp)
|
||||
|
||||
# Get changed files with specific filters (Added, Copied, Modified, Renamed)
|
||||
git diff --name-only --diff-filter=ACMR HEAD@{1} HEAD 2>/dev/null | \
|
||||
while IFS= read -r file; do
|
||||
local full_path="${REPO_ROOT}/${file}"
|
||||
if [[ "$full_path" == "$dir_path"/* ]] && [[ -f "$full_path" ]]; then
|
||||
printf '%s\0' "$full_path"
|
||||
has_changes=true
|
||||
echo "$full_path"
|
||||
fi
|
||||
done < <(git diff --name-only HEAD@{1} HEAD 2>/dev/null || true)
|
||||
|
||||
# If git diff found changes, we're done
|
||||
if [[ "$has_changes" == true ]]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
done > "$temp_file"
|
||||
|
||||
# Fallback: check all files (fresh clone or no git changes)
|
||||
find "$dir_path" -type f -print0 2>/dev/null
|
||||
if [[ -s "$temp_file" ]]; then
|
||||
# Found changes via git
|
||||
tr '\n' '\0' < "$temp_file"
|
||||
rm -f "$temp_file"
|
||||
return
|
||||
fi
|
||||
rm -f "$temp_file"
|
||||
fi
|
||||
|
||||
# Fallback: check all files
|
||||
find "$dir_path" -type f -print0 2>/dev/null
|
||||
}
|
||||
|
||||
# Function to check if we have new commits
|
||||
@@ -556,6 +701,40 @@ has_new_commits() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Cleanup function for signal handling
|
||||
cleanup_on_exit() {
|
||||
local exit_code=$?
|
||||
|
||||
# Remove lock file
|
||||
rm -f "${REPO_ROOT}/.update-lock" 2>/dev/null || true
|
||||
|
||||
if [[ $exit_code -ne 0 ]] && [[ "$DRY_RUN" != true ]]; then
|
||||
echo
|
||||
log_warning "Update interrupted or failed (exit code: $exit_code)"
|
||||
log_info "System may be in an inconsistent state"
|
||||
log_info "Run the update again to complete the process"
|
||||
fi
|
||||
}
|
||||
|
||||
# Set up signal handling and lock file
|
||||
trap cleanup_on_exit EXIT INT TERM
|
||||
|
||||
# Check for concurrent runs
|
||||
if [[ -f "${REPO_ROOT}/.update-lock" ]] && [[ "$DRY_RUN" != true ]]; then
|
||||
# Check if the process is still running
|
||||
if kill -0 $(cat "${REPO_ROOT}/.update-lock" 2>/dev/null) 2>/dev/null; then
|
||||
log_die "Another update is already running (PID: $(cat "${REPO_ROOT}/.update-lock"))"
|
||||
else
|
||||
log_warning "Found stale lock file, removing..."
|
||||
rm -f "${REPO_ROOT}/.update-lock"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create lock file with current PID
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
echo $$ > "${REPO_ROOT}/.update-lock"
|
||||
fi
|
||||
|
||||
# Main script starts here
|
||||
log_header "Dotfiles Update Script"
|
||||
|
||||
@@ -598,6 +777,9 @@ else
|
||||
log_die "Failed to detect repository structure. Make sure you're in the correct directory."
|
||||
fi
|
||||
|
||||
# Load ignore patterns once at startup (performance optimization)
|
||||
load_ignore_patterns
|
||||
|
||||
# Step 1: Pull latest commits
|
||||
log_header "Pulling Latest Changes"
|
||||
|
||||
@@ -646,6 +828,12 @@ if git remote get-url origin &>/dev/null; then
|
||||
else
|
||||
if git pull --ff-only; then
|
||||
log_success "Successfully pulled latest changes"
|
||||
# Verify we actually got new commits
|
||||
if git rev-parse --verify HEAD@{1} &>/dev/null; then
|
||||
if [[ "$(git rev-parse HEAD)" == "$(git rev-parse HEAD@{1})" ]]; then
|
||||
log_info "Already up to date with remote"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_warning "Failed to pull changes from remote. Continuing with local repository..."
|
||||
log_info "You may need to resolve conflicts manually later."
|
||||
@@ -783,6 +971,16 @@ if [[ "$process_files" == true ]]; then
|
||||
files_processed=0
|
||||
files_updated=0
|
||||
files_created=0
|
||||
|
||||
# Count total files for progress indication (optional)
|
||||
total_files=0
|
||||
if [[ "$VERBOSE" == false ]] && command -v tput &>/dev/null 2>&1; then
|
||||
for dir_name in "${MONITOR_DIRS[@]}"; do
|
||||
repo_dir_path="${REPO_ROOT}/${dir_name}"
|
||||
[[ ! -d "$repo_dir_path" ]] && continue
|
||||
total_files=$((total_files + $(find "$repo_dir_path" -type f 2>/dev/null | wc -l)))
|
||||
done
|
||||
fi
|
||||
|
||||
for dir_name in "${MONITOR_DIRS[@]}"; do
|
||||
repo_dir_path="${REPO_ROOT}/${dir_name}"
|
||||
@@ -806,11 +1004,7 @@ if [[ "$process_files" == true ]]; then
|
||||
|
||||
log_info "Processing directory: $dir_name → ${home_dir_path}"
|
||||
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
mkdir -p "$home_dir_path"
|
||||
else
|
||||
log_info "[DRY-RUN] Would create directory: $home_dir_path"
|
||||
fi
|
||||
ensure_directory "$home_dir_path" || continue
|
||||
|
||||
while IFS= read -r -d '' repo_file; do
|
||||
# Calculate relative path from the repo source directory
|
||||
@@ -829,13 +1023,21 @@ if [[ "$process_files" == true ]]; then
|
||||
fi
|
||||
|
||||
((files_processed++))
|
||||
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
mkdir -p "$(dirname "$home_file")"
|
||||
|
||||
# Show progress for non-verbose mode
|
||||
if [[ "$VERBOSE" == false ]] && command -v tput &>/dev/null 2>&1 && [[ $total_files -gt 0 ]]; then
|
||||
printf "\r[INFO] Processing files: %d/%d" "$files_processed" "$total_files" >&2
|
||||
fi
|
||||
|
||||
ensure_directory "$(dirname "$home_file")" || continue
|
||||
|
||||
if [[ -f "$home_file" ]]; then
|
||||
if ! cmp -s "$repo_file" "$home_file"; then
|
||||
# Clear progress line if showing
|
||||
if [[ "$VERBOSE" == false ]] && command -v tput &>/dev/null 2>&1 && [[ $total_files -gt 0 ]]; then
|
||||
printf "\r%*s\r" "80" "" >&2
|
||||
fi
|
||||
|
||||
log_info "Found difference in: $rel_path"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_warning "[DRY-RUN] Conflict detected (would prompt): $home_file"
|
||||
@@ -847,16 +1049,25 @@ if [[ "$process_files" == true ]]; then
|
||||
fi
|
||||
else
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would create new file: $home_file"
|
||||
if [[ "$VERBOSE" == true ]]; then
|
||||
log_info "[DRY-RUN] Would create new file: $home_file"
|
||||
fi
|
||||
else
|
||||
cp -p "$repo_file" "$home_file"
|
||||
log_success "Created new file: $home_file"
|
||||
if [[ "$VERBOSE" == true ]]; then
|
||||
log_success "Created new file: $home_file"
|
||||
fi
|
||||
fi
|
||||
((files_created++))
|
||||
fi
|
||||
done < <(get_changed_files "$repo_dir_path") || true
|
||||
done
|
||||
|
||||
# Clear progress line if it was shown
|
||||
if [[ "$VERBOSE" == false ]] && command -v tput &>/dev/null 2>&1 && [[ $total_files -gt 0 ]]; then
|
||||
printf "\r%*s\r" "80" "" >&2
|
||||
fi
|
||||
|
||||
echo
|
||||
log_info "File processing summary:"
|
||||
log_info "- Files processed: $files_processed"
|
||||
@@ -919,7 +1130,13 @@ if [[ ! -f "$HOME_UPDATE_IGNORE_FILE" && ! -f "$UPDATE_IGNORE_FILE" ]]; then
|
||||
echo " .config/personal/ # Ignore entire directory"
|
||||
echo " secret-config.conf # Ignore specific file"
|
||||
echo " /temp-file # Ignore from root only"
|
||||
echo " *secret* # Ignore files containing 'secret'"
|
||||
echo " **secret** # Ignore files containing 'secret'"
|
||||
fi
|
||||
|
||||
echo
|
||||
# Show backup directory if any backups were created
|
||||
if [[ -d "${REPO_ROOT}/.update-backups" ]] && [[ "$DRY_RUN" != true ]]; then
|
||||
echo
|
||||
log_info "Backups stored in: ${REPO_ROOT}/.update-backups/"
|
||||
fi
|
||||
|
||||
echo
|
||||
Reference in New Issue
Block a user