forked from Shinonome/dots-hyprland
Rename scripts, move into sdata/step
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# This script is meant to be sourced.
|
||||
# It's not for directly running.
|
||||
|
||||
printf 'Hi there!\n'
|
||||
printf 'This script 1. will uninstall [end-4/dots-hyprland > illogical-impulse] dotfiles\n'
|
||||
printf ' 2. will try to revert *mostly everything* installed using install.sh, so it'\''s pretty destructive\n'
|
||||
printf ' 3. has not been tested, use at your own risk.\n'
|
||||
printf ' 4. will show all commands that it runs.\n'
|
||||
printf 'Ctrl+C to exit. Enter to continue.\n'
|
||||
read -r
|
||||
##############################################################################################################################
|
||||
|
||||
# Undo Step 3: Removing copied config and local folders
|
||||
printf "${STY_CYAN}Removing copied config and local folders...\n${STY_RST}"
|
||||
|
||||
dirs=(
|
||||
Kvantum/
|
||||
fish/
|
||||
fontconfig/
|
||||
foot/
|
||||
fuzzel/
|
||||
hypr/
|
||||
kde-material-you-colors/
|
||||
kitty/
|
||||
matugen/
|
||||
mpv/
|
||||
qt5ct/
|
||||
qt6ct/
|
||||
quickshell/
|
||||
wlogout/
|
||||
xdg-desktop-portal/
|
||||
zshrc.d/
|
||||
chrome-flags.conf
|
||||
code-flags.conf
|
||||
darklyrc
|
||||
dolphinrc
|
||||
kdeglobals
|
||||
konsolerc
|
||||
starship.toml
|
||||
thorium-flags.conf
|
||||
)
|
||||
|
||||
for i in ${dirs[@]}
|
||||
do v rm -rf "$XDG_CONFIG_HOME/$i"
|
||||
done
|
||||
|
||||
for i in "glib-2.0/schemas/com.github.GradienceTeam.Gradience.Devel.gschema.xml" "gradience"
|
||||
do v rm -rf "$XDG_DATA_HOME/$i"
|
||||
done
|
||||
v rm -rf "$XDG_CACHE_HOME/quickshell"
|
||||
v sudo rm -rf "$XDG_STATE_HOME/quickshell"
|
||||
|
||||
##############################################################################################################################
|
||||
|
||||
# Undo Step 2: Uninstall AGS - Disabled for now, check issues
|
||||
# echo 'Uninstalling AGS...'
|
||||
# sudo meson uninstall -C ~/ags/build
|
||||
# rm -rf ~/ags
|
||||
|
||||
##############################################################################################################################
|
||||
|
||||
# Undo Step 1: Remove added user from video, i2c, and input groups and remove yay packages
|
||||
printf "${STY_CYAN}Removing user from video, i2c, and input groups and removing packages...\n${STY_RST}"
|
||||
user=$(whoami)
|
||||
v sudo gpasswd -d "$user" video
|
||||
v sudo gpasswd -d "$user" i2c
|
||||
v sudo gpasswd -d "$user" input
|
||||
v sudo rm /etc/modules-load.d/i2c-dev.conf
|
||||
|
||||
##############################################################################################################################
|
||||
read -p "Do you want to uninstall the illogical-impulse-* meta packages (Arch Linux only)?
|
||||
Ctrl+C to exit, or press Enter to proceed" p
|
||||
|
||||
# Removing installed yay packages and dependencies
|
||||
v yay -Rns illogical-impulse-{audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,screencapture,toolkit,widgets} plasma-browser-integration
|
||||
|
||||
printf "${STY_CYAN}Uninstall Complete.\n${STY_RST}"
|
||||
printf "${STY_CYAN}Hint: If you had agreed to backup when you ran \"install.sh\", you should be able to find it under \"$BACKUP_DIR\".\n${STY_RST}"
|
||||
Executable
+592
@@ -0,0 +1,592 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# exp-update-tester.sh - Test suite for update.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
TEST_DIR=""
|
||||
ORIGINAL_DIR="$PWD"
|
||||
|
||||
# Helper functions
|
||||
log_test() {
|
||||
echo -e "${BLUE}[TEST]${NC} $1"
|
||||
}
|
||||
|
||||
log_pass() {
|
||||
echo -e "${GREEN}[PASS]${NC} $1"
|
||||
((TESTS_PASSED++))
|
||||
}
|
||||
|
||||
log_fail() {
|
||||
echo -e "${RED}[FAIL]${NC} $1"
|
||||
((TESTS_FAILED++))
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${YELLOW}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
# Setup test environment
|
||||
setup_test_env() {
|
||||
local temp_dir
|
||||
temp_dir=$(mktemp -d -t dotfiles-test.XXXXXX)
|
||||
|
||||
cd "$temp_dir" || { echo "Failed to cd to test directory"; return 1; }
|
||||
git init -q
|
||||
git config user.email "test@example.com"
|
||||
git config user.name "Test User"
|
||||
|
||||
git commit --allow-empty -m "Initial commit" -q
|
||||
|
||||
echo "$temp_dir"
|
||||
}
|
||||
|
||||
# Cleanup test environment
|
||||
cleanup_test_env() {
|
||||
if [[ -n "${TEST_DIR:-}" && -d "$TEST_DIR" ]]; then
|
||||
rm -rf "$TEST_DIR"
|
||||
TEST_DIR=""
|
||||
fi
|
||||
}
|
||||
|
||||
# Run a test and handle cleanup
|
||||
run_test() {
|
||||
local test_name="$1"
|
||||
local test_func="$2"
|
||||
|
||||
# Cleanup before test
|
||||
cleanup_test_env
|
||||
|
||||
# Run the test
|
||||
if $test_func; then
|
||||
echo "✓ $test_name passed"
|
||||
return 0
|
||||
else
|
||||
echo "✗ $test_name failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 1: Script exists and is executable
|
||||
test_script_exists() {
|
||||
log_test "Checking if update.sh exists and is executable"
|
||||
|
||||
if [[ ! -f "update.sh" ]]; then
|
||||
log_fail "update.sh not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -x "update.sh" ]]; then
|
||||
log_fail "update.sh is not executable"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_pass "Script exists and is executable"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test 2: Script has no syntax errors
|
||||
test_syntax() {
|
||||
log_test "Checking script syntax"
|
||||
|
||||
if bash -n update.sh; then
|
||||
log_pass "No syntax errors found"
|
||||
return 0
|
||||
else
|
||||
log_fail "Syntax errors detected"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 3: Help option works
|
||||
test_help_option() {
|
||||
log_test "Testing --help option"
|
||||
|
||||
if ./update.sh --help 2>&1 | grep -q "Usage:"; then
|
||||
log_pass "Help option works"
|
||||
return 0
|
||||
else
|
||||
log_fail "Help option failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 4: Test repository structure detection (dots/ prefix)
|
||||
test_dots_structure() {
|
||||
log_test "Testing dots/ prefix structure detection"
|
||||
|
||||
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; }
|
||||
|
||||
mkdir -p dots/.config/test-app
|
||||
mkdir -p dots/.local/bin
|
||||
echo "test config" > dots/.config/test-app/config.conf
|
||||
|
||||
git add .
|
||||
git commit -m "Add dots structure" -q
|
||||
|
||||
cat > test_detection.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
REPO_ROOT="$1"
|
||||
detect_repo_structure() {
|
||||
local found_dirs=()
|
||||
if [[ -d "${REPO_ROOT}/dots/.config" ]]; then
|
||||
found_dirs+=("dots/.config")
|
||||
[[ -d "${REPO_ROOT}/dots/.local/bin" ]] && found_dirs+=("dots/.local/bin")
|
||||
elif [[ -d "${REPO_ROOT}/.config" ]]; then
|
||||
found_dirs+=(".config")
|
||||
[[ -d "${REPO_ROOT}/.local/bin" ]] && found_dirs+=(".local/bin")
|
||||
else
|
||||
for candidate in "dots/.config" ".config" "dots/.local/bin" ".local/bin"; do
|
||||
if [[ -d "${REPO_ROOT}/${candidate}" ]]; then
|
||||
if [[ ! " ${found_dirs[*]} " =~ " ${candidate} " ]]; then
|
||||
found_dirs+=("${candidate}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ ${#found_dirs[@]} -eq 0 ]]; then
|
||||
echo "ERROR" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "${found_dirs[@]}"
|
||||
}
|
||||
detect_repo_structure
|
||||
EOF
|
||||
|
||||
chmod +x test_detection.sh
|
||||
result=$(./test_detection.sh "$test_repo")
|
||||
|
||||
if [[ "$result" == *"dots/.config"* ]]; then
|
||||
log_pass "Dots structure detected correctly"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 0
|
||||
else
|
||||
log_fail "Failed to detect dots structure. Got: $result"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 5: Test flat structure detection
|
||||
test_flat_structure() {
|
||||
log_test "Testing flat structure detection"
|
||||
|
||||
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; }
|
||||
|
||||
mkdir -p .config/test-app
|
||||
mkdir -p .local/bin
|
||||
echo "test config" > .config/test-app/config.conf
|
||||
|
||||
git add .
|
||||
git commit -m "Add flat structure" -q
|
||||
|
||||
cat > test_detection.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
REPO_ROOT="$1"
|
||||
detect_repo_structure() {
|
||||
local found_dirs=()
|
||||
if [[ -d "${REPO_ROOT}/dots/.config" ]]; then
|
||||
found_dirs+=("dots/.config")
|
||||
[[ -d "${REPO_ROOT}/dots/.local/bin" ]] && found_dirs+=("dots/.local/bin")
|
||||
elif [[ -d "${REPO_ROOT}/.config" ]]; then
|
||||
found_dirs+=(".config")
|
||||
[[ -d "${REPO_ROOT}/.local/bin" ]] && found_dirs+=(".local/bin")
|
||||
else
|
||||
for candidate in "dots/.config" ".config" "dots/.local/bin" ".local/bin"; do
|
||||
if [[ -d "${REPO_ROOT}/${candidate}" ]]; then
|
||||
if [[ ! " ${found_dirs[*]} " =~ " ${candidate} " ]]; then
|
||||
found_dirs+=("${candidate}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ ${#found_dirs[@]} -eq 0 ]]; then
|
||||
echo "ERROR" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "${found_dirs[@]}"
|
||||
}
|
||||
detect_repo_structure
|
||||
EOF
|
||||
|
||||
chmod +x test_detection.sh
|
||||
result=$(./test_detection.sh "$test_repo")
|
||||
|
||||
if [[ "$result" == *".config"* ]] && [[ "$result" != *"dots/"* ]]; then
|
||||
log_pass "Flat structure detected correctly"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 0
|
||||
else
|
||||
log_fail "Failed to detect flat structure. Got: $result"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 6: Test dots prefix mapping to home directory
|
||||
test_dots_mapping() {
|
||||
log_test "Testing dots/ prefix home directory mapping"
|
||||
|
||||
dir_name="dots/.config"
|
||||
if [[ "$dir_name" == dots/* ]]; then
|
||||
home_subdir="${dir_name#dots/}"
|
||||
home_dir_path="${HOME}/${home_subdir}"
|
||||
else
|
||||
home_dir_path="${HOME}/${dir_name}"
|
||||
fi
|
||||
|
||||
expected_path="${HOME}/.config"
|
||||
if [[ "$home_dir_path" == "$expected_path" ]]; then
|
||||
log_pass "Dots prefix mapping correct: $dir_name → $home_dir_path"
|
||||
return 0
|
||||
else
|
||||
log_fail "Dots prefix mapping failed: $dir_name → $home_dir_path (expected: $expected_path)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 7: Test ignore file patterns
|
||||
test_ignore_patterns() {
|
||||
log_test "Testing ignore file pattern matching"
|
||||
|
||||
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 > .updateignore << 'EOF'
|
||||
*.log
|
||||
secrets/
|
||||
.config/private*
|
||||
*backup*
|
||||
EOF
|
||||
|
||||
mkdir -p .config
|
||||
mkdir -p secrets
|
||||
|
||||
cat > test_ignore.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
REPO_ROOT="$1"
|
||||
UPDATE_IGNORE_FILE="${REPO_ROOT}/.updateignore"
|
||||
HOME_UPDATE_IGNORE_FILE="/dev/null"
|
||||
|
||||
should_ignore() {
|
||||
local file_path="$1"
|
||||
local relative_path="${file_path#$HOME/}"
|
||||
local repo_relative=""
|
||||
if [[ "$file_path" == "$REPO_ROOT"* ]]; then
|
||||
repo_relative="${file_path#$REPO_ROOT/}"
|
||||
fi
|
||||
|
||||
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
|
||||
[[ -z "$pattern" || "$pattern" =~ ^[[:space:]]*# ]] && continue
|
||||
pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[[ -z "$pattern" ]] && continue
|
||||
|
||||
if [[ "$relative_path" == "$pattern" ]] || [[ "$repo_relative" == "$pattern" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$relative_path" == $pattern ]] || [[ "$repo_relative" == $pattern ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$pattern" == */ ]]; then
|
||||
local dir_pattern="${pattern%/}"
|
||||
if [[ "$relative_path" == "$dir_pattern"/* ]] || [[ "$repo_relative" == "$dir_pattern"/* ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [[ "$file_path" == *"$pattern"* ]] || [[ "$relative_path" == *"$pattern"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done <"$ignore_file"
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
all_passed=true
|
||||
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
|
||||
result=0
|
||||
else
|
||||
result=1
|
||||
fi
|
||||
|
||||
if [[ $result -ne $expected ]]; then
|
||||
echo "FAIL: $file (expected: $expected, got: $result)"
|
||||
all_passed=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$all_passed" == true ]]; then
|
||||
echo "PASS"
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x test_ignore.sh
|
||||
result=$(./test_ignore.sh "$test_repo")
|
||||
|
||||
if [[ "$result" == "PASS" ]]; then
|
||||
log_pass "All ignore pattern tests passed"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 0
|
||||
else
|
||||
log_fail "Some ignore pattern tests failed"
|
||||
echo "$result"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 8: Test safe_read security - COMPLETELY NON-INTERACTIVE
|
||||
test_safe_read_security() {
|
||||
log_test "Testing safe_read uses secure assignment (printf -v)"
|
||||
|
||||
# Check that safe_read uses printf -v and not eval
|
||||
if grep -A 10 "safe_read()" update.sh | grep -q "printf -v.*varname"; then
|
||||
log_pass "safe_read uses secure printf -v assignment"
|
||||
return 0
|
||||
elif grep -A 10 "safe_read()" update.sh | grep -q "eval.*varname"; then
|
||||
log_fail "safe_read uses vulnerable eval assignment"
|
||||
return 1
|
||||
else
|
||||
log_fail "Cannot determine safe_read assignment method"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 9: Test dry-run mode
|
||||
test_dry_run() {
|
||||
log_test "Testing dry-run mode"
|
||||
|
||||
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; }
|
||||
|
||||
mkdir -p dots/.config/test-app
|
||||
echo "repo config" > dots/.config/test-app/config.conf
|
||||
|
||||
git add .
|
||||
git commit -m "Add test config" -q
|
||||
|
||||
cp "$ORIGINAL_DIR/update.sh" .
|
||||
chmod +x update.sh
|
||||
|
||||
# Use printf to pipe responses automatically
|
||||
printf "y\ny\n" | ./update.sh -n --skip-notice 2>&1 | tee dry_run_output.txt
|
||||
|
||||
if grep -q "DRY-RUN" dry_run_output.txt; then
|
||||
log_pass "Dry-run mode detected in output"
|
||||
else
|
||||
log_fail "Dry-run mode not properly indicated"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${HOME}/.config/test-app/config.conf" ]]; then
|
||||
log_pass "No files created in home during dry-run"
|
||||
else
|
||||
log_fail "Files were created in home during dry-run"
|
||||
rm -f "${HOME}/.config/test-app/config.conf"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test 10: Test command-line flags
|
||||
test_flags() {
|
||||
log_test "Testing command-line flags"
|
||||
|
||||
# Only test non-interactive flags
|
||||
local flags=("-h" "--help")
|
||||
local all_passed=true
|
||||
|
||||
for flag in "${flags[@]}"; do
|
||||
if ./update.sh "$flag" 2>&1 | grep -q -E "(Usage|help)"; then
|
||||
log_info " ✓ $flag recognized"
|
||||
else
|
||||
log_info " ✗ $flag not recognized"
|
||||
all_passed=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$all_passed" == true ]]; then
|
||||
log_pass "Help flags recognized correctly"
|
||||
return 0
|
||||
else
|
||||
log_fail "Some flags not recognized properly"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 11: Check for shellcheck
|
||||
test_shellcheck() {
|
||||
log_test "Running shellcheck (if available)"
|
||||
|
||||
if ! command -v shellcheck &>/dev/null; then
|
||||
log_info "shellcheck not found, skipping static analysis"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if shellcheck -e SC1090,SC1091,SC2148,SC2034,SC2155,SC2164 update.sh; then
|
||||
log_pass "shellcheck passed"
|
||||
return 0
|
||||
else
|
||||
log_fail "shellcheck found issues"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 12: Test fresh clone scenario
|
||||
test_fresh_clone() {
|
||||
log_test "Testing fresh clone scenario"
|
||||
|
||||
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; }
|
||||
|
||||
mkdir -p .config/test-app
|
||||
echo "config" > .config/test-app/settings.conf
|
||||
|
||||
cat > test_fresh_clone.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
has_new_commits() {
|
||||
if git rev-parse --verify HEAD@{1} &>/dev/null; then
|
||||
[[ "$(git rev-parse HEAD)" != "$(git rev-parse HEAD@{1})" ]]
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
if has_new_commits; then
|
||||
echo "PASS"
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x test_fresh_clone.sh
|
||||
result=$(./test_fresh_clone.sh)
|
||||
|
||||
if [[ "$result" == "PASS" ]]; then
|
||||
log_pass "Fresh clone scenario handled correctly"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 0
|
||||
else
|
||||
log_fail "Fresh clone scenario not handled properly"
|
||||
cd "$ORIGINAL_DIR"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main test runner
|
||||
main() {
|
||||
echo -e "${BLUE}================================${NC}"
|
||||
echo -e "${BLUE} Update.sh Test Suite${NC}"
|
||||
echo -e "${BLUE}================================${NC}\n"
|
||||
|
||||
if [[ ! -f "update.sh" ]]; then
|
||||
log_error "Please run this test from the directory containing update.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x update.sh 2>/dev/null || true
|
||||
|
||||
# Define tests
|
||||
tests=(
|
||||
"test_script_exists"
|
||||
"test_syntax"
|
||||
"test_help_option"
|
||||
"test_dots_structure"
|
||||
"test_flat_structure"
|
||||
"test_dots_mapping"
|
||||
"test_ignore_patterns"
|
||||
"test_safe_read_security"
|
||||
"test_dry_run"
|
||||
"test_flags"
|
||||
"test_shellcheck"
|
||||
"test_fresh_clone"
|
||||
)
|
||||
|
||||
# Run tests
|
||||
for test in "${tests[@]}"; do
|
||||
if $test; then
|
||||
echo "✓ $test passed"
|
||||
else
|
||||
echo "✗ $test failed"
|
||||
fi
|
||||
echo
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo -e "${BLUE}================================${NC}"
|
||||
echo -e "${BLUE} Test Summary${NC}"
|
||||
echo -e "${BLUE}================================${NC}"
|
||||
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
|
||||
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
|
||||
echo -e "${BLUE}Total: ${#tests[@]}${NC}\n"
|
||||
|
||||
if [[ $TESTS_FAILED -eq 0 ]]; then
|
||||
echo -e "${GREEN}All tests passed! 🎉${NC}\n"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Some tests failed! ❌${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Global cleanup
|
||||
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 dry_run_output.txt 2>/dev/null || true
|
||||
rm -rf "${HOME}/.config/test-app" 2>/dev/null || true
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
@@ -0,0 +1,895 @@
|
||||
# This script is meant to be sourced.
|
||||
# It's not for directly running.
|
||||
|
||||
#####################################################################################
|
||||
#
|
||||
# update.sh - Enhanced dotfiles update script
|
||||
#
|
||||
# Features:
|
||||
# - Auto-detect repository structure (dots/ prefix or direct config)
|
||||
# - Pull latest commits from remote
|
||||
# - Rebuild packages if PKGBUILD files changed (user choice)
|
||||
# - Handle config file conflicts with user choices
|
||||
# - Respect .updateignore file for exclusions
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# TODO: For Arch(-Linux) specific part please check if pacman exists first, if not it should be skipped.
|
||||
|
||||
# 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
|
||||
fi
|
||||
UPDATE_IGNORE_FILE="${REPO_ROOT}/.updateignore"
|
||||
HOME_UPDATE_IGNORE_FILE="${HOME}/.updateignore"
|
||||
|
||||
# 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() {
|
||||
local found_dirs=()
|
||||
|
||||
# Check for dots/ prefixed structure
|
||||
if [[ -d "${REPO_ROOT}/dots/.config" ]]; then
|
||||
found_dirs+=("dots/.config")
|
||||
[[ -d "${REPO_ROOT}/dots/.local/bin" ]] && found_dirs+=("dots/.local/bin")
|
||||
[[ -d "${REPO_ROOT}/dots/.local/share" ]] && found_dirs+=("dots/.local/share")
|
||||
# Check for flat structure
|
||||
elif [[ -d "${REPO_ROOT}/.config" ]]; then
|
||||
found_dirs+=(".config")
|
||||
[[ -d "${REPO_ROOT}/.local/bin" ]] && found_dirs+=(".local/bin")
|
||||
[[ -d "${REPO_ROOT}/.local/share" ]] && found_dirs+=(".local/share")
|
||||
else
|
||||
# Manual detection of common directories
|
||||
for candidate in "dots/.config" ".config" "config" "dots/.local/bin" ".local/bin" "dots/.local/share" ".local/share"; do
|
||||
if [[ -d "${REPO_ROOT}/${candidate}" ]]; then
|
||||
# Avoid duplicates
|
||||
if [[ ! " ${found_dirs[*]} " =~ " ${candidate} " ]]; then
|
||||
found_dirs+=("${candidate}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ${#found_dirs[@]} -eq 0 ]]; then
|
||||
echo "ERROR: Could not detect repository structure" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "${found_dirs[@]}"
|
||||
}
|
||||
|
||||
# Directories to monitor for changes (will be auto-detected)
|
||||
MONITOR_DIRS=()
|
||||
|
||||
# Function to safely read input with terminal compatibility
|
||||
safe_read() {
|
||||
local prompt="$1"
|
||||
local varname="$2"
|
||||
local default="${3:-}"
|
||||
|
||||
local input_value=""
|
||||
|
||||
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
|
||||
else
|
||||
if [[ -n "$default" ]]; then
|
||||
echo
|
||||
log_warning "Using default: $default"
|
||||
printf -v "$varname" '%s' "$default"
|
||||
return 0
|
||||
else
|
||||
echo
|
||||
log_error "Failed to read input"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check if a file should be ignored
|
||||
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
|
||||
|
||||
local should_skip=false
|
||||
|
||||
# Exact match
|
||||
if [[ "$relative_path" == "$pattern" ]] || [[ "$repo_relative" == "$pattern" ]]; then
|
||||
should_skip=true
|
||||
fi
|
||||
|
||||
# Wildcard patterns (basic glob matching)
|
||||
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
|
||||
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
|
||||
|
||||
# Simple substring matching (for backward compatibility)
|
||||
if [[ ! "$should_skip" == true ]]; then
|
||||
if [[ "$file_path" == *"$pattern"* ]] || [[ "$relative_path" == *"$pattern"* ]]; then
|
||||
should_skip=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$should_skip" == true ]]; then
|
||||
return 0
|
||||
fi
|
||||
done <"$ignore_file"
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to show file diff
|
||||
show_diff() {
|
||||
local file1="$1"
|
||||
local file2="$2"
|
||||
|
||||
echo -e "\n${STY_CYAN}Showing differences:${STY_RST}"
|
||||
echo -e "${STY_CYAN}Old file: $file1${STY_RST}"
|
||||
echo -e "${STY_CYAN}New file: $file2${STY_RST}"
|
||||
echo "----------------------------------------"
|
||||
|
||||
if command -v diff &>/dev/null; then
|
||||
diff -u "$file1" "$file2" || true
|
||||
else
|
||||
echo "diff command not available"
|
||||
fi
|
||||
echo "----------------------------------------"
|
||||
}
|
||||
|
||||
# Function to handle file conflicts
|
||||
handle_file_conflict() {
|
||||
local repo_file="$1"
|
||||
local home_file="$2"
|
||||
local filename=$(basename "$home_file")
|
||||
local dirname=$(dirname "$home_file")
|
||||
|
||||
echo -e "\n${STY_YELLOW}Conflict detected:${STY_RST} $home_file"
|
||||
echo "Repository version differs from your local version."
|
||||
echo
|
||||
echo "Choose an action:"
|
||||
echo "1) Replace local file with repository version"
|
||||
echo "2) Keep local file unchanged"
|
||||
echo "3) Backup local file as ${filename}.old, use repository version"
|
||||
echo "4) Save repository version as ${filename}.new, keep local file"
|
||||
echo "5) Show diff and decide"
|
||||
echo "6) Skip this file"
|
||||
echo "7) Add to ignore and skip"
|
||||
echo
|
||||
|
||||
while true; do
|
||||
if ! safe_read "Enter your choice (1-7): " choice "6"; then
|
||||
echo
|
||||
log_warning "Failed to read input. Skipping file."
|
||||
return
|
||||
fi
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would replace $home_file with repository version"
|
||||
else
|
||||
cp -p "$repo_file" "$home_file"
|
||||
log_success "Replaced $home_file with repository version"
|
||||
fi
|
||||
break
|
||||
;;
|
||||
2)
|
||||
log_info "Keeping local version of $home_file"
|
||||
break
|
||||
;;
|
||||
3)
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would backup local file to ${filename}.old and update with repository version"
|
||||
else
|
||||
mv "$home_file" "${dirname}/${filename}.old"
|
||||
cp -p "$repo_file" "$home_file"
|
||||
log_success "Backed up local file to ${filename}.old and updated with repository version"
|
||||
fi
|
||||
break
|
||||
;;
|
||||
4)
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would save repository version as ${filename}.new, keep local file"
|
||||
else
|
||||
cp -p "$repo_file" "${dirname}/${filename}.new"
|
||||
log_success "Saved repository version as ${filename}.new, kept local file"
|
||||
fi
|
||||
break
|
||||
;;
|
||||
5)
|
||||
show_diff "$home_file" "$repo_file"
|
||||
echo
|
||||
echo "After reviewing the diff, choose:"
|
||||
echo "r) Replace with repository version"
|
||||
echo "k) Keep local version"
|
||||
echo "b) Backup local and use repository version"
|
||||
echo "n) Save repository version as .new"
|
||||
echo "s) Skip this file"
|
||||
echo "i) Add to ignore and skip"
|
||||
|
||||
if ! safe_read "Enter your choice (r/k/b/n/s/i): " subchoice "s"; then
|
||||
echo
|
||||
log_warning "Failed to read input. Skipping file."
|
||||
return
|
||||
fi
|
||||
|
||||
case $subchoice in
|
||||
r)
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would replace $home_file with repository version"
|
||||
else
|
||||
cp -p "$repo_file" "$home_file"
|
||||
log_success "Replaced $home_file with repository version"
|
||||
fi
|
||||
break
|
||||
;;
|
||||
k)
|
||||
log_info "Keeping local version of $home_file"
|
||||
break
|
||||
;;
|
||||
b)
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would backup local file to ${filename}.old and update"
|
||||
else
|
||||
mv "$home_file" "${dirname}/${filename}.old"
|
||||
cp -p "$repo_file" "$home_file"
|
||||
log_success "Backed up local file to ${filename}.old and updated"
|
||||
fi
|
||||
break
|
||||
;;
|
||||
n)
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would save repository version as ${filename}.new"
|
||||
else
|
||||
cp -p "$repo_file" "${dirname}/${filename}.new"
|
||||
log_success "Saved repository version as ${filename}.new"
|
||||
fi
|
||||
break
|
||||
;;
|
||||
s)
|
||||
log_info "Skipping $home_file"
|
||||
break
|
||||
;;
|
||||
i)
|
||||
local relative_path_to_home="${home_file#$HOME/}"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would add '$relative_path_to_home' to $HOME_UPDATE_IGNORE_FILE"
|
||||
else
|
||||
echo "$relative_path_to_home" >>"$HOME_UPDATE_IGNORE_FILE"
|
||||
log_success "Added '$relative_path_to_home' to $HOME_UPDATE_IGNORE_FILE and skipped."
|
||||
fi
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice. Please try again."
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
6)
|
||||
log_info "Skipping $home_file"
|
||||
break
|
||||
;;
|
||||
7)
|
||||
local relative_path_to_home="${home_file#$HOME/}"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would add '$relative_path_to_home' to $HOME_UPDATE_IGNORE_FILE"
|
||||
else
|
||||
echo "$relative_path_to_home" >>"$HOME_UPDATE_IGNORE_FILE"
|
||||
log_success "Added '$relative_path_to_home' to $HOME_UPDATE_IGNORE_FILE and skipped."
|
||||
fi
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice. Please enter 1-7."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Function to check if PKGBUILD has changed
|
||||
check_pkgbuild_changed() {
|
||||
local pkg_dir="$1"
|
||||
local pkgbuild_path="${pkg_dir}/PKGBUILD"
|
||||
|
||||
[[ ! -f "$pkgbuild_path" ]] && return 1
|
||||
|
||||
local relative_path="${pkgbuild_path#$REPO_ROOT/}"
|
||||
|
||||
if [[ "$FORCE_CHECK" == true ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if HEAD@{1} exists before trying to use it
|
||||
if ! git rev-parse --verify HEAD@{1} &>/dev/null; then
|
||||
# Fresh clone, assume all PKGBUILDs need checking
|
||||
return 0
|
||||
fi
|
||||
|
||||
if git diff --name-only HEAD@{1} HEAD 2>/dev/null | grep -q "^${relative_path}$"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to list available packages
|
||||
list_packages() {
|
||||
local available_packages=()
|
||||
local changed_packages=()
|
||||
|
||||
if [[ ! -d "$ARCH_PACKAGES_DIR" ]]; then
|
||||
log_warning "No package directory found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
|
||||
if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
|
||||
local pkg_name=$(basename "$pkg_dir")
|
||||
available_packages+=("$pkg_name")
|
||||
|
||||
if check_pkgbuild_changed "$pkg_dir"; then
|
||||
changed_packages+=("$pkg_name")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#available_packages[@]} -eq 0 ]]; then
|
||||
log_info "No packages found in package directory"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "\n${STY_CYAN}Available packages:${STY_RST}"
|
||||
for pkg in "${available_packages[@]}"; do
|
||||
if [[ " ${changed_packages[*]} " =~ " ${pkg} " ]]; then
|
||||
echo -e " ${STY_GREEN}● ${pkg}${STY_RST} (PKGBUILD changed)"
|
||||
else
|
||||
echo -e " ○ ${pkg}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#changed_packages[@]} -gt 0 ]]; then
|
||||
echo -e "\n${STY_YELLOW}Packages with changed PKGBUILDs: ${changed_packages[*]}${STY_RST}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to build selected packages
|
||||
build_packages() {
|
||||
local build_mode="$1"
|
||||
local packages_to_build=()
|
||||
local rebuilt_packages=0
|
||||
|
||||
case "$build_mode" in
|
||||
"changed")
|
||||
for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
|
||||
if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
|
||||
local pkg_name=$(basename "$pkg_dir")
|
||||
if check_pkgbuild_changed "$pkg_dir"; then
|
||||
packages_to_build+=("$pkg_name")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
;;
|
||||
"all")
|
||||
for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
|
||||
if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
|
||||
local pkg_name=$(basename "$pkg_dir")
|
||||
packages_to_build+=("$pkg_name")
|
||||
fi
|
||||
done
|
||||
;;
|
||||
"select")
|
||||
echo -e "\nEnter package names separated by spaces (or 'all' for all packages):"
|
||||
if ! safe_read "Packages to build: " user_selection ""; then
|
||||
log_warning "Failed to read input. Skipping package builds."
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$user_selection" == "all" ]]; then
|
||||
for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
|
||||
if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
|
||||
local pkg_name=$(basename "$pkg_dir")
|
||||
packages_to_build+=("$pkg_name")
|
||||
fi
|
||||
done
|
||||
else
|
||||
read -ra packages_to_build <<<"$user_selection"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ${#packages_to_build[@]} -eq 0 ]]; then
|
||||
log_info "No packages selected for building"
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "\n${STY_CYAN}Packages to build: ${packages_to_build[*]}${STY_RST}"
|
||||
|
||||
if ! safe_read "Proceed with building these packages? (Y/n): " confirm "Y"; then
|
||||
log_warning "Failed to read input. Skipping package builds."
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$confirm" =~ ^[Nn]$ ]]; then
|
||||
log_info "Package building cancelled by user"
|
||||
return
|
||||
fi
|
||||
|
||||
for pkg_name in "${packages_to_build[@]}"; do
|
||||
pkg_dir="${ARCH_PACKAGES_DIR}/${pkg_name}"
|
||||
|
||||
if [[ ! -d "$pkg_dir" || ! -f "${pkg_dir}/PKGBUILD" ]]; then
|
||||
log_error "Package not found or missing PKGBUILD: $pkg_name"
|
||||
continue
|
||||
fi
|
||||
|
||||
log_info "Building package: $pkg_name"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would build package in directory: $pkg_dir"
|
||||
continue
|
||||
fi
|
||||
|
||||
cd "$pkg_dir" || continue
|
||||
|
||||
if makepkg -si --noconfirm; then
|
||||
log_success "Successfully built and installed $pkg_name"
|
||||
((rebuilt_packages++))
|
||||
else
|
||||
log_error "Failed to build package $pkg_name"
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT" || log_die "Failed to return to repository directory"
|
||||
done
|
||||
|
||||
if [[ $rebuilt_packages -eq 0 ]]; then
|
||||
log_warning "No packages were successfully built"
|
||||
else
|
||||
log_success "Successfully rebuilt $rebuilt_packages package(s)"
|
||||
fi
|
||||
}
|
||||
|
||||
# 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
|
||||
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
|
||||
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
|
||||
|
||||
# Fallback: check all files (fresh clone or no git changes)
|
||||
find "$dir_path" -type f -print0 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check if we have new commits
|
||||
has_new_commits() {
|
||||
if git rev-parse --verify HEAD@{1} &>/dev/null; then
|
||||
[[ "$(git rev-parse HEAD)" != "$(git rev-parse HEAD@{1})" ]]
|
||||
else
|
||||
# Fresh clone or no reflog - assume we want to process files
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Main script starts here
|
||||
log_header "Dotfiles Update Script"
|
||||
|
||||
if [[ "$SKIP_NOTICE" == false ]]; then
|
||||
log_warning "THIS SCRIPT IS NOT FULLY TESTED AND MAY CAUSE ISSUES!"
|
||||
log_warning "It might be safer if you want to preserve your modifications and not delete added files,"
|
||||
log_warning " but this can cause partial updates and therefore unexpected behavior like in #1856."
|
||||
log_warning "In general, prefer install.sh for updates if available."
|
||||
safe_read "Continue? (y/N): " response "N"
|
||||
|
||||
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
||||
log_error "Update aborted by user"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
exit
|
||||
|
||||
# Check if we're in a git repository
|
||||
cd "$REPO_ROOT" || log_die "Failed to change to repository directory"
|
||||
|
||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
log_info "Running in git repository: $(git rev-parse --show-toplevel)"
|
||||
else
|
||||
log_error "Not in a git repository. Please run this script from your dotfiles repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Auto-detect repository structure
|
||||
log_header "Detecting Repository Structure"
|
||||
if detected_dirs=$(detect_repo_structure); then
|
||||
read -ra MONITOR_DIRS <<<"$detected_dirs"
|
||||
log_success "Detected repository structure:"
|
||||
for dir in "${MONITOR_DIRS[@]}"; do
|
||||
if [[ -d "${REPO_ROOT}/${dir}" ]]; then
|
||||
log_info " ✓ ${REPO_ROOT}/${dir}"
|
||||
else
|
||||
log_warning " ✗ ${REPO_ROOT}/${dir} (not found, will skip)"
|
||||
fi
|
||||
done
|
||||
else
|
||||
log_die "Failed to detect repository structure. Make sure you're in the correct directory."
|
||||
fi
|
||||
|
||||
# Step 1: Pull latest commits
|
||||
log_header "Pulling Latest Changes"
|
||||
|
||||
current_branch=$(git branch --show-current)
|
||||
if [[ -z "$current_branch" ]]; then
|
||||
log_warning "In detached HEAD state. Checking out main/master branch..."
|
||||
if git show-ref --verify --quiet refs/heads/main; then
|
||||
git checkout main
|
||||
current_branch="main"
|
||||
elif git show-ref --verify --quiet refs/heads/master; then
|
||||
git checkout master
|
||||
current_branch="master"
|
||||
else
|
||||
log_die "Could not find main or master branch"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info "Current branch: $current_branch"
|
||||
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
log_warning "You have uncommitted changes:"
|
||||
git status --short
|
||||
echo
|
||||
|
||||
if ! safe_read "Do you want to continue? This will stash your changes. (y/N): " response "N"; then
|
||||
echo
|
||||
log_error "Failed to read input. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
||||
log_die "Aborted by user"
|
||||
fi
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would stash changes"
|
||||
else
|
||||
git stash push -m "Auto-stash before update $(date)"
|
||||
log_info "Changes stashed"
|
||||
fi
|
||||
fi
|
||||
|
||||
if git remote get-url origin &>/dev/null; then
|
||||
log_info "Pulling changes from origin/$current_branch..."
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would run: git pull --ff-only"
|
||||
else
|
||||
if git pull --ff-only; then
|
||||
log_success "Successfully pulled latest changes"
|
||||
else
|
||||
log_warning "Failed to pull changes from remote. Continuing with local repository..."
|
||||
log_info "You may need to resolve conflicts manually later."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_warning "No remote 'origin' configured. Skipping pull operation."
|
||||
log_info "This appears to be a local-only repository."
|
||||
fi
|
||||
|
||||
# Step 2: Handle package building
|
||||
rebuilt_packages=0
|
||||
|
||||
if [[ "$CHECK_PACKAGES" == true ]]; then
|
||||
log_header "Package Management"
|
||||
|
||||
if [[ ! -d "$ARCH_PACKAGES_DIR" ]]; then
|
||||
log_warning "No packages directory found (tried: dist-arch, arch-packages, sdist/arch). Skipping package management."
|
||||
else
|
||||
changed_pkgbuilds=()
|
||||
for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
|
||||
if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
|
||||
pkg_name=$(basename "$pkg_dir")
|
||||
if check_pkgbuild_changed "$pkg_dir"; then
|
||||
changed_pkgbuilds+=("$pkg_name")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#changed_pkgbuilds[@]} -gt 0 ]]; then
|
||||
log_info "Found ${#changed_pkgbuilds[@]} package(s) with changed PKGBUILDs: ${changed_pkgbuilds[*]}"
|
||||
echo
|
||||
echo "Package build options:"
|
||||
echo "1) Build only packages with changed PKGBUILDs"
|
||||
echo "2) List all packages and select which to build"
|
||||
echo "3) Build all packages"
|
||||
echo "4) Skip package building"
|
||||
echo
|
||||
|
||||
if safe_read "Choose an option (1-4): " pkg_choice "1"; then
|
||||
case $pkg_choice in
|
||||
1) build_packages "changed" ;;
|
||||
2)
|
||||
if list_packages; then
|
||||
build_packages "select"
|
||||
fi
|
||||
;;
|
||||
3) build_packages "all" ;;
|
||||
4 | *) log_info "Skipping package building" ;;
|
||||
esac
|
||||
else
|
||||
log_warning "Failed to read input. Skipping package building."
|
||||
fi
|
||||
else
|
||||
log_info "No PKGBUILDs have changed since last update."
|
||||
echo
|
||||
if safe_read "Do you want to check and build packages anyway? (y/N): " check_anyway "N"; then
|
||||
if [[ "$check_anyway" =~ ^[Yy]$ ]]; then
|
||||
if list_packages; then
|
||||
echo
|
||||
echo "Package build options:"
|
||||
echo "1) Select specific packages to build"
|
||||
echo "2) Build all packages"
|
||||
echo "3) Skip package building"
|
||||
|
||||
if safe_read "Choose an option (1-3): " build_choice "3"; then
|
||||
case $build_choice in
|
||||
1) build_packages "select" ;;
|
||||
2) build_packages "all" ;;
|
||||
3 | *) log_info "Skipping package building" ;;
|
||||
esac
|
||||
else
|
||||
log_info "Skipping package building"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_info "Skipping package management"
|
||||
fi
|
||||
else
|
||||
log_info "Skipping package management"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_header "Package Management"
|
||||
log_info "Package checking disabled. Use -p or --packages flag to enable package management."
|
||||
|
||||
if [[ -d "$ARCH_PACKAGES_DIR" ]]; then
|
||||
changed_count=0
|
||||
for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
|
||||
if [[ -f "${pkg_dir}/PKGBUILD" ]] && check_pkgbuild_changed "$pkg_dir"; then
|
||||
((changed_count++))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $changed_count -gt 0 ]]; then
|
||||
log_warning "Note: $changed_count package(s) have changed PKGBUILDs. Use -p flag to manage packages."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 3: Update configuration files
|
||||
log_header "Updating Configuration Files"
|
||||
|
||||
process_files=false
|
||||
if [[ "$FORCE_CHECK" == true ]]; then
|
||||
process_files=true
|
||||
log_info "Force mode: checking all configuration files"
|
||||
elif has_new_commits; then
|
||||
process_files=true
|
||||
log_info "New commits detected: checking changed configuration files"
|
||||
else
|
||||
log_info "No new commits found: checking for local file differences"
|
||||
process_files=true
|
||||
fi
|
||||
|
||||
if [[ "$process_files" == true ]]; then
|
||||
files_processed=0
|
||||
files_updated=0
|
||||
files_created=0
|
||||
|
||||
for dir_name in "${MONITOR_DIRS[@]}"; do
|
||||
repo_dir_path="${REPO_ROOT}/${dir_name}"
|
||||
|
||||
if [[ ! -d "$repo_dir_path" ]]; then
|
||||
if [[ "$VERBOSE" == true ]]; then
|
||||
log_warning "Skipping non-existent directory: $repo_dir_path"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
# FIX: Properly handle dots/ prefix mapping
|
||||
if [[ "$dir_name" == dots/* ]]; then
|
||||
# Strip "dots/" prefix for home directory mapping
|
||||
home_subdir="${dir_name#dots/}"
|
||||
home_dir_path="${HOME}/${home_subdir}"
|
||||
else
|
||||
# Direct structure
|
||||
home_dir_path="${HOME}/${dir_name}"
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
while IFS= read -r -d '' repo_file; do
|
||||
# Calculate relative path from the repo source directory
|
||||
rel_path="${repo_file#$repo_dir_path/}"
|
||||
home_file="${home_dir_path}/${rel_path}"
|
||||
|
||||
if should_ignore "$home_file"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
((files_processed++))
|
||||
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
mkdir -p "$(dirname "$home_file")"
|
||||
fi
|
||||
|
||||
if [[ -f "$home_file" ]]; then
|
||||
if ! cmp -s "$repo_file" "$home_file"; then
|
||||
log_info "Found difference in: $rel_path"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_warning "[DRY-RUN] Conflict detected (would prompt): $home_file"
|
||||
((files_updated++))
|
||||
else
|
||||
handle_file_conflict "$repo_file" "$home_file"
|
||||
((files_updated++))
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would create new file: $home_file"
|
||||
else
|
||||
cp -p "$repo_file" "$home_file"
|
||||
log_success "Created new file: $home_file"
|
||||
fi
|
||||
((files_created++))
|
||||
fi
|
||||
done < <(get_changed_files "$repo_dir_path") || true
|
||||
done
|
||||
|
||||
echo
|
||||
log_info "File processing summary:"
|
||||
log_info "- Files processed: $files_processed"
|
||||
log_info "- Files with conflicts: $files_updated"
|
||||
log_info "- New files created: $files_created"
|
||||
else
|
||||
log_info "Skipping file updates (no changes detected and not in force mode)"
|
||||
fi
|
||||
|
||||
# Step 4: Update script permissions
|
||||
log_header "Updating Script Permissions"
|
||||
|
||||
if [[ -d "${HOME}/.local/bin" ]]; then
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would update script permissions in ~/.local/bin"
|
||||
else
|
||||
find "${HOME}/.local/bin" -type f -exec chmod +x {} \; 2>/dev/null || true
|
||||
log_success "Updated ~/.local/bin script permissions"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_header "Update Complete"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_warning "DRY-RUN MODE: No changes were actually made"
|
||||
log_info "Run without -n/--dry-run to apply changes"
|
||||
else
|
||||
log_success "Dotfiles update completed successfully!"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "${STY_CYAN}Summary:${STY_RST}"
|
||||
if command -v git >/dev/null && git rev-parse --git-dir >/dev/null 2>&1; then
|
||||
echo "- Repository: $(git log -1 --pretty=format:'%h - %s (%cr)' 2>/dev/null || echo 'Unknown')"
|
||||
else
|
||||
echo "- Repository: Unknown (git not available)"
|
||||
fi
|
||||
echo "- Branch: ${current_branch:-Unknown}"
|
||||
echo "- Structure: ${MONITOR_DIRS[*]}"
|
||||
echo "- Mode: $([ "$FORCE_CHECK" == true ] && echo "Force check" || echo "Normal")"
|
||||
echo "- Package checking: $([ "$CHECK_PACKAGES" == true ] && echo "Enabled" || echo "Disabled")"
|
||||
|
||||
if [[ $rebuilt_packages -gt 0 ]]; then
|
||||
echo "- Packages rebuilt: $rebuilt_packages"
|
||||
fi
|
||||
|
||||
if [[ "$process_files" == true ]]; then
|
||||
echo "- Files processed: $files_processed"
|
||||
echo "- Files updated/conflicted: $files_updated"
|
||||
echo "- New files created: $files_created"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$HOME_UPDATE_IGNORE_FILE" && ! -f "$UPDATE_IGNORE_FILE" ]]; then
|
||||
echo
|
||||
log_info "Tip: Create ignore files to exclude files from updates:"
|
||||
echo " - Repository ignore: ${REPO_ROOT}/.updateignore"
|
||||
echo " - User ignore: ~/.updateignore"
|
||||
echo
|
||||
echo "Example patterns:"
|
||||
echo " *.log # Ignore all .log files"
|
||||
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'"
|
||||
fi
|
||||
|
||||
echo
|
||||
Reference in New Issue
Block a user