diff options
author | Yehonal <yehonal.azeroth@gmail.com> | 2025-09-06 11:22:22 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-09-06 11:22:22 +0200 |
commit | d3a6c09b3164d5ad0f05924bdad30f98416e6503 (patch) | |
tree | a931e0ffca2782de26c5f791e04dfca566686dc9 /apps/installer | |
parent | 725b475dd4522b34b2182c9721671f1a49ca1c80 (diff) |
feat(config): add support for excluding modules during installation and updates (#22793)
Diffstat (limited to 'apps/installer')
-rw-r--r-- | apps/installer/includes/modules-manager/README.md | 145 | ||||
-rw-r--r-- | apps/installer/includes/modules-manager/modules.sh | 308 | ||||
-rwxr-xr-x | apps/installer/test/test_module_commands.bats | 398 |
3 files changed, 775 insertions, 76 deletions
diff --git a/apps/installer/includes/modules-manager/README.md b/apps/installer/includes/modules-manager/README.md index f478035beb..93496a91a6 100644 --- a/apps/installer/includes/modules-manager/README.md +++ b/apps/installer/includes/modules-manager/README.md @@ -9,6 +9,10 @@ This directory contains the module management system for AzerothCore, providing - **Custom Directory Naming**: Prevent conflicts with custom directory names - **Duplicate Prevention**: Smart detection and prevention of duplicate installations - **Multi-Host Support**: GitHub, GitLab, and other Git hosts +- **Module Exclusion**: Support for excluding modules via environment variable +- **Interactive Menu System**: Easy-to-use menu interface for module management +- **Colored Output**: Enhanced terminal output with color support (respects NO_COLOR) +- **Flat Directory Structure**: Uses flat module installation (no owner subfolders) ## 📁 File Structure @@ -100,10 +104,33 @@ repo[:dirname][@branch[:commit]] # Search with multiple terms ./acore.sh module search auction house -# Show all available modules +# Search with input prompt ./acore.sh module search ``` +### Listing Installed Modules + +```bash +# List all installed modules +./acore.sh module list +``` + +### Interactive Menu + +```bash +# Start interactive menu system +./acore.sh module + +# Menu options: +# s - Search for available modules +# i - Install one or more modules +# u - Update installed modules +# r - Remove installed modules +# l - List installed modules +# h - Show detailed help +# q - Close this menu +``` + ## 🔍 Cross-Format Recognition The system intelligently recognizes the same module across different specification formats: @@ -129,13 +156,55 @@ The system prevents common conflicts: ```bash # If 'mod-transmog' directory already exists: $ ./acore.sh module install mod-transmog:mod-transmog -Error: Directory 'mod-transmog' already exists. Possible solutions: 1. Use a different directory name: mod-transmog:my-custom-name 2. Remove the existing directory first 3. Use the update command if this is the same module ``` +### Duplicate Module Prevention +The system uses intelligent owner/name matching to prevent installing the same module multiple times, even when specified in different formats. + +## 🚫 Module Exclusion + +You can exclude modules from installation using the `MODULES_EXCLUDE_LIST` environment variable: + +```bash +# Exclude specific modules (space-separated) +export MODULES_EXCLUDE_LIST="mod-test-module azerothcore/mod-dev-only" +./acore.sh module install --all # Will skip excluded modules + +# Supports cross-format matching +export MODULES_EXCLUDE_LIST="https://github.com/azerothcore/mod-transmog.git" +./acore.sh module install mod-transmog # Will be skipped as excluded +``` + +The exclusion system: +- Uses the same cross-format recognition as other module operations +- Works with all installation methods (`install`, `install --all`) +- Provides clear feedback when modules are skipped +- Supports URLs, owner/name format, and simple names + +## 🎨 Color Support + +The module manager provides enhanced terminal output with colors: + +- **Info**: Cyan text for informational messages +- **Success**: Green text for successful operations +- **Warning**: Yellow text for warnings +- **Error**: Red text for errors +- **Headers**: Bold cyan text for section headers + +Color support is automatically disabled when: +- Output is not to a terminal (piped/redirected) +- `NO_COLOR` environment variable is set +- Terminal doesn't support colors + +You can force color output with: +```bash +export FORCE_COLOR=1 +``` + ## 🔄 Integration ### Including in Scripts @@ -165,24 +234,78 @@ azerothcore/mod-transmog master abc123def456 https://github.com/custom/mod-custom.git develop def456abc789 mod-eluna:custom-eluna-dir main 789abc123def ``` + +The list maintains: +- **Alphabetical ordering** by normalized owner/name for consistency +- **Original format preservation** of how modules were specified +- **Automatic deduplication** across different specification formats +- **Custom directory tracking** when specified + ## 🔧 Configuration ### Environment Variables -- `MODULES_LIST_FILE`: Override default modules list path -- `J_PATH_MODULES`: Modules installation directory -- `AC_PATH_ROOT`: AzerothCore root path + +| Variable | Description | Default | +|----------|-------------|---------| +| `MODULES_LIST_FILE` | Override default modules list path | `$AC_PATH_ROOT/conf/modules.list` | +| `MODULES_EXCLUDE_LIST` | Space-separated list of modules to exclude | - | +| `J_PATH_MODULES` | Modules installation directory | `$AC_PATH_ROOT/modules` | +| `AC_PATH_ROOT` | AzerothCore root path | - | +| `NO_COLOR` | Disable colored output | - | +| `FORCE_COLOR` | Force colored output even when not TTY | - | ### Default Paths -- Modules list: `$AC_PATH_ROOT/conf/modules.list` +- **Modules list**: `$AC_PATH_ROOT/conf/modules.list` +- **Installation directory**: `$J_PATH_MODULES` (flat structure, no owner subfolders) + +## 🏗️ Architecture + +### Core Functions + +| Function | Purpose | +|----------|---------| +| `inst_module()` | Main dispatcher and interactive menu | +| `inst_parse_module_spec()` | Parse advanced module syntax | +| `inst_extract_owner_name()` | Normalize modules for cross-format recognition | +| `inst_mod_list_*()` | Module list management (read/write/update) | +| `inst_module_*()` | Module operations (install/update/remove/search) | + +### Key Features + +- **Flat Directory Structure**: All modules install directly under `modules/` without owner subdirectories +- **Smart Conflict Detection**: Prevents directory name conflicts with helpful suggestions +- **Cross-Platform Compatibility**: Works on Linux, macOS, and Windows (Git Bash) +- **Version Compatibility**: Checks `acore-module.json` for AzerothCore version compatibility +- **Git Integration**: Uses Joiner system for Git repository management + +### Debug Mode + +For debugging module operations, you can examine the generated commands: +```bash +# Check what Joiner commands would be executed +tail -f /tmp/joiner_called.txt # In test environments +``` ## 🤝 Contributing When modifying the module manager: -1. Maintain backwards compatibility -2. Update tests in `test_module_commands.bats` -3. Update this documentation -4. Test cross-format recognition thoroughly -5. Ensure helpful error messages +1. **Maintain backwards compatibility** with existing module list format +2. **Update tests** in `test_module_commands.bats` for new functionality +3. **Update this documentation** for any new features or changes +4. **Test cross-format recognition** thoroughly across all supported formats +5. **Ensure helpful error messages** for common user mistakes +6. **Test exclusion functionality** with various module specification formats +7. **Verify color output** works correctly in different terminal environments + +### Testing Guidelines +```bash +# Run all module-related tests +cd apps/installer +bats test/test_module_commands.bats +# Test with different environments +NO_COLOR=1 ./acore.sh module list +FORCE_COLOR=1 ./acore.sh module help +``` diff --git a/apps/installer/includes/modules-manager/modules.sh b/apps/installer/includes/modules-manager/modules.sh index 88cadf01e2..787d07677c 100644 --- a/apps/installer/includes/modules-manager/modules.sh +++ b/apps/installer/includes/modules-manager/modules.sh @@ -28,6 +28,49 @@ source "$CURRENT_PATH/../../../bash_shared/includes.sh" source "$CURRENT_PATH/../includes.sh" source "$AC_PATH_APPS/bash_shared/menu_system.sh" +# ----------------------------------------------------------------------------- +# Color support (disabled when not a TTY or NO_COLOR is set) +# ----------------------------------------------------------------------------- +if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then + if command -v tput >/dev/null 2>&1; then + _ac_cols=$(tput colors 2>/dev/null || echo 0) + else + _ac_cols=0 + fi +else + _ac_cols=0 +fi + +if [ "${FORCE_COLOR:-}" != "" ] || [ "${_ac_cols}" -ge 8 ]; then + C_RESET='\033[0m' + C_BOLD='\033[1m' + C_DIM='\033[2m' + C_RED='\033[31m' + C_GREEN='\033[32m' + C_YELLOW='\033[33m' + C_BLUE='\033[34m' + C_MAGENTA='\033[35m' + C_CYAN='\033[36m' +else + C_RESET='' + C_BOLD='' + C_DIM='' + C_RED='' + C_GREEN='' + C_YELLOW='' + C_BLUE='' + C_MAGENTA='' + C_CYAN='' +fi + +# Simple helpers for consistent colored output +function print_info() { printf "%b\n" "${C_CYAN}$*${C_RESET}"; } +function print_warn() { printf "%b\n" "${C_YELLOW}$*${C_RESET}"; } +function print_error() { printf "%b\n" "${C_RED}$*${C_RESET}"; } +function print_success() { printf "%b\n" "${C_GREEN}$*${C_RESET}"; } +function print_skip() { printf "%b\n" "${C_BLUE}$*${C_RESET}"; } +function print_header() { printf "%b\n" "${C_BOLD}${C_CYAN}$*${C_RESET}"; } + # Module management menu definition # Format: "key|short|description" module_menu_items=( @@ -65,11 +108,11 @@ function handle_module_command() { inst_module_help ;; "quit") - echo "Exiting module manager..." + print_info "Exiting module manager..." return 0 ;; *) - echo "Invalid option. Use 'help' to see available commands." + print_error "Invalid option. Use 'help' to see available commands." return 1 ;; esac @@ -77,7 +120,7 @@ function handle_module_command() { # Show detailed module help function inst_module_help() { - echo "AzerothCore Module Manager Help" + print_header "AzerothCore Module Manager Help" echo "===============================" echo "" echo "Usage:" @@ -106,20 +149,20 @@ function inst_module_help() { # List installed modules function inst_module_list() { - echo "Installed Modules:" + print_header "Installed Modules" echo "==================" local count=0 while read -r repo_ref branch commit; do [[ -z "$repo_ref" ]] && continue count=$((count + 1)) - echo " $count. $repo_ref ($branch)" + printf " %s. %b (%s)%b\n" "$count" "${C_GREEN}${repo_ref}" "${branch}" "${C_RESET}" if [[ "$commit" != "-" ]]; then - echo " Commit: $commit" + printf " %bCommit:%b %s\n" "${C_DIM}" "${C_RESET}" "$commit" fi done < <(inst_mod_list_read) if [[ $count -eq 0 ]]; then - echo " No modules installed." + print_warn " No modules installed." fi echo "" } @@ -160,8 +203,7 @@ function inst_module() { inst_module_list "${args[@]}" ;; *) - echo "Unknown module command: $cmd" - echo "Use 'help' to see available commands." + print_error "Unknown module command: $cmd. Use 'help' to see available commands." return 1 ;; esac @@ -188,20 +230,72 @@ function inst_parse_module_spec() { # Parse the new syntax: repo[:dirname][@branch[:commit]] - # First, extract custom directory name if present (format: repo:dirname@branch) + # First, check if this is a URL (contains :// or starts with git@) + local is_url=0 + if [[ "$spec" =~ :// ]] || [[ "$spec" =~ ^git@ ]]; then + is_url=1 + fi + + # Parse directory and branch differently for URLs vs simple names local repo_with_branch="$spec" - if [[ "$spec" =~ ^([^@:]+):([^@:]+)(@.*)?$ ]]; then - repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}" - dirname="${BASH_REMATCH[2]}" + if [[ $is_url -eq 1 ]]; then + # For URLs, look for :dirname pattern, but be careful about ports + # Strategy: only match :dirname if it's clearly after the repository path + + # Look for :dirname patterns at the end, but not if it looks like a port + if [[ "$spec" =~ ^(.*\.git):([^@/:]+)(@.*)?$ ]]; then + # Repo ending with .git:dirname + repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}" + dirname="${BASH_REMATCH[2]}" + elif [[ "$spec" =~ ^(.*://[^/]+/[^:]*[^0-9]):([^@/:]+)(@.*)?$ ]]; then + # URL with path ending in non-digit:dirname (avoid matching ports) + repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}" + dirname="${BASH_REMATCH[2]}" + fi + # If no custom dirname found, repo_with_branch remains the original spec + else + # For simple names, use the original logic + if [[ "$spec" =~ ^([^@:]+):([^@:]+)(@.*)?$ ]]; then + repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}" + dirname="${BASH_REMATCH[2]}" + fi fi # Now parse branch and commit from the repo part - if [[ "$repo_with_branch" =~ ^([^@]+)@([^:]+)(:(.+))?$ ]]; then - repo_part="${BASH_REMATCH[1]}" - branch="${BASH_REMATCH[2]}" - commit="${BASH_REMATCH[4]:-}" + # Be careful not to confuse URL @ with branch @ + if [[ "$repo_with_branch" =~ :// ]]; then + # For URLs, look for @ after the authority part + if [[ "$repo_with_branch" =~ ^[^/]*//[^/]+/.*@([^:]+)(:(.+))?$ ]]; then + # @ found in path part - treat as branch + repo_part="${repo_with_branch%@*}" + branch="${BASH_REMATCH[1]}" + commit="${BASH_REMATCH[3]:-}" + elif [[ "$repo_with_branch" =~ ^([^@]*@[^/]+/.*)@([^:]+)(:(.+))?$ ]]; then + # @ found after URL authority @ - treat as branch + repo_part="${BASH_REMATCH[1]}" + branch="${BASH_REMATCH[2]}" + commit="${BASH_REMATCH[4]:-}" + else + repo_part="$repo_with_branch" + fi + elif [[ "$repo_with_branch" =~ ^git@ ]]; then + # Git SSH format - look for @ after the initial git@host: part + if [[ "$repo_with_branch" =~ ^git@[^:]+:.*@([^:]+)(:(.+))?$ ]]; then + repo_part="${repo_with_branch%@*}" + branch="${BASH_REMATCH[1]}" + commit="${BASH_REMATCH[3]:-}" + else + repo_part="$repo_with_branch" + fi else - repo_part="$repo_with_branch" + # Non-URL format - use original logic + if [[ "$repo_with_branch" =~ ^([^@]+)@([^:]+)(:(.+))?$ ]]; then + repo_part="${BASH_REMATCH[1]}" + branch="${BASH_REMATCH[2]}" + commit="${BASH_REMATCH[4]:-}" + else + repo_part="$repo_with_branch" + fi fi # Normalize repo reference and extract owner/name. @@ -210,10 +304,39 @@ function inst_parse_module_spec() { # If repo_ref is a URL, extract owner/name from path when possible if [[ "$repo_ref" =~ :// ]] || [[ "$repo_ref" =~ ^git@ ]]; then - # Extract owner/name (last two path components) - owner_repo=$(echo "$repo_ref" | sed -E 's#(git@[^:]+:|https?://[^/]+/|ssh://[^/]+/)?(.*?)(\.git)?$#\2#') - owner="$(echo "$owner_repo" | awk -F'/' '{print $(NF-1)}')" - name="$(echo "$owner_repo" | awk -F'/' '{print $NF}' | sed -E 's/\.git$//')" + # Handle various URL formats + local path_part="" + if [[ "$repo_ref" =~ ^https?://[^/]+:?[0-9]*/(.+)$ ]]; then + # HTTPS URL (with or without port) + path_part="${BASH_REMATCH[1]}" + elif [[ "$repo_ref" =~ ^ssh://.*@[^/]+:?[0-9]*/(.+)$ ]]; then + # SSH URL with user@host:port/path format + path_part="${BASH_REMATCH[1]}" + elif [[ "$repo_ref" =~ ^ssh://[^@/]+:?[0-9]*/(.+)$ ]]; then + # SSH URL with host:port/path format (no user@) + path_part="${BASH_REMATCH[1]}" + elif [[ "$repo_ref" =~ ^git@[^:]+:(.+)$ ]]; then + # Git SSH format (git@host:path) + path_part="${BASH_REMATCH[1]}" + fi + + # Extract owner/name from path + if [[ -n "$path_part" ]]; then + # Remove .git suffix and any :dirname suffix + path_part="${path_part%.git}" + path_part="${path_part%:*}" + + if [[ "$path_part" == *"/"* ]]; then + owner="$(echo "$path_part" | awk -F'/' '{print $(NF-1)}')" + name="$(echo "$path_part" | awk -F'/' '{print $NF}')" + else + owner="unknown" + name="$path_part" + fi + else + owner="unknown" + name="unknown" + fi else owner_repo="$repo_ref" if [[ "$owner_repo" == *"/"* ]]; then @@ -266,21 +389,28 @@ function inst_extract_owner_name { base_ref="${repo_ref%%:*}" fi - if [[ "$base_ref" =~ ^https?://github\.com/([^/]+)/([^/]+)(\.git)?(/.*)?$ ]]; then - # HTTPS URL format - check this first before owner/name pattern + # Handle various URL formats with possible ports + if [[ "$base_ref" =~ ^https?://[^/]+:?[0-9]*/([^/]+)/([^/?]+) ]]; then + # HTTPS URL format (with or without port) - matches github.com, gitlab.com, custom hosts + local owner="${BASH_REMATCH[1]}" local name="${BASH_REMATCH[2]}" - name="${name%.git}" # Remove .git suffix if present - echo "${BASH_REMATCH[1]}/$name" - elif [[ "$base_ref" =~ ^https?://gitlab\.com/([^/]+)/([^/]+)(\.git)?(/.*)?$ ]]; then - # GitLab URL format + name="${name%:*}" # Remove any :dirname suffix first + name="${name%.git}" # Then remove .git suffix if present + echo "$owner/$name" + elif [[ "$base_ref" =~ ^ssh://[^/]+:?[0-9]*/([^/]+)/([^/?]+) ]]; then + # SSH URL format (with or without port) + local owner="${BASH_REMATCH[1]}" local name="${BASH_REMATCH[2]}" - name="${name%.git}" # Remove .git suffix if present - echo "${BASH_REMATCH[1]}/$name" - elif [[ "$base_ref" =~ ^git@github\.com:([^/]+)/([^/]+)(\.git)?$ ]]; then - # SSH URL format + name="${name%:*}" # Remove any :dirname suffix first + name="${name%.git}" # Then remove .git suffix if present + echo "$owner/$name" + elif [[ "$base_ref" =~ ^git@[^:]+:([^/]+)/([^/?]+) ]]; then + # Git SSH format (git@host:owner/repo) + local owner="${BASH_REMATCH[1]}" local name="${BASH_REMATCH[2]}" - name="${name%.git}" # Remove .git suffix if present - echo "${BASH_REMATCH[1]}/$name" + name="${name%:*}" # Remove any :dirname suffix first + name="${name%.git}" # Then remove .git suffix if present + echo "$owner/$name" elif [[ "$base_ref" =~ ^[^/]+/[^/]+$ ]]; then # Format: owner/name (check after URL patterns) echo "$base_ref" @@ -330,6 +460,40 @@ function inst_mod_list_read() { done < "$file" } +# Check whether a module spec matches the exclusion list. +# - Reads space/newline separated items from env var MODULES_EXCLUDE_LIST +# - Supports cross-format matching via inst_extract_owner_name +# Returns 0 if excluded, 1 otherwise. +function inst_mod_is_excluded() { + local spec="$1" + local target_owner_name + target_owner_name=$(inst_extract_owner_name "$spec") + + # No exclusions configured + if [[ -z "${MODULES_EXCLUDE_LIST:-}" ]]; then + return 1 + fi + + # Split on default IFS (space, tab, newline) + local items=() + # Use mapfile to split MODULES_EXCLUDE_LIST on newlines; fallback to space if no newlines + if [[ "${MODULES_EXCLUDE_LIST}" == *$'\n'* ]]; then + mapfile -t items <<< "${MODULES_EXCLUDE_LIST}" + else + read -r -a items <<< "${MODULES_EXCLUDE_LIST}" + fi + + local it it_owner + for it in "${items[@]}"; do + [[ -z "$it" ]] && continue + it_owner=$(inst_extract_owner_name "$it") + if [[ "$it_owner" == "$target_owner_name" ]]; then + return 0 + fi + done + return 1 +} + # Add or update an entry in the list: repo_ref branch commit # Removes any existing entries with the same owner/name to avoid duplicates function inst_mod_list_upsert() { @@ -448,12 +612,12 @@ function inst_check_module_conflict { local repo_ref="$2" if [ -d "$J_PATH_MODULES/$dirname" ]; then - echo "Error: Directory '$dirname' already exists." - echo "Possible solutions:" - echo " 1. Use a different directory name: $repo_ref:my-custom-name" - echo " 2. Remove the existing directory first" - echo " 3. Use the update command if this is the same module" - return 1 + print_error "Error: Directory '$dirname' already exists." + print_warn "Possible solutions:" + echo " 1. Use a different directory name: $repo_ref:my-custom-name" + echo " 2. Remove the existing directory first" + echo " 3. Use the update command if this is the same module" + return 1 fi return 0 } @@ -511,7 +675,7 @@ function inst_module_search { local CATALOG_URL="https://www.azerothcore.org/data/catalogue.json" - echo "Searching ${terms[*]}..." + print_header "Searching ${terms[*]}..." echo "" # Build candidate list from catalogue (full_name = owner/repo) @@ -567,9 +731,9 @@ function inst_module_search { read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/${mod_full}/master/acore-module.json") if [[ "$b" != "none" ]]; then - echo "-> $mod (tested with AC version: $v)" + printf "%b -> %b (tested with AC version: %s)%b\n" "" "${C_GREEN}${mod}${C_RESET}" "$v" "" else - echo "-> $mod (NOTE: The module latest tested AC revision is Unknown)" + printf "%b -> %b %b(NOTE: The module latest tested AC revision is Unknown)%b\n" "" "${C_GREEN}${mod}${C_RESET}" "${C_YELLOW}" "${C_RESET}" fi done @@ -590,7 +754,7 @@ function inst_module_install { local modules=("$@") - echo "Installing modules: ${modules[*]}" + print_header "Installing modules: ${modules[*]}" if $use_all; then # Install all modules from the list (respecting recorded branch and commit). @@ -602,14 +766,18 @@ function inst_module_install { local dup_error=0 while read -r repo_ref branch commit; do [ -z "$repo_ref" ] && continue + # Skip excluded modules when checking duplicates + if inst_mod_is_excluded "$repo_ref"; then + continue + fi parsed_output=$(inst_parse_module_spec "$repo_ref") IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output" # dirname defaults to repo name; flat install path uses dirname only if [[ -n "${_seen[$dirname]:-}" ]]; then - echo "Error: duplicate module target directory '$dirname' detected in modules.list:" + print_error "Error: duplicate module target directory '$dirname' detected in modules.list:" echo " - ${_first[$dirname]}" echo " - ${repo_ref}" - echo "Use a custom folder name to disambiguate, e.g.: ${repo_ref}:$dirname-alt" + print_warn "Use a custom folder name to disambiguate, e.g.: ${repo_ref}:$dirname-alt" dup_error=1 else _seen[$dirname]=1 @@ -623,11 +791,16 @@ function inst_module_install { # Second pass: install in flat modules directory (no owner subfolders) while read -r repo_ref branch commit; do [ -z "$repo_ref" ] && continue + # Skip excluded entries during installation + if inst_mod_is_excluded "$repo_ref"; then + print_warn "[$repo_ref] Excluded by MODULES_EXCLUDE_LIST (skipping)." + continue + fi parsed_output=$(inst_parse_module_spec "$repo_ref") IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output" if [ -d "$J_PATH_MODULES/$dirname" ]; then - echo "[$repo_ref] Already installed (skipping)." + print_skip "[$repo_ref] Already installed (skipping)." continue fi @@ -642,9 +815,9 @@ function inst_module_install { local curCommit curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") inst_mod_list_upsert "$repo_ref" "$branch" "$curCommit" - echo "[$repo_ref] Installed." + print_success "[$repo_ref] Installed." else - echo "[$repo_ref] Install failed." + print_error "[$repo_ref] Install failed." exit 1; fi done < <(inst_mod_list_read) @@ -663,7 +836,7 @@ function inst_module_install { # Check if module is already installed (by owner/name matching) existing_repo_ref=$(inst_mod_is_installed "$spec" || true) if [ -n "$existing_repo_ref" ]; then - echo "[$spec] Already installed as [$existing_repo_ref] (skipping)." + print_skip "[$spec] Already installed as [$existing_repo_ref] (skipping)." continue fi @@ -689,14 +862,14 @@ function inst_module_install { fi if [[ "$v" == "none" || "$v" == "not-defined" || "$b" == "none" ]]; then def="$(inst_get_default_branch "$repo_ref")" - echo "Warning: $repo_ref has no compatible acore-module.json; installing from branch '$def' (latest commit)." + print_warn "Warning: $repo_ref has no compatible acore-module.json; installing from branch '$def' (latest commit)." b="$def" fi fi # Use flat directory structure with custom directory name if [ -d "$J_PATH_MODULES/$dirname" ]; then - echo "[$repo_ref] Already installed (skipping)." + print_skip "[$repo_ref] Already installed (skipping)." curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") inst_mod_list_upsert "$repo_ref" "$b" "$curCommit" continue @@ -709,14 +882,14 @@ function inst_module_install { if git -C "$J_PATH_MODULES/$dirname" rev-parse --verify "$override_commit" >/dev/null 2>&1; then git -C "$J_PATH_MODULES/$dirname" checkout --quiet "$override_commit" else - echo "[$repo_ref] Warning: provided commit '$override_commit' not found; staying on branch '$b' HEAD." + print_warn "[$repo_ref] provided commit '$override_commit' not found; staying on branch '$b' HEAD." fi fi curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") inst_mod_list_upsert "$repo_ref" "$b" "$curCommit" - echo "[$repo_ref] Installed in '$dirname'. Please re-run compiling and db assembly." + print_success "[$repo_ref] Installed in '$dirname'. Please re-run compiling and db assembly." else - echo "[$repo_ref] Install failed or module not found" + print_error "[$repo_ref] Install failed or module not found" exit 1; fi done @@ -748,21 +921,26 @@ function inst_module_update { local line repo_ref branch commit newCommit owner modname url dirname while read -r repo_ref branch commit; do [ -z "$repo_ref" ] && continue + # Skip excluded modules during update --all + if inst_mod_is_excluded "$repo_ref"; then + print_warn "[$repo_ref] Excluded by MODULES_EXCLUDE_LIST (skipping)." + continue + fi parsed_output=$(inst_parse_module_spec "$repo_ref") IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output" dirname="${dirname:-$modname}" if [ ! -d "$J_PATH_MODULES/$dirname/" ]; then - echo "[$repo_ref] Not installed locally, skipping." + print_skip "[$repo_ref] Not installed locally, skipping." continue fi if Joiner:upd_repo "$url" "$dirname" "$branch" ""; then newCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") inst_mod_list_upsert "$repo_ref" "$branch" "$newCommit" - echo "[$repo_ref] Updated to latest commit on '$branch'." + print_success "[$repo_ref] Updated to latest commit on '$branch'." else - echo "[$repo_ref] Cannot update" + print_error "[$repo_ref] Cannot update" fi done < <(inst_mod_list_read) else @@ -793,11 +971,11 @@ function inst_module_update { fi if [[ "$v" == "none" || "$v" == "not-defined" || "$b" == "none" ]]; then if branch=$(git -C "$J_PATH_MODULES/$dirname" rev-parse --abbrev-ref HEAD 2>/dev/null); then - echo "Warning: $repo_ref has no compatible acore-module.json; updating current branch '$branch'." + print_warn "Warning: $repo_ref has no compatible acore-module.json; updating current branch '$branch'." b="$branch" else def="$(inst_get_default_branch "$repo_ref")" - echo "Warning: $repo_ref has no compatible acore-module.json and no git branch detected; updating default branch '$def'." + print_warn "Warning: $repo_ref has no compatible acore-module.json and no git branch detected; updating default branch '$def'." b="$def" fi fi @@ -806,12 +984,12 @@ function inst_module_update { if Joiner:upd_repo "$url" "$dirname" "$b" ""; then newCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") inst_mod_list_upsert "$repo_ref" "$b" "$newCommit" - echo "[$repo_ref] Done, please re-run compiling and db assembly" + print_success "[$repo_ref] Done, please re-run compiling and db assembly" else - echo "[$repo_ref] Cannot update" + print_error "[$repo_ref] Cannot update" fi else - echo "[$repo_ref] Cannot update! Path doesn't exist ($J_PATH_MODULES/$dirname/)" + print_error "[$repo_ref] Cannot update! Path doesn't exist ($J_PATH_MODULES/$dirname/)" fi done fi @@ -840,9 +1018,9 @@ function inst_module_remove { dirname="${dirname:-$modname}" if Joiner:remove "$dirname" ""; then inst_mod_list_remove "$repo_ref" - echo "[$repo_ref] Done, please re-run compiling" + print_success "[$repo_ref] Done, please re-run compiling" else - echo "[$repo_ref] Cannot remove" + print_error "[$repo_ref] Cannot remove" fi done diff --git a/apps/installer/test/test_module_commands.bats b/apps/installer/test/test_module_commands.bats index 30ee94e816..1223a80a6d 100755 --- a/apps/installer/test/test_module_commands.bats +++ b/apps/installer/test/test_module_commands.bats @@ -146,6 +146,27 @@ teardown() { [ "$output" = "myorg/mymodule" ] } +@test "inst_extract_owner_name should handle URLs with ports correctly" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test HTTPS URL with port + run inst_extract_owner_name "https://example.com:8080/user/repo.git" + [ "$output" = "user/repo" ] + + # Test SSH URL with port + run inst_extract_owner_name "ssh://git@example.com:2222/owner/module" + [ "$output" = "owner/module" ] + + # Test URL with port and custom directory (should ignore the directory part) + run inst_extract_owner_name "https://gitlab.internal:9443/team/project.git:custom-dir" + [ "$output" = "team/project" ] + + # Test complex URL with port (should extract owner/name correctly) + run inst_extract_owner_name "https://git.company.com:8443/department/awesome-module.git" + [ "$output" = "department/awesome-module" ] +} + @test "duplicate module entries should be prevented across different formats" { cd "$TEST_DIR" source "$TEST_DIR/apps/installer/includes/includes.sh" @@ -189,6 +210,30 @@ teardown() { [ "$status" -ne 0 ] } +@test "module installed via URL with port should be recognized correctly" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Install via URL with port + inst_mod_list_upsert "https://gitlab.internal:9443/myorg/my-module.git" "master" "abc123" + + # Should be detected as installed using normalized owner/name + run inst_mod_is_installed "myorg/my-module" + [ "$status" -eq 0 ] + + # Should be detected when checking with different URL format + run inst_mod_is_installed "ssh://git@gitlab.internal:9443/myorg/my-module" + [ "$status" -eq 0 ] + + # Should be detected when checking with custom directory syntax + run inst_mod_is_installed "myorg/my-module:custom-dir" + [ "$status" -eq 0 ] + + # Different module should not be detected + run inst_mod_is_installed "myorg/different-module" + [ "$status" -ne 0 ] +} + @test "cross-format module removal should work" { cd "$TEST_DIR" source "$TEST_DIR/apps/installer/includes/includes.sh" @@ -355,3 +400,356 @@ EOF [ "$owner_name_format" = "azerothcore/mod-transmog" ] [ "$simple_format" = "azerothcore/mod-transmog" ] } + +# Tests for module exclusion functionality + +@test "module exclusion should work with MODULES_EXCLUDE_LIST" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test exclusion with simple name + export MODULES_EXCLUDE_LIST="mod-test-module" + run inst_mod_is_excluded "mod-test-module" + [ "$status" -eq 0 ] + + # Test exclusion with owner/name format + export MODULES_EXCLUDE_LIST="azerothcore/mod-test" + run inst_mod_is_excluded "mod-test" + [ "$status" -eq 0 ] + + # Test exclusion with space-separated list + export MODULES_EXCLUDE_LIST="mod-one mod-two mod-three" + run inst_mod_is_excluded "mod-two" + [ "$status" -eq 0 ] + + # Test exclusion with newline-separated list + export MODULES_EXCLUDE_LIST=" +mod-alpha +mod-beta +mod-gamma +" + run inst_mod_is_excluded "mod-beta" + [ "$status" -eq 0 ] + + # Test exclusion with URL format + export MODULES_EXCLUDE_LIST="https://github.com/azerothcore/mod-transmog.git" + run inst_mod_is_excluded "mod-transmog" + [ "$status" -eq 0 ] + + # Test non-excluded module + export MODULES_EXCLUDE_LIST="mod-other" + run inst_mod_is_excluded "mod-transmog" + [ "$status" -eq 1 ] + + # Test empty exclusion list + unset MODULES_EXCLUDE_LIST + run inst_mod_is_excluded "mod-transmog" + [ "$status" -eq 1 ] +} + +@test "install --all should skip excluded modules" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Setup modules list with excluded module + mkdir -p "$TEST_DIR/conf" + cat > "$TEST_DIR/conf/modules.list" << 'EOF' +azerothcore/mod-transmog master abc123 +azerothcore/mod-excluded master def456 +EOF + + # Set exclusion list + export MODULES_EXCLUDE_LIST="mod-excluded" + + # Mock the install process to capture output + run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install --all 2>&1" + + # Should show that excluded module was skipped + [[ "$output" == *"azerothcore/mod-excluded"* && "$output" == *"Excluded by MODULES_EXCLUDE_LIST"* && "$output" == *"skipping"* ]] +} + +@test "exclusion should work with multiple formats in same list" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test multiple exclusion formats + export MODULES_EXCLUDE_LIST="mod-test https://github.com/azerothcore/mod-transmog.git custom/mod-other" + + run inst_mod_is_excluded "mod-test" + [ "$status" -eq 0 ] + + run inst_mod_is_excluded "azerothcore/mod-transmog" + [ "$status" -eq 0 ] + + run inst_mod_is_excluded "custom/mod-other" + [ "$status" -eq 0 ] + + run inst_mod_is_excluded "mod-allowed" + [ "$status" -eq 1 ] +} + +# Tests for color support functionality + +@test "color functions should work correctly" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test that print functions exist and work + run print_info "test message" + [ "$status" -eq 0 ] + + run print_warn "test warning" + [ "$status" -eq 0 ] + + run print_error "test error" + [ "$status" -eq 0 ] + + run print_success "test success" + [ "$status" -eq 0 ] + + run print_skip "test skip" + [ "$status" -eq 0 ] + + run print_header "test header" + [ "$status" -eq 0 ] +} + +@test "color support should respect NO_COLOR environment variable" { + cd "$TEST_DIR" + + # Test with NO_COLOR set + export NO_COLOR=1 + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Colors should be empty when NO_COLOR is set + [ -z "$C_RED" ] + [ -z "$C_GREEN" ] + [ -z "$C_RESET" ] +} + +# Tests for interactive menu system + +@test "module help should display comprehensive help" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + run inst_module_help + [ "$status" -eq 0 ] + + # Should contain key sections + [[ "$output" =~ "Module Manager Help" ]] + [[ "$output" =~ "Usage:" ]] + [[ "$output" =~ "Module Specification Syntax:" ]] + [[ "$output" =~ "Examples:" ]] +} + +@test "module list should show installed modules correctly" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Setup modules list + mkdir -p "$TEST_DIR/conf" + cat > "$TEST_DIR/conf/modules.list" << 'EOF' +azerothcore/mod-transmog master abc123 +custom/mod-test develop def456 +EOF + + run inst_module_list + [ "$status" -eq 0 ] + + # Should show both modules + [[ "$output" =~ "mod-transmog" ]] + [[ "$output" =~ "custom/mod-test" ]] + [[ "$output" =~ "master" ]] + [[ "$output" =~ "develop" ]] +} + +@test "module list should handle empty list gracefully" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Ensure empty modules list + mkdir -p "$TEST_DIR/conf" + touch "$TEST_DIR/conf/modules.list" + + run inst_module_list + [ "$status" -eq 0 ] + [[ "$output" =~ "No modules installed" ]] +} + +# Tests for advanced parsing edge cases + +@test "parsing should handle complex URL formats" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test GitLab URL with custom directory and branch + run inst_parse_module_spec "https://gitlab.com/myorg/mymodule.git:custom-dir@develop:abc123" + [ "$status" -eq 0 ] + IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output" + [ "$repo_ref" = "https://gitlab.com/myorg/mymodule.git" ] + [ "$owner" = "myorg" ] + [ "$name" = "mymodule" ] + [ "$branch" = "develop" ] + [ "$commit" = "abc123" ] + [ "$dirname" = "custom-dir" ] +} + +@test "parsing should handle URLs with ports correctly (fix for port/dirname confusion)" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test HTTPS URL with port - should NOT treat port as dirname + run inst_parse_module_spec "https://example.com:8080/user/repo.git" + [ "$status" -eq 0 ] + IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output" + [ "$repo_ref" = "https://example.com:8080/user/repo.git" ] + [ "$owner" = "user" ] + [ "$name" = "repo" ] + [ "$branch" = "-" ] + [ "$commit" = "-" ] + [ "$url" = "https://example.com:8080/user/repo.git" ] + [ "$dirname" = "repo" ] # Should default to repo name, NOT port number +} + +@test "parsing should handle URLs with ports and custom directory correctly" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test URL with port AND custom directory - should parse custom directory correctly + run inst_parse_module_spec "https://example.com:8080/user/repo.git:custom-dir" + [ "$status" -eq 0 ] + IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output" + [ "$repo_ref" = "https://example.com:8080/user/repo.git" ] + [ "$owner" = "user" ] + [ "$name" = "repo" ] + [ "$branch" = "-" ] + [ "$commit" = "-" ] + [ "$url" = "https://example.com:8080/user/repo.git" ] + [ "$dirname" = "custom-dir" ] # Should be custom-dir, not port number +} + +@test "parsing should handle SSH URLs with ports correctly" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test SSH URL with port + run inst_parse_module_spec "ssh://git@example.com:2222/user/repo" + [ "$status" -eq 0 ] + IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output" + [ "$repo_ref" = "ssh://git@example.com:2222/user/repo" ] + [ "$owner" = "user" ] + [ "$name" = "repo" ] + [ "$dirname" = "repo" ] # Should be repo name, not port number +} + +@test "parsing should handle SSH URLs with ports and custom directory" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test SSH URL with port and custom directory + run inst_parse_module_spec "ssh://git@example.com:2222/user/repo:my-custom-dir@develop" + [ "$status" -eq 0 ] + IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output" + [ "$repo_ref" = "ssh://git@example.com:2222/user/repo" ] + [ "$owner" = "user" ] + [ "$name" = "repo" ] + [ "$branch" = "develop" ] + [ "$dirname" = "my-custom-dir" ] +} + +@test "parsing should handle complex URLs with ports, custom dirs, and branches" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test comprehensive URL with port, custom directory, branch, and commit + run inst_parse_module_spec "https://gitlab.example.com:9443/myorg/myrepo.git:custom-name@feature-branch:abc123def" + [ "$status" -eq 0 ] + IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output" + [ "$repo_ref" = "https://gitlab.example.com:9443/myorg/myrepo.git" ] + [ "$owner" = "myorg" ] + [ "$name" = "myrepo" ] + [ "$branch" = "feature-branch" ] + [ "$commit" = "abc123def" ] + [ "$url" = "https://gitlab.example.com:9443/myorg/myrepo.git" ] + [ "$dirname" = "custom-name" ] +} + +@test "URL port parsing regression test - ensure ports are not confused with directory names" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # These are the problematic cases that the fix addresses + local test_cases=( + "https://example.com:8080/repo.git" + "https://gitlab.internal:9443/group/project.git" + "ssh://git@server.com:2222/owner/repo" + "https://git.company.com:8443/team/module.git" + ) + + for spec in "${test_cases[@]}"; do + run inst_parse_module_spec "$spec" + [ "$status" -eq 0 ] + IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output" + + # Critical: dirname should NEVER be a port number + [[ ! "$dirname" =~ ^[0-9]+$ ]] || { + echo "FAIL: Port number '$dirname' incorrectly parsed as directory name for spec: $spec" + return 1 + } + + # dirname should be the repository name by default + local expected_name + if [[ "$spec" =~ /([^/]+)(\.git)?$ ]]; then + expected_name="${BASH_REMATCH[1]}" + expected_name="${expected_name%.git}" + fi + [ "$dirname" = "$expected_name" ] || { + echo "FAIL: Expected dirname '$expected_name' but got '$dirname' for spec: $spec" + return 1 + } + done +} + +@test "parsing should handle URL with custom directory but no branch" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + run inst_parse_module_spec "https://github.com/owner/repo.git:my-dir" + [ "$status" -eq 0 ] + IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output" + [ "$repo_ref" = "https://github.com/owner/repo.git" ] + [ "$dirname" = "my-dir" ] + [ "$branch" = "-" ] +} + +@test "modules list should maintain alphabetical order" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Add modules in random order + inst_mod_list_upsert "zeta/mod-z" "master" "abc" + inst_mod_list_upsert "alpha/mod-a" "master" "def" + inst_mod_list_upsert "beta/mod-b" "master" "ghi" + + # Read the list and verify alphabetical order + local entries=() + while read -r repo_ref branch commit; do + [[ -z "$repo_ref" ]] && continue + entries+=("$repo_ref") + done < <(inst_mod_list_read) + + # Should be in alphabetical order by owner/name + [ "${entries[0]}" = "alpha/mod-a" ] + [ "${entries[1]}" = "beta/mod-b" ] + [ "${entries[2]}" = "zeta/mod-z" ] +} + +@test "module dispatcher should handle unknown commands gracefully" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + run inst_module "unknown-command" + [ "$status" -eq 1 ] + [[ "$output" =~ "Unknown module command" ]] +}
\ No newline at end of file |