From a6c4233a4d3ac3d79994b7e8e4da83389e3bdd92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:23:02 +0000 Subject: [PATCH 1/5] Initial plan From faebe7cb81c0cef82324ab10a6647b0df97dc639 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:28:33 +0000 Subject: [PATCH 2/5] Add automated dotfiles deployment scripts - Add deploy.sh script with symlink creation and backup functionality - Add uninstall.sh script to remove symlinks and restore backups - Update README.md with installation and usage instructions - Support for dry-run mode in both scripts - Automatic backup of existing files before creating symlinks Co-authored-by: kirillbobyrev <3352968+kirillbobyrev@users.noreply.github.com> --- README.md | 45 ++++++++++++ deploy.sh | 176 ++++++++++++++++++++++++++++++++++++++++++++++ uninstall.sh | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 416 insertions(+) create mode 100755 deploy.sh create mode 100755 uninstall.sh diff --git a/README.md b/README.md index bdad7c8..1eabd57 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,48 @@ whenever I have to. I mostly try to keep configuration files as minimalistic as possible in order to make things easier and not to pollute the environment. + +## Installation + +To deploy these dotfiles to your system, use the automated deployment script: + +```bash +./deploy.sh +``` + +This will create symlinks from the repository to your `$HOME` directory. Any existing files will be automatically backed up with a `.backup.TIMESTAMP` suffix. + +### Preview Changes (Dry Run) + +To see what would be deployed without making any changes: + +```bash +./deploy.sh --dry-run +``` + +### Uninstall + +To remove the symlinks created by the deployment script: + +```bash +./uninstall.sh +``` + +To remove symlinks and restore your backed-up files: + +```bash +./uninstall.sh --restore +``` + +## What Gets Deployed + +The deployment script creates symlinks for: + +- **Root-level dotfiles**: `.profile`, `.zshrc` +- **Vim configuration**: `.vim/` +- **Application configs**: `.config/alacritty/`, `.config/clangd/`, `.config/git/`, `.config/nvim/`, `.config/tmux/` + +## Requirements + +- Bash shell +- `readlink` command (usually pre-installed on most Linux distributions) diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..dfd665d --- /dev/null +++ b/deploy.sh @@ -0,0 +1,176 @@ +#!/bin/bash +# +# Dotfiles deployment script +# +# This script creates symlinks from the repository to $HOME, making it easy to +# keep your dotfiles synchronized across multiple machines. +# + +set -e + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Get the absolute path to the dotfiles directory +DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Function to print colored messages +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to create a backup of an existing file or directory +backup_file() { + local file="$1" + local backup="${file}.backup.$(date +%Y%m%d_%H%M%S)" + + if [ -e "$file" ] && [ ! -L "$file" ]; then + mv "$file" "$backup" + print_warning "Backed up existing $file to $backup" + return 0 + fi + return 1 +} + +# Function to create a symlink +create_symlink() { + local source="$1" + local target="$2" + + # If target is a symlink pointing to the correct source, skip + if [ -L "$target" ] && [ "$(readlink -f "$target")" = "$(readlink -f "$source")" ]; then + print_info "Already linked: $target -> $source" + return 0 + fi + + # If target exists but is not the correct symlink, back it up + if [ -e "$target" ] || [ -L "$target" ]; then + backup_file "$target" + fi + + # Create parent directory if it doesn't exist + local target_dir=$(dirname "$target") + if [ ! -d "$target_dir" ]; then + mkdir -p "$target_dir" + print_info "Created directory: $target_dir" + fi + + # Create the symlink + ln -s "$source" "$target" + print_success "Linked: $target -> $source" +} + +# Main deployment function +deploy_dotfiles() { + print_info "Starting dotfiles deployment from $DOTFILES_DIR" + echo "" + + # Deploy root-level dotfiles + print_info "Deploying root-level dotfiles..." + create_symlink "$DOTFILES_DIR/.profile" "$HOME/.profile" + create_symlink "$DOTFILES_DIR/.zshrc" "$HOME/.zshrc" + echo "" + + # Deploy .vim directory + print_info "Deploying .vim configuration..." + create_symlink "$DOTFILES_DIR/.vim" "$HOME/.vim" + echo "" + + # Deploy .config subdirectories + print_info "Deploying .config subdirectories..." + for config_dir in "$DOTFILES_DIR/.config"/*; do + if [ -d "$config_dir" ]; then + local dir_name=$(basename "$config_dir") + create_symlink "$config_dir" "$HOME/.config/$dir_name" + fi + done + echo "" + + print_success "Dotfiles deployment completed successfully!" + echo "" + print_info "Note: Any existing files have been backed up with a .backup.TIMESTAMP suffix." +} + +# Function to show usage +show_usage() { + cat << EOF +Usage: $(basename "$0") [OPTIONS] + +Automated dotfiles deployment script. + +OPTIONS: + -h, --help Show this help message + -d, --dry-run Show what would be done without making changes + +DESCRIPTION: + This script creates symlinks from the dotfiles repository to your home + directory. Any existing files will be backed up automatically. + +EXAMPLES: + # Deploy dotfiles + ./deploy.sh + + # See what would be deployed without making changes + ./deploy.sh --dry-run + +EOF +} + +# Parse command line arguments +DRY_RUN=false +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_usage + exit 0 + ;; + -d|--dry-run) + DRY_RUN=true + shift + ;; + *) + print_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Dry run mode +if [ "$DRY_RUN" = true ]; then + print_info "DRY RUN MODE - No changes will be made" + echo "" + print_info "The following symlinks would be created:" + echo "" + echo " $HOME/.profile -> $DOTFILES_DIR/.profile" + echo " $HOME/.zshrc -> $DOTFILES_DIR/.zshrc" + echo " $HOME/.vim -> $DOTFILES_DIR/.vim" + for config_dir in "$DOTFILES_DIR/.config"/*; do + if [ -d "$config_dir" ]; then + dir_name=$(basename "$config_dir") + echo " $HOME/.config/$dir_name -> $config_dir" + fi + done + echo "" + print_info "Run without --dry-run to perform the deployment" + exit 0 +fi + +# Run the deployment +deploy_dotfiles diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..6ab7e5f --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,195 @@ +#!/bin/bash +# +# Dotfiles uninstall script +# +# This script removes symlinks created by deploy.sh and optionally restores +# backups if they exist. +# + +set -e + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Get the absolute path to the dotfiles directory +DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Function to print colored messages +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to remove a symlink if it points to our dotfiles +remove_symlink() { + local target="$1" + local expected_source="$2" + + if [ -L "$target" ]; then + local actual_source=$(readlink -f "$target") + local expected_source_abs=$(readlink -f "$expected_source") + + if [ "$actual_source" = "$expected_source_abs" ]; then + rm "$target" + print_success "Removed symlink: $target" + return 0 + else + print_warning "Skipping $target (points to $actual_source, not our dotfiles)" + return 1 + fi + elif [ -e "$target" ]; then + print_warning "Skipping $target (not a symlink)" + return 1 + else + print_info "Already removed: $target" + return 0 + fi +} + +# Function to find and optionally restore the most recent backup +restore_backup() { + local target="$1" + local backup_pattern="${target}.backup.*" + + # Find the most recent backup + local latest_backup=$(ls -t ${backup_pattern} 2>/dev/null | head -n 1) + + if [ -n "$latest_backup" ] && [ -e "$latest_backup" ]; then + print_info "Found backup: $latest_backup" + + if [ "$RESTORE_BACKUPS" = true ]; then + mv "$latest_backup" "$target" + print_success "Restored backup: $latest_backup -> $target" + else + print_info "Use --restore to restore this backup" + fi + fi +} + +# Main uninstall function +uninstall_dotfiles() { + print_info "Starting dotfiles uninstall" + echo "" + + # Remove root-level dotfiles + print_info "Removing root-level dotfiles..." + remove_symlink "$HOME/.profile" "$DOTFILES_DIR/.profile" && restore_backup "$HOME/.profile" + remove_symlink "$HOME/.zshrc" "$DOTFILES_DIR/.zshrc" && restore_backup "$HOME/.zshrc" + echo "" + + # Remove .vim directory + print_info "Removing .vim configuration..." + remove_symlink "$HOME/.vim" "$DOTFILES_DIR/.vim" && restore_backup "$HOME/.vim" + echo "" + + # Remove .config subdirectories + print_info "Removing .config subdirectories..." + for config_dir in "$DOTFILES_DIR/.config"/*; do + if [ -d "$config_dir" ]; then + local dir_name=$(basename "$config_dir") + local target="$HOME/.config/$dir_name" + remove_symlink "$target" "$config_dir" && restore_backup "$target" + fi + done + echo "" + + print_success "Dotfiles uninstall completed!" + + if [ "$RESTORE_BACKUPS" != true ]; then + echo "" + print_info "Backup files were not restored. Use --restore to restore them." + fi +} + +# Function to show usage +show_usage() { + cat << EOF +Usage: $(basename "$0") [OPTIONS] + +Remove dotfiles symlinks created by deploy.sh. + +OPTIONS: + -h, --help Show this help message + -r, --restore Restore backup files after removing symlinks + -d, --dry-run Show what would be done without making changes + +DESCRIPTION: + This script removes symlinks created by deploy.sh. Only symlinks that point + to this dotfiles repository will be removed. Use --restore to automatically + restore the most recent backup files. + +EXAMPLES: + # Remove dotfiles symlinks + ./uninstall.sh + + # Remove symlinks and restore backups + ./uninstall.sh --restore + + # See what would be removed without making changes + ./uninstall.sh --dry-run + +EOF +} + +# Parse command line arguments +DRY_RUN=false +RESTORE_BACKUPS=false +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_usage + exit 0 + ;; + -r|--restore) + RESTORE_BACKUPS=true + shift + ;; + -d|--dry-run) + DRY_RUN=true + shift + ;; + *) + print_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Dry run mode +if [ "$DRY_RUN" = true ]; then + print_info "DRY RUN MODE - No changes will be made" + echo "" + print_info "The following symlinks would be removed (if they exist and point to our dotfiles):" + echo "" + echo " $HOME/.profile" + echo " $HOME/.zshrc" + echo " $HOME/.vim" + for config_dir in "$DOTFILES_DIR/.config"/*; do + if [ -d "$config_dir" ]; then + dir_name=$(basename "$config_dir") + echo " $HOME/.config/$dir_name" + fi + done + echo "" + print_info "Run without --dry-run to perform the uninstall" + exit 0 +fi + +# Run the uninstall +uninstall_dotfiles From 89c19a3af50a603f7beb44ee8df5e507d2da83a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:30:06 +0000 Subject: [PATCH 3/5] Improve error handling in deployment scripts - Add error handling for symlink creation in deploy.sh - Add error handling for symlink removal in uninstall.sh - Fix quoting issue in backup pattern variable - Suppress stderr for cleaner output on expected failures Co-authored-by: kirillbobyrev <3352968+kirillbobyrev@users.noreply.github.com> --- deploy.sh | 5 ++++- uninstall.sh | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/deploy.sh b/deploy.sh index dfd665d..a53715f 100755 --- a/deploy.sh +++ b/deploy.sh @@ -72,7 +72,10 @@ create_symlink() { fi # Create the symlink - ln -s "$source" "$target" + if ! ln -s "$source" "$target" 2>/dev/null; then + print_error "Failed to create symlink: $target -> $source" + return 1 + fi print_success "Linked: $target -> $source" } diff --git a/uninstall.sh b/uninstall.sh index 6ab7e5f..cdf6eb9 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -45,7 +45,10 @@ remove_symlink() { local expected_source_abs=$(readlink -f "$expected_source") if [ "$actual_source" = "$expected_source_abs" ]; then - rm "$target" + if ! rm "$target" 2>/dev/null; then + print_error "Failed to remove symlink: $target" + return 1 + fi print_success "Removed symlink: $target" return 0 else @@ -67,7 +70,7 @@ restore_backup() { local backup_pattern="${target}.backup.*" # Find the most recent backup - local latest_backup=$(ls -t ${backup_pattern} 2>/dev/null | head -n 1) + local latest_backup=$(ls -t "${backup_pattern}" 2>/dev/null | head -n 1) if [ -n "$latest_backup" ] && [ -e "$latest_backup" ]; then print_info "Found backup: $latest_backup" From 207ddb46c26c67c426bc106511e5380ed04f5743 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:33:38 +0000 Subject: [PATCH 4/5] Fix shellcheck warnings for better shell script quality - Separate variable declarations from assignments (SC2155) - Replace ls with find for better handling of filenames (SC2012) - Remove unused backup_pattern variable - All shellcheck warnings resolved Co-authored-by: kirillbobyrev <3352968+kirillbobyrev@users.noreply.github.com> --- deploy.sh | 9 ++++++--- uninstall.sh | 16 ++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/deploy.sh b/deploy.sh index a53715f..de6ac6c 100755 --- a/deploy.sh +++ b/deploy.sh @@ -38,7 +38,8 @@ print_error() { # Function to create a backup of an existing file or directory backup_file() { local file="$1" - local backup="${file}.backup.$(date +%Y%m%d_%H%M%S)" + local backup + backup="${file}.backup.$(date +%Y%m%d_%H%M%S)" if [ -e "$file" ] && [ ! -L "$file" ]; then mv "$file" "$backup" @@ -65,7 +66,8 @@ create_symlink() { fi # Create parent directory if it doesn't exist - local target_dir=$(dirname "$target") + local target_dir + target_dir=$(dirname "$target") if [ ! -d "$target_dir" ]; then mkdir -p "$target_dir" print_info "Created directory: $target_dir" @@ -99,7 +101,8 @@ deploy_dotfiles() { print_info "Deploying .config subdirectories..." for config_dir in "$DOTFILES_DIR/.config"/*; do if [ -d "$config_dir" ]; then - local dir_name=$(basename "$config_dir") + local dir_name + dir_name=$(basename "$config_dir") create_symlink "$config_dir" "$HOME/.config/$dir_name" fi done diff --git a/uninstall.sh b/uninstall.sh index cdf6eb9..9d11f1c 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -41,8 +41,10 @@ remove_symlink() { local expected_source="$2" if [ -L "$target" ]; then - local actual_source=$(readlink -f "$target") - local expected_source_abs=$(readlink -f "$expected_source") + local actual_source + local expected_source_abs + actual_source=$(readlink -f "$target") + expected_source_abs=$(readlink -f "$expected_source") if [ "$actual_source" = "$expected_source_abs" ]; then if ! rm "$target" 2>/dev/null; then @@ -67,10 +69,10 @@ remove_symlink() { # Function to find and optionally restore the most recent backup restore_backup() { local target="$1" - local backup_pattern="${target}.backup.*" # Find the most recent backup - local latest_backup=$(ls -t "${backup_pattern}" 2>/dev/null | head -n 1) + local latest_backup + latest_backup=$(find "$(dirname "$target")" -maxdepth 1 -name "$(basename "$target").backup.*" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -n 1 | cut -d' ' -f2-) if [ -n "$latest_backup" ] && [ -e "$latest_backup" ]; then print_info "Found backup: $latest_backup" @@ -104,8 +106,10 @@ uninstall_dotfiles() { print_info "Removing .config subdirectories..." for config_dir in "$DOTFILES_DIR/.config"/*; do if [ -d "$config_dir" ]; then - local dir_name=$(basename "$config_dir") - local target="$HOME/.config/$dir_name" + local dir_name + local target + dir_name=$(basename "$config_dir") + target="$HOME/.config/$dir_name" remove_symlink "$target" "$config_dir" && restore_backup "$target" fi done From b49087600017633881a0869eee1f7437a0fc5195 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:35:59 +0000 Subject: [PATCH 5/5] Make uninstall script portable across Linux and macOS - Use stat command detection to choose GNU or BSD variant - Support both GNU stat (Linux) and BSD stat (macOS) - Ensures backup restoration works on all platforms - Maintains shellcheck compliance Co-authored-by: kirillbobyrev <3352968+kirillbobyrev@users.noreply.github.com> --- uninstall.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/uninstall.sh b/uninstall.sh index 9d11f1c..5952b1b 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -70,9 +70,16 @@ remove_symlink() { restore_backup() { local target="$1" - # Find the most recent backup + # Find the most recent backup (portable across Linux and macOS/BSD) local latest_backup - latest_backup=$(find "$(dirname "$target")" -maxdepth 1 -name "$(basename "$target").backup.*" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -n 1 | cut -d' ' -f2-) + # Try GNU stat first, then BSD stat + if stat -c '%Y' "$(dirname "$target")" >/dev/null 2>&1; then + # GNU stat (Linux) + latest_backup=$(find "$(dirname "$target")" -maxdepth 1 -name "$(basename "$target").backup.*" -type f -exec stat -c '%Y %n' {} \; 2>/dev/null | sort -rn | head -n 1 | cut -d' ' -f2-) + else + # BSD stat (macOS) + latest_backup=$(find "$(dirname "$target")" -maxdepth 1 -name "$(basename "$target").backup.*" -type f -exec stat -f '%m %N' {} \; 2>/dev/null | sort -rn | head -n 1 | cut -d' ' -f2-) + fi if [ -n "$latest_backup" ] && [ -e "$latest_backup" ]; then print_info "Found backup: $latest_backup"