ilscipio/scipio-erp

View on GitHub
git-addons

Summary

Maintainability
Test Coverage
#!/usr/bin/env bash

project_dir="$(pwd)"

print_title() { echo -e "SCIPIO-ERP Addons and Themes Git Merging Script\n"; }

print_usage() {
  echo "usage: ./git-addons [action] [addons]... [options]...

ACTIONS

  help                      Show this help dialog.

  config                    Configures the specified addon(s) and (by default) commits
                            any configuration changes made to the Git working copy.
                            
                            Note: The 'pull' command automatically runs this for newly-added addons.
                            Note (2018-08-14): This currently does nothing for most addons; reserved for future use.
                            
                            Command-specific options (see options descriptions):
                                -t, --no-commit       Do not create a Git commit for the config changes.
                      
  install-xxxx [addons]     Combines the 'pull', 'config' and 'load-xxx' commands.
  
                            Runs 'pull' to fetch the specified addons, followed by 'config',
                            followed by 'load-xxx' to load the specified seed and/or demo data.
                            This is often enough to fully install an addon in a single copy
                            of Scipio (for demoing or small setups), but usually this is
                            not ideal for production setups (see discussion below).
                            
                            Command-specific options:
                                -t, --no-commit       Do not create a Git commit for config changes.

  list                      List currently installed addons.
  
                            Command-specific options:
                                -a, --all             Show all recognized addons instead of installed ones.

  load-xxxx [addons]        Loads seed and/or demo data for the specified addon(s) only,
                            where load-xxxx is an Ant data load target ('./ant load-xxx') or alias.
                            This addon must first have been added using 'pull'.
                      
                            Common targets (for more descriptions and targets, use: './ant -p'):
                                load-demo           Load all data, including demo data (for demoing Scipio)
                                load-production     Load seed, seed-initial and ext data (alias for load-extseed)
                                load-seed           Load only seed data (not seed-initial, demo, or ext*)
                          
                            Command-specific options:
                                (none)
                      
  pull [addons]             Add or merge as appropriate addons into working copy using git subtree.
                            
                            In addition, for newly-added addons, this will automatically attempt to run the 'config' 
                            action if applicable, unless --no-config is passed.
                            
                            Command-specific options:
                                -a, --all           Update all existing supported addons.
                                -g, --no-config     Do not run 'config' action.
                        
  push [addons]             Push addons to their upstream repos using git subtree.
  
                            For this command to work, git-addons-config entries must be configured with the "push" option,
                            otherwise must contain SCIPIO_ADDONS_OP_PUSH_ALL=true.

                            Command-specific options:
                                -a, --all           Update all existing supported addons.

  remote-reset [addons]     Force reset addon remotes.
  
                            Command-specific options:
                                -a, --all           Update all existing supported addons.
                                
OPTIONS (must come after ACTION and ADDON names):

  -a, --all                 Apply action to all existing supported addons.
  
  -n, --dry-run             Dry run (git commands that modify working copy are not run)

  -z, --no-commit           Do not commit configuration changes produced by the 'config'
                            or 'install' commands.
                      
                            If you specify this, you may need to manually commit the
                            changes afterward (if any), using 'git commit' on command line;
                            this allows specifying a custom commit message.
                            
                            Note: The 'pull' command always creates a new commit regardless
                            of this flag (git subtree always creates a new commit).

  -g, --no-config           pull: Prevent automatic run of the 'config' action hook.

  -e, --no-msg-edit         Sets GIT_MERGE_AUTOEDIT=no, so no git prompts for merge messages (unless problems).

  -h, --help                General help or help about specific command

  -v, --verbose             Verbose output (currently also counts as 'debug' - output subject to change)

DESCRIPTION

This script uses the git subtree command to integrate SCIPIO-ERP addons
into your working copy of ScipioCE/EE. Its primary purpose is to allow
easy pulling and merging of addon files from the Scipio Git repositories.
It can also be used launch addon configuration and data load;
this functionality is largely a wrapper around existing Ant targets, for which
you can find further descriptions using: './ant -p'

For more information about Scipio addons and examples, visit:
  https://www.scipioerp.com/community/end-user/addons/

Configuration can be done in the accompanying git-addons-config file (and committed), 
or instead by creating a git-addons-config-local file (ignored by git).

In order to use this script with purchased addons, you must set the variable
SCIPIO_ADDONS_URL_EE in the accompanying git-addons-config/git-addons-config-local script
to the base ssh://.../scipio-erp/ addons location provided when you purchase an addon.

For enterprise addons, you must have an SSH agent loaded with your private key for access
to the Ilscipio (or other) servers.

The addons' histories will appear as squashed in your working copy.
This is normal and made required by git subtree.

This script handles common merge cases. If you have any issues,
you may occasionally need to consult and use the git subtree command directly.
For more information on git subtree, type 'git subtree --help'.

NOTE: There is no official support for git submodules at this time, and using
submodules is not recommended. subtrees should be used for most client projects.

REQUIREMENTS

* Bash 4 or higher (for Linux/Mac; for Windows, use Git Bash, Cygwin or other emulation)
* Git 2.0 or higher (with git subtree command)

INSTALLATION

Addons can be installed at 2 different times: before or after you have loaded the data
for the Scipio platform ('./ant [load-demo|load-extseed|...]' or install.sh/install.bat).

Before platform data load: If you add the addon before the platform data is loaded, you simply need to
use the 'pull' command to pull the addon files, and it will trigger 'config'
automatically if needed. Afterward, when you run the './ant [load-demo|load-extseed|...]'
or install.sh/install.bat commands, the addon data will be automatically loaded 
along with all other components.

After platform data load: If you add the addon after your Scipio platform data is already loaded,
you must run the data load for the specific addon after pulling it. This can be done either
using the dedicated 'load-xxxx' command (usually load-demo or load-production/load-extseed),
or all at once including pull using the high-level 'install-xxxx' command.

The 'install-xxxx' command is mainly appropriate for small projects or to demo Scipio,
where only one database is involved. If you have a development/production setup,
you will likely need to push the changes of the 'pull' command to your Git repository,
pull it to your production (and other) branches, and finally run the appropriate 'load-xxxx'
for every database instance you have (e.g. staging vs production).

EXAMPLES

List addons:
* ./git-addons list --all

Pull addons from Scipio Git (always makes a commit; configure is automatically invoked if applicable):
* ./git-addons pull angular-shop
* ./git-addons pull angular-shop ignite-admin-theme --dry-run
* ./git-addons pull --all
* ./git-addons pull angular-shop --no-config    # skip configure

(Re-)Configure addon files on-demand (usually already done on first pull):
* ./git-addons config angular-shop
* ./git-addons config angular-shop --no-commit  # don't commit, for manual commit message later

Load data for addon (using Ant load-xxxx targets or aliases):
* ./git-addons load-demo angular-shop
* ./git-addons load-production paypal

Combined pull, configure and load data for new addon into existing database, in one command (demo or small project):
* ./git-addons install-demo angular-shop
* ./git-addons install-production paypal

Steps to pull, configure and load data for new addon into existing databases (production/company project):
1. ./git-addons pull paypal                # Get the addon files, configure (if applicable)
2. git push                                # Push the addon and configuration back to your own repository
3. git pull                                # Pull changes on each server
3. ./git-addons load-production paypal     # Load data - run once for every database (staging, production, etc.)
"
}

#
# DEV NOTES: known issues:
# * FIXME: non-exported var names should be lowercase (historical)
# * FIXME: not POSIX, requires bash 4
#

print_usage_cmd_pull() {
  echo "pull: Add or merge as appropriate addons into working copy using git subtree.

If its component directory does not exist, it is added using 'git subtree add' command.

If its component directory already exists, it is pulled using 'git subtree pull' command.

The command internally knows the correct target folder for the SCIPIO-ERP addons.
"
}

print_usage_cmd_push() {
  echo "push: Push addons to their upstream repos using git subtree.

The command internally knows the correct target folder for the SCIPIO-ERP addons.
"
}

print_usage_cmd_remote_reset() {
  echo "remote-reset: Force reset addon remotes.

May be needed if you have old repository locations as the pull command
will not override existing remotes or they may be outdated.
"
}

join_string() { 
  local IFS="$1"; shift; echo "$*"; 
}

print_addon_line() {
  get_addon_info "$2" || return
  if [ "$1" = "${ADDON_REPOTYPE}" ]; then
    echo "${ADDON_NAME} -> ${ADDON_DIR} $(print_addon_format)"
  fi
}

print_addon_format() {
  if is_addon_dir_present; then
    if is_addon_subtree; then
      echo "[installed, subtree]"
    elif is_addon_submodule_known; then
      echo "[installed, submodule]"
    elif is_addon_subtree_basic; then
      if is_remote_exists "${ADDON_NAME}"; then
        echo "[present - warning: could not validate as subtree; may not be compatible with script; git subtree may fail]"
      else
        echo "[present - warning: could not validate as subtree; addon remote appears missing - try remote-reset]"
      fi
    elif is_addon_submodule; then
      echo "[present - warning: Appears to be a submodule of unknown origin; incompatible with script]"
    elif is_addon_dir_empty; then
      echo "[present - warning: Empty addon directory; incompatible with script]"
    else
      echo "[present - warning: Does not appear to be subtree-compatible; incompatible with script]"
    fi
  fi
}

sort_args() {
  echo "$@" | xargs -n1 | sort -u | xargs
}

list_addons_core() {
  echo -e "\nCommunity addons:"
  for ADDON; do
    print_addon_line ce "${ADDON}"
  done
  echo -e "\nEnterprise addons:"
  for ADDON; do
    print_addon_line ee "${ADDON}"
  done
  if [ ! -z "${SCIPIO_ADDONS_URL_DEV}" ]; then
    echo -e "\nDev addons:"
    for ADDON; do
      print_addon_line dev "${ADDON}"
    done
  fi
}

list_addons() {
  get_present_addons
  echo "Addons installed in your working copy:"
  if [ -z "${ADDONS}" ]; then
    echo -e "\n(no subtree-compatible addons detected in working copy)"
    return
  fi
  local ADDONS=$(sort_args "${ADDONS}")
  list_addons_core ${ADDONS}
}

list_addons_all() {
  echo "All known SCIPIO-ERP addons:"
  local ADDONS=$(sort_args "${!SCIPIO_ADDON_MAP[@]}")
  list_addons_core ${ADDONS}
}

list_addons_all_names() {
  echo "All known SCIPIO-ERP addons:"
  local ADDONS=$(sort_args "${!SCIPIO_ADDON_MAP[@]}")
  echo "${ADDONS}"
}

print_error() {
  echo -e "error: ""$@" >&2
}

print_warn() {
  echo -e "warning: ""$@" >&2
}

print_msg() {
  echo -e "$@"
}

print_verbose() {
  if [ "${VERBOSE}" -eq 1 ]; then echo -e "$@"; fi
}

debug() {
  # FIXME: this will break functionality, needs to go to log or stderr or something...
  #print_verbose "$@"
  true
}

die() {
  print_error "${@}"
  exit 1
}

print_error_req() {
  print_error "$@"
}

print_error_usage() {
  print_error "$@"
  echo ""
  print_usage
}

print_targetaddons_msg() { print_msg "
##############################################
$1: Target addons: ${@:2}
##############################################"
}

print_targetaddons_all_msg() { print_msg "
##############################################
$1: Target addons (auto-detected): ${ADDONS}
NOTE: If any addons appear to not be detected, use list --all command for more details, or specify them explicitly.
##############################################"
}

print_subheader() { print_msg "
===========================
${@:2}
==========================="
}

callgit() {
  if [ "${DRYRUN}" = 1 ]; then echo "dry-run: git $@"; else git "$@"; fi
}

check_min_version() {
  local BASE=$1
  local MIN=$2
  local BASEPARTS=(${BASE//./ })
  local MINPARTS=(${MIN//./ })
  if [[ ${#BASEPARTS[@]} -lt 2 ]] || [[ ${#MINPARTS[@]} -lt 2 ]]; then
    return 2
  fi
  if [[ ${BASEPARTS[0]} -lt ${MINPARTS[0]} ]] || [[ ${BASEPARTS[0]} -eq ${MINPARTS[0]} && ${BASEPARTS[1]} -lt ${MINPARTS[1]} ]]; then
    return 1
  fi
  return 0
}

declare -A SCIPIO_ADDON_DIR_MAP
process_config() {
  SCIPIO_ADDON_DIR_MAP=()
  for ADDON in "${!SCIPIO_ADDON_MAP[@]}"; do
    get_addon_info "${ADDON}" || return
    if [ ! -z "${SCIPIO_ADDON_DIR_MAP[${ADDON_DIR}]}" ]; then
      print_error "Config error (SCIPIO_ADDON_MAP): Illegal duplicate target addon dir detected: ${ADDON_DIR}"
      return 1
    fi
    if [ "${ADDON}" != "${ADDON_NAME}" ]; then
      print_error "Config error (SCIPIO_ADDON_MAP): Illegal entry detected having array key (${ADDON}) different from name property (${ADDON_NAME}) - must be same."
      return 1
    fi
    SCIPIO_ADDON_DIR_MAP["${ADDON_DIR}"]="${ADDON_INFO_STR}"
  done
}

check_requirements() {
  if [ -z "${BASH_VERSION}" ]; then
    print_error_req "Could not detect bash version"
    return 2
  fi
  if ! check_min_version "${BASH_VERSION}" "4.0"; then
    print_error_req "insufficient bash version - required: 4.0, you: ${BASH_VERSION}"
    return 2
  fi
  local git_ver=$(git --version)
  if [ $? -ne 0 ] || [ -z "${git_ver}" ]; then
    print_error_req "Could not detect git version - is git installed?"
    return 2
  fi
  local git_ver=$(echo -n "${git_ver}" | sed -r -n 's/^git\s\s*version\s\s*(.*)$/\1/p')
  if [ $? -ne 0 ] || [ -z "${git_ver}" ] || ! check_min_version "${git_ver}" "2.0"; then
    print_error_req "Insufficient git version - required: 2.0, system: ${git_ver}"
    return 2
  fi
  if ! check_min_version "${git_ver}" "2.7"; then
    print_warn "You are running git ${git_ver}; this script has not been tested with git version less than 2.7"
  fi
  # FIXME: subtree command test broken since git 2.20.0 or .1, gone from list (?)
  #if ! git --help --all | grep -q 'subtree'; then
  #  print_error_req "git subtree command does not appear installed - upgrade git?"
  #  return 2
  #fi
  if [ ! -e ".git/config" ]; then
    print_error_req "This directory does not appear to be a git working copy - this script requires SCIPIO-ERP git repository"
    return 2
  fi
  if [ ! -d "addons" ] || [ ! -d "themes" ]; then
    print_error_req "Addons or themes subfolder is missing from directory - invalid SCIPIO-ERP working copy"
    return 2
  fi
  if [ ! -f "framework/base/config/scipiometainfo.properties" ]; then
    print_error_req "Missing file: framework/base/config/scipiometainfo.properties - invalid SCIPIO-ERP working copy"
    return 2
  fi
  if [ -z "${SCIPIO_ADDONS_URL_EE}" ]; then
    print_msg "NOTE: The variable SCIPIO_ADDONS_URL_EE is not set. You will only be able to install community addons from Github. \
If you have purchased an addon from the Scipio store, you must input the base address (ssh://.../scipio-erp/) provided to you \
into the git-addons-config (+ git commit) or git-addons-config-local file, including trailing slash. See './git-addons help' and git-addons-config for details.\n"
    if [ -z "${SCIPIO_ADDONS_URL_CE}" ]; then
      print_error_req "Could not read any addons URL from git-addons-config script"
      return 2
    fi
  fi
  if [ -z "${SCIPIO_BRANCH}" ]; then
    determine_scipio_branch || return
    print_msg "Detected Scipio Git branch: ${SCIPIO_BRANCH}\n"
  fi
}

determine_scipio_branch() {
  SCIPIO_BRANCH=$(grep '^scipio.release.vcbranch=' "framework/base/config/scipiometainfo.properties" | grep -o '[^= ]*$')
  if [ ! -z "${SCIPIO_BRANCH}" ]; then
    return 0
  fi
  local SCPVER=$(grep '^scipio.release.branch=' "framework/base/config/scipiometainfo.properties" | grep -o '[^= ]*$')
  if [ "${SCPVER}" = "dev" ]; then
    SCIPIO_BRANCH="master"
  elif [ "${SCPVER}" = "main" ]; then
    local SCPVERSPEC=$(grep '^scipio.release.version.general=' "framework/base/config/scipiometainfo.properties" | grep -o '[^= ]*$')
    if [ -z "${SCPVERSPEC}" ]; then
      print_error_req "Scipio version in framework/base/config/scipiometainfo.properties appears invalid - try updating script?"
      return 2
    fi
    if [ "${SCPVERSPEC}" = "2.0" ]; then
      SCIPIO_BRANCH="2.x"
    else
      SCIPIO_BRANCH="${SCPVERSPEC}"
    fi
  else
    print_error_req "Scipio version in framework/base/config/scipiometainfo.properties appears invalid - try updating script?"
    return 2
  fi
}

extract_pos_args() {
  for ARG; do if [[ "${ARG}" == -* ]]; then return; fi; echo -n " ${ARG}"; done
}

parse_options() {
  # Official options
  ACTION="${1}"
  VERBOSE=0
  HELP=0
  ALLADDONS=0
  DRYRUN=0
  NOCOMMIT=0
  NOCONFIG=0
  
  # Unofficial/unsupported options
  REBASE=0
  REPOTYPE=subtree
  DOCHECKOUT=0
  
  # Internal
  DATA_TARGET=""
  
  if [ -z "${ACTION}" ]; then
    print_error_usage "missing action argument"
    return 2
  fi

  if [ "${ACTION}" = "help" ] || [ "${ACTION}" = "-h" ] || [ "${ACTION}" = "--help" ]; then
    ACTION=help
    HELP=1
  elif [ "${ACTION}" = "pull" ]; then
    TARGETADDONS=$(extract_pos_args "${@:2}")
  elif [ "${ACTION}" = "push" ]; then
    TARGETADDONS=$(extract_pos_args "${@:2}")
  elif [ "${ACTION}" = "remote-reset" ]; then
    TARGETADDONS=$(extract_pos_args "${@:2}")
  elif [[ "${ACTION}" == load-* ]]; then
    TARGETADDONS=$(extract_pos_args "${@:2}")
    DATA_TARGET="${ACTION}"
  elif [[ "${ACTION}" == install-* ]]; then
    TARGETADDONS=$(extract_pos_args "${@:2}")
    DATA_TARGET="load-${ACTION#install-}"
  elif [ "${ACTION}" = "config" ]; then
    TARGETADDONS=$(extract_pos_args "${@:2}")
  elif [ "${ACTION}" = "list" ]; then
    true
  else
    print_error_usage "Unrecognized action: ${ACTION}"
    return 2
  fi

  for ARG; do
    if [ "${ARG}" = "-v" ] || [ "${ARG}" = "--verbose" ]; then
      VERBOSE=1
    elif [ "${ARG}" = "-h" ] || [ "${ARG}" = "--help" ]; then
      HELP=1
    elif [ "${ARG}" = "-a" ] || [ "${ARG}" = "--all" ]; then
      ALLADDONS=1
      if [ ! -z "${TARGETADDONS}" ]; then
        print_error_usage "--all cannot be used at same time as addon-name list."
        return 2
      fi
    elif [ "${ARG}" = "-n" ] || [ "${ARG}" = "--dry-run" ]; then
      DRYRUN=1
    elif [ "${ARG}" = "-e" ] || [ "${ARG}" = "--no-msg-edit" ]; then
      export GIT_MERGE_AUTOEDIT=no
    elif [ "${ARG}" = "-t" ] || [ "${ARG}" = "--no-commit" ]; then
      NOCOMMIT=1
    elif [ "${ARG}" = "-g" ] || [ "${ARG}" = "--no-config" ]; then
      NOCONFIG=1
    elif [ "${ARG}" = "--rebase" ]; then
      REBASE=1
    elif [ "${ARG}" = "--subtree" ]; then
      # already the default
      REPOTYPE=subtree
    elif [ "${ARG}" = "--submodule" ]; then
      REPOTYPE=submodule
    elif [ "${ARG}" = "--checkout" ]; then
      DOCHECKOUT=1
    elif [[ "${ARG}" == -* ]]; then
      print_error_usage "Unrecognized option: ${ARG}"
      return 2
    fi
  done
}

process_action() {
  if [ "${ACTION}" = "help" ]; then
    print_usage
  elif [ "${ACTION}" = "pull" ]; then
    if [ "${HELP}" -eq 1 ]; then
      print_usage_cmd_pull
    else
      if [ "${ALLADDONS}" -eq 1 ]; then
        pull_addons_all || return
      else
        if [ -z "${TARGETADDONS}" ]; then
          print_error_usage "missing addon names or --all arguments"
          return 2
        fi
        check_target_addons ${TARGETADDONS} || return
        pull_addons ${TARGETADDONS} || return
      fi
    fi
  elif [ "${ACTION}" = "push" ]; then
    if [ "${HELP}" -eq 1 ]; then
      print_usage_cmd_push
    else
      if [ "${ALLADDONS}" -eq 1 ]; then
        push_addons_all || return
      else
        if [ -z "${TARGETADDONS}" ]; then
          print_error_usage "missing addon names or --all arguments"
          return 2
        fi
        check_target_addons ${TARGETADDONS} || return
        push_addons ${TARGETADDONS} || return
      fi
    fi
  elif [ "${ACTION}" = "remote-reset" ]; then
    if [ "${HELP}" -eq 1 ]; then
      print_usage_cmd_remote_reset
    else
      if [ "${ALLADDONS}" -eq 1 ]; then
        remote_reset_addons_all
      else
        if [ -z "${TARGETADDONS}" ]; then
          print_error_usage "missing addon names or --all arguments"
          return 2
        fi
        check_target_addons ${TARGETADDONS} || return
        remote_reset_addons ${TARGETADDONS} || return
      fi
    fi
  elif [ "${ACTION}" = "list" ]; then
    if [ "${ALLADDONS}" -eq 1 ]; then
      list_addons_all || return
    else
      list_addons || return
    fi
  elif [[ "${ACTION}" == load-* ]]; then
    if [ "${ALLADDONS}" -eq 1 ]; then
      load_addons_all || return
    else
      check_target_addons ${TARGETADDONS} || return
      load_addons ${TARGETADDONS} || return
    fi
  elif [[ "${ACTION}" == install-* ]]; then
    if [ "${ALLADDONS}" -eq 1 ]; then
      install_addons_all || return
    else
      check_target_addons ${TARGETADDONS} || return
      install_addons ${TARGETADDONS} || return
    fi
  elif [ "${ACTION}" = "config" ]; then
    if [ "${ALLADDONS}" -eq 1 ]; then
      config_addons_all || return
    else
      check_target_addons ${TARGETADDONS} || return
      config_addons ${TARGETADDONS} || return
    fi
  fi
}

process_main() {
  print_title || return $?
  process_config || return $?
  check_requirements || return $?
  parse_options "$@" || return $?
  process_action || return $?
}

check_target_addons() {
  for ADDON; do
    local ADDONINFO="${SCIPIO_ADDON_MAP[${ADDON}]}"
    if [ -z "${ADDONINFO}" ]; then
      print_error "Unrecognized addon: ${ADDON} (use list --all to see available)\n"
      list_addons_all_names
      return 2
    fi
  done
  return 0
}

get_present_addons() {
  ADDONS=
  for ADDON in $(sort_args "${!SCIPIO_ADDON_MAP[@]}"); do
    get_addon_info "${ADDON}" || return
    if is_addon_dir_present; then
      ADDONS="${ADDONS} ${ADDON}"
    fi
  done
  ADDONS=$(echo ${ADDONS})
  if [ -z "${ADDONS}" ]; then
    print_msg "No addons detected in working copy; please install addons before using --all: pull [addons]; see: list --all"
    return 2
  fi
  #print_msg "Addons auto-detected in working copy (--all):\n${ADDONS}"
  return 0
}

get_valid_addons() {
  ADDONS=
  for ADDON in $(sort_args "${!SCIPIO_ADDON_MAP[@]}"); do
    get_addon_info "${ADDON}" || return
    if is_addon_supported; then
      ADDONS="${ADDONS} ${ADDON}"
    fi
  done
  ADDONS=$(echo ${ADDONS})
  if [ -z "${ADDONS}" ]; then
    print_msg "No valid addons detected in working copy; please install some addons before using --all (pull [addon-name]); use 'list --all' for available"
    return 2
  fi
  #print_msg "Addons auto-detected in working copy (--all):\n${ADDONS}"
  return 0
}

is_addon_dir_present() {
  [ ! -z "${ADDON_DIR}" ] && [ -d "${ADDON_DIR}" ]
}

is_addon_dir_empty() {
  [ ! -z "${ADDON_DIR}" ] && [ -d "${ADDON_DIR}" ] && [ -z "$(ls -A ${ADDON_DIR})" ]
}

is_addon_submodule() {
  [ ! -z "${ADDON_DIR}" ] && [ -d "${ADDON_DIR}" ] && [ -e "${ADDON_DIR}/.git" ]
}

is_addon_submodule_known() {
  is_addon_submodule && [ "${ADDON_URL}" = "$(print_submodule_url_for_dir "${ADDON_DIR}")" ]
}

print_submodule_url_for_dir() {
  git config --file ".gitmodules" --get "submodule.$1.url"
}

is_addon_subtree() {
  is_addon_subtree_basic && is_addon_subtree_deep
}

is_addon_subtree_basic() {
  [ ! -z "${ADDON_DIR}" ] && [ -d "${ADDON_DIR}" ] && [ ! -z "$(ls -A ${ADDON_DIR})" ] && [ ! -e "${ADDON_DIR}/.git" ]
}

is_addon_subtree_deep() {
  if [ "${VERBOSE}" = 1 ]; then
    local latest_squash=$(find_latest_squash "${ADDON_DIR}")
  else
    local latest_squash=$(find_latest_squash "${ADDON_DIR}" 2>/dev/null)
  fi
  [ ! -z "${latest_squash}" ]
}

# NOTE: function imported and modified from git package 2.17 git-subtree.sh (this is unlikely to ever change much)
# 2019-06-12: MODIFIED git code below to remove the git rev-parse, because it causes this to require
# fully-fetched addon remotes, and this is nothing but problems 
find_latest_squash() {
    debug "Looking for latest squash ($dir)..."
    dir="$1"
    sq=
    main=
    sub=
    # SCIPIO: removed: missing from older git versions: --no-show-signature 
    git log --grep="^git-subtree-dir: $dir/*\$" \
        --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
    while read a b junk
    do
        debug "$a $b $junk"
        debug "{{$sq/$main/$sub}}"
        case "$a" in
        START)
            sq="$b"
            ;;
        git-subtree-mainline:)
            main="$b"
            ;;
        git-subtree-split:)
            # SCIPIO: don't require it to be fetched (rev-parse) - we only need a basic check
            #sub="$(git rev-parse "$b^0")" ||
            #die "could not rev-parse split hash $b from commit $sq"
            sub="$b"
            ;;
        END)
            if test -n "$sub"
            then
                if test -n "$main"
                then
                    # a rejoin commit?
                    # Pretend its sub was a squash.
                    sq="$sub"
                fi
                debug "Squash found: $sq $sub"
                echo "$sq" "$sub"
                break
            fi
            sq=
            main=
            sub=
            ;;
        esac
    done
}

is_addon_supported() {
  is_addon_subtree || is_addon_submodule_known
}

check_addon_supported() {
  if ! is_addon_supported; then
    if is_addon_subtree_basic; then
      print_warn "Addon ${ADDON_NAME} -> ${ADDON_DIR} has no record of being a subtree in its log; git subtree pull may fail (if it does, you may need to remove the directory, re-run this script, and reapply your changes after)"
    elif is_addon_subtree; then
      print_warn "Skipping ${ADDON_NAME} -> ${ADDON_DIR} - submodule URL does not match any known addon URL; assuming not a Scipio addon.\nDetected as: $(print_addon_format)"
      return 2
    else
      print_warn "Skipping ${ADDON_NAME} -> ${ADDON_DIR} - does not appear to be a subtree! Cannot be managed using this script unless removed first.\nDetected as: $(print_addon_format)"
      return 2
    fi
  fi
  return 0
}

declare -a AUTOCONFIG_ADDONS
pull_addons() {
  print_targetaddons_msg "pull" "$@"
  AUTOCONFIG_ADDONS=()
  for ADDON; do
    get_addon_info "${ADDON}" || return
    pull_addon || return
  done
  if [ "${NOCONFIG}" -eq 0 ] && [ ${#AUTOCONFIG_ADDONS[@]} -gt 0 ]; then
    config_addons "${AUTOCONFIG_ADDONS[@]}"
  fi
}

declare -a AUTOCONFIG_ADDONS
pull_addons_all() {
  get_valid_addons || return
  print_targetaddons_all_msg "pull"
  AUTOCONFIG_ADDONS=() 
  for ADDON in ${ADDONS}; do
    get_addon_info "${ADDON}" || return
    pull_addon || return
  done
  if [ "${NOCONFIG}" -eq 0 ] && [ ${#AUTOCONFIG_ADDONS[@]} -gt 0 ]; then
    config_addons "${AUTOCONFIG_ADDONS[@]}"
  fi
}

# TODO: better missing config detection, using file presence and flags in files (add component name to AUTOCONFIG_ADDONS array)
pull_addon() {
  print_subheader "pull" "${ADDON_NAME}"
  if is_addon_dir_present; then
    check_addon_supported || return 0
    if is_addon_submodule; then
      local REBASE_STR=""
      if [ "${REBASE}" -eq 1 ]; then
        local REBASE_STR=" --rebase"
      fi
      cd "${ADDON_DIR}" || { echo "Could not change into dir: ${ADDON_DIR}"; return 1; }

      local CURR_BRANCH=$(git rev-parse --abbrev-ref HEAD)
      if [ "${CURR_BRANCH}" != "${ADDON_BRANCH}" ]; then
        if [ "${DOCHECKOUT}" -eq 1 ]; then
          print_warn "The submodule is not checked out on the expected branch (${ADDON_BRANCH}); is currently on: ${CURR_BRANCH} (or detached head). Now checking out ${ADDON_BRANCH}..."
          callgit checkout "${ADDON_BRANCH}" || { echo "Could not checkout submodule branch ${ADDON_BRANCH}. Please verify."; return 1; }
        else
          print_error "The submodule is not checked out on the expected branch (${ADDON_BRANCH}); is currently on: ${CURR_BRANCH} (or detached head).\nIf you wish to automatically checkout the right branch, pass the --checkout option."
          return 1
        fi
      fi
      print_msg "Running 'git pull${REBASE_STR}' for existing submodule addon ${ADDON_NAME} -> ${ADDON_DIR} (branch: ${ADDON_BRANCH})..."

      callgit pull ${REBASE_STR} || { echo "Pull failed. Please verify, resolve conflicts and re-run script."; return 1; }
      cd "${project_dir}" || { echo "Could not switch back to project directory: ${project_dir}"; return 1; }
    else
      ensure_addon_remote || return

      print_msg "Running 'git subtree pull' for existing subtree addon ${ADDON_NAME} -> ${ADDON_DIR} (branch: ${ADDON_BRANCH})..."
      callgit subtree pull --squash "--prefix=${ADDON_DIR}" "${ADDON_NAME}" "${ADDON_BRANCH}" || \
        { echo "Subtree pull failed. Please verify, resolve conflicts and re-run script. For conflict resolution try 'git mergetool'; afterward, 'git merge --continue'; or to abort, 'git reset --hard'."; return 1; }
    fi
  else
    if [ "${REPOTYPE}" = "submodule" ]; then
      print_msg "Running 'git submodule add' for new submodule addon ${ADDON_NAME} -> ${ADDON_DIR} (branch: ${ADDON_BRANCH})..."

      callgit submodule add -b "${ADDON_BRANCH}" "${ADDON_URL}" "${ADDON_DIR}" || { echo "Submodule add failed. Please verify."; return 1; }
      cd "${ADDON_DIR}" || { echo "Could not change into dir: ${ADDON_DIR}"; return 1; }
      callgit checkout "${ADDON_BRANCH}" || { echo "Could not checkout submodule branch ${ADDON_BRANCH}. Please verify."; return 1; }
      cd "${project_dir}" || { echo "Could not switch back to top dir: ${project_dir}"; return 1; }
      print_msg "\nNOTICE: You have added a new submodule to your root project (.gitmodules and other git properties modified); you will have to commit (and push) the result in your top project manually (see 'git status', or use 'git commit -m \"Added submodule\"')"
    else
      ensure_addon_remote || return

      print_msg "Running 'git subtree add' for new subtree addon ${ADDON_NAME} -> ${ADDON_DIR} (branch: ${ADDON_BRANCH})..."
      callgit subtree add --squash "--prefix=${ADDON_DIR}" "${ADDON_NAME}" "${ADDON_BRANCH}" || { echo "Subtree add failed. Please verify."; return 1; }
    fi
    AUTOCONFIG_ADDONS+=("${ADDON_NAME}")
  fi
}

declare -a AUTOCONFIG_ADDONS
push_addons() {
  print_targetaddons_msg "push" "$@"
  AUTOCONFIG_ADDONS=()
  for ADDON; do
    get_addon_info "${ADDON}" || return
    push_addon || return
  done
}

declare -a AUTOCONFIG_ADDONS
push_addons_all() {
  get_valid_addons || return
  print_targetaddons_all_msg "push"
  AUTOCONFIG_ADDONS=() 
  for ADDON in ${ADDONS}; do
    get_addon_info "${ADDON}" || return
    push_addon || return
  done
}

# TODO: better missing config detection, using file presence and flags in files (add component name to AUTOCONFIG_ADDONS array)
push_addon() {
  print_subheader "push" "${ADDON_NAME}"
  if is_addon_dir_present; then
    check_addon_supported || return 0
    if [ "${ADDON_OPTION_PUSH_ON}" = "true" ] || [ "${SCIPIO_ADDONS_OP_PUSH_ALL}" = "true" ]; then
      if is_addon_submodule; then
        print_error "push: submodules not yet supported - please ask"
        return 1
      else
        ensure_addon_remote || return

        print_msg "Running 'git subtree push' for existing subtree addon ${ADDON_NAME} -> ${ADDON_DIR} (branch: ${ADDON_BRANCH})..."
        callgit subtree push "--prefix=${ADDON_DIR}" "${ADDON_NAME}" "${ADDON_BRANCH}" || \
          { echo "Subtree push failed. Please verify, resolve conflicts and re-run script."; return 1; }
      fi
    else
      print_error "push: not supported for addon, skipping: ${ADDON_NAME}"
    fi
  else
    print_error "push: addon not installed: ${ADDON_NAME}"
  fi
}

remote_reset_addons() {
  print_targetaddons_msg "remote-reset" "$@"
  remote_reset_addons_tips
  for ADDON; do
    get_addon_info "${ADDON}" || return
    remote_reset_addon || return
  done
}

remote_reset_addons_all() {
  get_valid_addons || return
  print_targetaddons_all_msg "remote-reset"
  remote_reset_addons_tips
  for ADDON in ${ADDONS}; do
    get_addon_info "${ADDON}" || return
    # no remotes needed for submodules
    if ! is_addon_submodule; then
      remote_reset_addon || return
    fi
  done
}

remote_reset_addons_tips() {
  print_msg "note: verify remotes using: git remote -v"
}

remote_reset_addon() {
  print_subheader "reset-remote" "${ADDON_NAME}"
  check_addon_supported || return 0
  remote_reset_addon_core
}

ensure_addon_remote() {
  if is_remote_exists "${ADDON_NAME}"; then
    return 0
  fi
  remote_reset_addon_core
}

remote_reset_addon_core() {
  echo "Setting remote for addon ${ADDON_NAME}: ${ADDON_URL}"
  if [ -z "${ADDON_URL}" ]; then
    print_error "Cannot determine a repository URL for addon ${ADDON_NAME}; did you set SCIPIO_ADDONS_URL_EE in git-addons-config or git-addons-config-local?"
    return 1
  fi
  if is_remote_exists "${ADDON_NAME}"; then
    callgit remote remove "${ADDON_NAME}"
  fi
  callgit remote add "${ADDON_NAME}" "${ADDON_URL}" || { print_error "Could not add remote for addon ${ADDON_NAME}"; return 1; }
}

is_remote_exists() {
  git remote | grep -q '^'"${1}"'$'
}

fetch_addon() {
  ensure_addon_remote && callgit fetch "${ADDON_NAME}"
}

load_addons() {
  print_targetaddons_msg "${DATA_TARGET}" "$@"
  get_addons_component_names "$@" || return
  load_addons_exec "$@" || return
}

load_addons_all() {
  get_valid_addons || return
  print_targetaddons_all_msg "${DATA_TARGET}"
  get_addons_component_names ${ADDONS} || return
  load_addons_exec ${ADDONS} || return
}

load_addons_exec() {
  #print_subheader "${DATA_TARGET}" "$@"
  if [ ${#ADDONS_COMPONENT_NAMES[@]} -gt 0 ]; then
    #print_msg "Component names: ${ADDONS_COMPONENT_NAMES[@]}"
    COMPNAMES_STRING=$(join_string , ${ADDONS_COMPONENT_NAMES[@]})
    local REAL_DATA_TARGET="${DATA_TARGET}"
    if [ "${REAL_DATA_TARGET}" = "load-production" ]; then
      local REAL_DATA_TARGET="load-extseed"
    fi
    print_msg "\nExecuting: ./ant ${REAL_DATA_TARGET} -Dcomponent=${COMPNAMES_STRING}\n"
    ./ant "${REAL_DATA_TARGET}" "-Dcomponent=${COMPNAMES_STRING}"
  else
    print_msg "No Scipio components found for data load (addons may not be traditional components)."
  fi
}

config_addons() {
  print_targetaddons_msg "config" "$@"
  for ADDON; do
    get_addon_info "${ADDON}" || return
    config_addon || return
  done
}

config_addons_all() {
  get_valid_addons || return
  print_targetaddons_all_msg "config"
  for ADDON in ${ADDONS}; do
    get_addon_info "${ADDON}" || return
    # no remotes needed for submodules
    if ! is_addon_submodule; then
      config_addon || return
    fi
  done
}

config_addon() {
  print_subheader "config" "${ADDON_NAME}"
  if is_addon_dir_present; then
    #check_addon_supported || return 0 # not really needed, and warnings are mostly about pull
    # TODO
    print_msg "No interactive configuration available or needed."
  else
    print_error "Cannot configure addon '${ADDON_NAME}': does not appear to be present or is invalid. Run 'list' and 'pull' commands first."
    return 1
  fi
}

install_addons() {
  pull_addons "$@" && load_addons "$@"
}

install_addons_all() {
  pull_addons_all && load_addons_all
}

get_addon_info() {
  ADDON_INFO_STR="${SCIPIO_ADDON_MAP[$1]}"
  if [ -z "${ADDON_INFO_STR}" ]; then
    print_error "Invalid addon name or definition: '$1'"
    return 1
  fi
  ADDON_INFO=( ${ADDON_INFO_STR} )
  ADDON_NAME="${ADDON_INFO[0]}"
  ADDON_DIR="${ADDON_INFO[1]}"
  ADDON_REPOTYPE="${ADDON_INFO[2]}"
  ADDON_URLPART="${ADDON_INFO[3]}"
  ADDON_URL=""
  if [[ "${ADDON_URLPART}" == *//* ]]; then # full url
    ADDON_URL="${ADDON_URLPART}"
  else # suffix only; prepend base...
    local repo_url_var="SCIPIO_ADDONS_URL_${ADDON_REPOTYPE^^}"
    local repo_baseurl="${!repo_url_var}"
    if [ ! -z "${repo_baseurl}" ]; then
      ADDON_URL="${repo_baseurl}${ADDON_URLPART}"
    fi
  fi
  if [ ! -z "${ADDON_URL}" ] && [[ "${ADDON_URL}" != *".git" ]]; then
    local url_suffix_var="SCIPIO_ADDONS_URL_SUFFIX_${ADDON_REPOTYPE^^}"
    local repo_urlsuffix="${!url_suffix_var}"
    if [ -z "${repo_urlsuffix}" ]; then local repo_urlsuffix="${SCIPIO_ADDONS_URL_SUFFIX}"; fi
    ADDON_URL="${ADDON_URL}${repo_urlsuffix}.git"
  fi
  ADDON_BRANCHPREFIX="${ADDON_INFO[4]}"
  ADDON_BRANCHPREFIX="${ADDON_BRANCHPREFIX%/}"
  ADDON_BRANCH="${ADDON_BRANCHPREFIX}/${SCIPIO_BRANCH}"
  ADDON_BRANCH="${ADDON_BRANCH#/}"
  ADDON_OPTIONS="${ADDON_INFO[5]}"
  ADDON_OPTION_PUSH_ON=false
  if echo "${ADDON_OPTIONS}" | grep -qP '(^|,)push(=true|,|$)'; then
    ADDON_OPTION_PUSH_ON=true
  fi
}

get_addon_component_info() {
  ADDON_COMPONENT_NAME=""
  if [ -f "${ADDON_DIR}/scipio-component.xml" ]; then
    ADDON_COMPONENT_NAME=$(cat "${ADDON_DIR}/scipio-component.xml" | sed -rn 's/^.*<ofbiz-component\s(.*\s)?name="([^"]*)".*$/\2/p')
    if [ -z "${ADDON_COMPONENT_NAME}" ]; then print_error "Could not determine component name for addon ${ADDON_NAME}; please contact support"; fi
  elif [ -f "${ADDON_DIR}/scipio-theme.xml" ]; then
    ADDON_COMPONENT_NAME=$(cat "${ADDON_DIR}/scipio-theme.xml" | sed -rn 's/^.*<ofbiz-component\s(.*\s)?name="([^"]*)".*$/\2/p')
    if [ -z "${ADDON_COMPONENT_NAME}" ]; then print_error "Could not determine component name for addon ${ADDON_NAME}; please contact support"; fi
  elif [ -f "${ADDON_DIR}/ofbiz-component.xml" ]; then
    ADDON_COMPONENT_NAME=$(cat "${ADDON_DIR}/ofbiz-component.xml" | sed -rn 's/^.*<ofbiz-component\s(.*\s)?name="([^"]*)".*$/\2/p')
    if [ -z "${ADDON_COMPONENT_NAME}" ]; then print_error "Could not determine component name for addon ${ADDON_NAME}; please contact support"; fi
  fi
}

declare -a ADDONS_COMPONENT_NAMES
get_addons_component_names() {
  ADDONS_COMPONENT_NAMES=()
  for ADDON; do
    get_addon_info "${ADDON}"
    if is_addon_dir_present; then
      get_addon_component_info
      if [ ! -z "${ADDON_COMPONENT_NAME}" ]; then
        ADDONS_COMPONENT_NAMES+=("${ADDON_COMPONENT_NAME}")
      fi
    else
      print_error "Addon '${ADDON}' is not present or is invalid. Use 'list' and 'pull' commands first."
      return 1
    fi
  done
}

source "./git-addons-config" || { print_error_req "git-addons-config script file missing or not sourceable"; exit 2; }
if [ ! -z "${SCIPIO_ADDONS_CONFIG_EXT}" ] && [ -f "${SCIPIO_ADDONS_CONFIG_EXT}" ]; then
  source "${SCIPIO_ADDONS_CONFIG_EXT}" || { print_error_req "external '${SCIPIO_ADDONS_CONFIG_EXT}' config script file not sourceable"; exit 2; }
fi
if [ -f "./addons/scipio-dev/git-addons-config-dev" ]; then
  source "./addons/scipio-dev/git-addons-config-dev" || { print_error_req "addons/scipio-dev/git-addons-config-dev script file not sourceable"; exit 2; }
fi
if [ -f "./git-addons-config-dev" ]; then
  source "./git-addons-config-dev" || { print_error_req "git-addons-config-dev script file not sourceable"; exit 2; }
fi
if [ -f "./git-addons-config-local" ]; then
  source "./git-addons-config-local" || { print_error_req "git-addons-config-local script file not sourceable"; exit 2; }
fi
process_main "$@" || exit $?